@proto-labs-inc/rig-core 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Proto Labs Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @proto-labs-inc/rig-core
2
+
3
+ The rig, framework-free: URL-state sync, the postMessage bridge to Proto's
4
+ Frame, and comment mode. Prototypes use it through an adapter,
5
+ `@proto-labs-inc/rig` for React or `@proto-labs-inc/rig-vue` for Vue.
6
+ Comment capture lazy-imports `modern-screenshot` from the prototype's own
7
+ dependencies; without it a comment still works, with no picture.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The rig's framework-free core: URL-state sync, the bridge, and comment
3
+ * mode. Everything here is plain DOM + history + postMessage — no framework.
4
+ * Framework adapters (@proto-labs-inc/rig for React, @proto-labs-inc/rig-vue for Vue) wrap
5
+ * this with their own idioms and nothing else.
6
+ */
7
+ import { type Manifest, type ResolvedPreviewTheme, type RigToFrame } from "@proto-labs-inc/wire";
8
+ export { STATE_PARAM, variantParam } from "@proto-labs-inc/wire";
9
+ export declare const INTERNAL_PREVIEW_THEME_PARAM = "__protoTheme";
10
+ export declare function applyPreviewTheme(theme: ResolvedPreviewTheme): void;
11
+ /** All addressable state, mirrored to the URL. Keys: "state" | "v.<component>". */
12
+ export declare class RigStore {
13
+ private values;
14
+ private listeners;
15
+ constructor(manifest: Manifest);
16
+ get(key: string): string | undefined;
17
+ set(key: string, value: string): void;
18
+ setMany(entries: Array<[string, string]>): void;
19
+ setState(id: string, variants: Record<string, string>): void;
20
+ subscribe: (l: () => void) => () => void;
21
+ private syncUrl;
22
+ }
23
+ export declare function postToFrame(msg: RigToFrame): void;
24
+ /**
25
+ * Wire a store to the bridge: listen for Frame messages, announce the rig,
26
+ * and report the boot-time state (the deep-link fix). Returns a disposer.
27
+ * Call client-side only.
28
+ */
29
+ export declare function wireRig(store: RigStore, manifest: Manifest): () => void;
package/dist/index.js ADDED
@@ -0,0 +1,569 @@
1
+ /**
2
+ * The rig's framework-free core: URL-state sync, the bridge, and comment
3
+ * mode. Everything here is plain DOM + history + postMessage — no framework.
4
+ * Framework adapters (@proto-labs-inc/rig for React, @proto-labs-inc/rig-vue for Vue) wrap
5
+ * this with their own idioms and nothing else.
6
+ */
7
+ import { PROTOCOL_VERSION, STATE_PARAM, variantParam, isFrameToRig, } from "@proto-labs-inc/wire";
8
+ export { STATE_PARAM, variantParam } from "@proto-labs-inc/wire";
9
+ const RIG_VERSION = "0.1.0";
10
+ export const INTERNAL_PREVIEW_THEME_PARAM = "__protoTheme";
11
+ export function applyPreviewTheme(theme) {
12
+ if (typeof document === "undefined")
13
+ return;
14
+ const root = document.documentElement;
15
+ root.dataset.protoTheme = theme;
16
+ root.classList.toggle("dark", theme === "dark");
17
+ root.style.colorScheme = theme;
18
+ }
19
+ function resolvedSystemTheme() {
20
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
21
+ ? "dark"
22
+ : "light";
23
+ }
24
+ function searchForFrame() {
25
+ const params = new URLSearchParams(window.location.search);
26
+ params.delete(INTERNAL_PREVIEW_THEME_PARAM);
27
+ const search = params.toString();
28
+ return search ? `?${search}` : "";
29
+ }
30
+ /** All addressable state, mirrored to the URL. Keys: "state" | "v.<component>". */
31
+ export class RigStore {
32
+ values = new Map();
33
+ listeners = new Set();
34
+ constructor(manifest) {
35
+ if (typeof window === "undefined")
36
+ return;
37
+ const params = new URLSearchParams(window.location.search);
38
+ const bootTheme = params.get(INTERNAL_PREVIEW_THEME_PARAM);
39
+ applyPreviewTheme(bootTheme === "light" || bootTheme === "dark"
40
+ ? bootTheme
41
+ : resolvedSystemTheme());
42
+ for (const [k, v] of params) {
43
+ if (k === STATE_PARAM || k.startsWith("v."))
44
+ this.values.set(k, v);
45
+ }
46
+ const state = manifest.states.find((s) => s.id === this.values.get(STATE_PARAM));
47
+ let hydrated = false;
48
+ for (const [component, variant] of Object.entries(state?.variants ?? {})) {
49
+ const key = variantParam(component);
50
+ if (this.values.has(key))
51
+ continue;
52
+ this.values.set(key, variant);
53
+ params.set(key, variant);
54
+ hydrated = true;
55
+ }
56
+ if (hydrated) {
57
+ window.history.replaceState(null, "", `?${params.toString()}`);
58
+ }
59
+ }
60
+ get(key) {
61
+ return this.values.get(key);
62
+ }
63
+ set(key, value) {
64
+ this.setMany([[key, value]]);
65
+ }
66
+ setMany(entries) {
67
+ let changed = false;
68
+ for (const [key, value] of entries) {
69
+ if (this.values.get(key) === value)
70
+ continue;
71
+ this.values.set(key, value);
72
+ changed = true;
73
+ }
74
+ if (!changed)
75
+ return;
76
+ this.syncUrl();
77
+ this.listeners.forEach((l) => l());
78
+ }
79
+ setState(id, variants) {
80
+ const next = new Map([[STATE_PARAM, id]]);
81
+ for (const [component, variant] of Object.entries(variants)) {
82
+ next.set(variantParam(component), variant);
83
+ }
84
+ let changed = this.values.size !== next.size;
85
+ if (!changed) {
86
+ for (const [key, value] of next) {
87
+ if (this.values.get(key) !== value) {
88
+ changed = true;
89
+ break;
90
+ }
91
+ }
92
+ }
93
+ if (!changed)
94
+ return;
95
+ this.values = next;
96
+ this.syncUrl();
97
+ this.listeners.forEach((l) => l());
98
+ }
99
+ subscribe = (l) => {
100
+ this.listeners.add(l);
101
+ return () => {
102
+ this.listeners.delete(l);
103
+ };
104
+ };
105
+ syncUrl() {
106
+ const params = new URLSearchParams(window.location.search);
107
+ for (const key of [...params.keys()]) {
108
+ if (key === STATE_PARAM || key.startsWith("v."))
109
+ params.delete(key);
110
+ }
111
+ for (const [k, v] of this.values)
112
+ params.set(k, v);
113
+ const search = `?${params.toString()}`;
114
+ window.history.replaceState(null, "", search);
115
+ postToFrame({ type: "state-changed", search: searchForFrame() });
116
+ }
117
+ }
118
+ export function postToFrame(msg) {
119
+ // P0: the Frame's origin isn't known to the prototype; "*" is acceptable
120
+ // because nothing sensitive crosses the bridge. Revisit with link protection.
121
+ if (typeof window !== "undefined" && window.parent !== window)
122
+ window.parent.postMessage(msg, "*");
123
+ }
124
+ // ─── comment mode ──────────────────────────────────────────────────
125
+ // The Frame can't touch this document (cross-origin iframe), so the blue
126
+ // selector runs here: hit-test [data-proto-id] under the cursor, draw the
127
+ // highlight, and on click capture the view with the highlight baked in.
128
+ // The highlight is Proto blue (#0088FF): 2px solid at 55%, a 6% fill,
129
+ // radius 4px, 0.12s ease-out entry; the picker layer sits at z-index 99997.
130
+ const COMMENT_BLUE = "#0088FF";
131
+ const TARGET_ATTR = "data-proto-id";
132
+ function selectorFor(element) {
133
+ if (element === document.documentElement)
134
+ return "html";
135
+ if (element === document.body)
136
+ return "body";
137
+ const protoId = element.getAttribute(TARGET_ATTR);
138
+ if (protoId)
139
+ return `[${TARGET_ATTR}="${CSS.escape(protoId)}"]`;
140
+ if (element.id) {
141
+ const idSelector = `#${CSS.escape(element.id)}`;
142
+ if (document.querySelectorAll(idSelector).length === 1)
143
+ return idSelector;
144
+ }
145
+ const owner = element.closest(`[${TARGET_ATTR}]`);
146
+ const boundary = owner ?? document.body;
147
+ const segments = [];
148
+ let current = element;
149
+ while (current && current !== boundary) {
150
+ const tag = current.tagName.toLowerCase();
151
+ const currentTagName = current.tagName;
152
+ const siblings = current.parentElement
153
+ ? [...current.parentElement.children].filter((sibling) => sibling.tagName === currentTagName)
154
+ : [];
155
+ const index = siblings.indexOf(current);
156
+ segments.unshift(siblings.length > 1 && index >= 0 ? `${tag}:nth-of-type(${index + 1})` : tag);
157
+ current = current.parentElement;
158
+ }
159
+ const base = owner
160
+ ? `[${TARGET_ATTR}="${CSS.escape(owner.getAttribute(TARGET_ATTR) ?? "")}"]`
161
+ : boundary === element
162
+ ? element.tagName.toLowerCase()
163
+ : "body";
164
+ return segments.length > 0 ? `${base} > ${segments.join(" > ")}` : base;
165
+ }
166
+ async function waitForCaptureReady() {
167
+ const pendingImages = [...document.images]
168
+ .filter((image) => !image.complete)
169
+ .map((image) => new Promise((resolve) => {
170
+ image.addEventListener("load", () => resolve(), { once: true });
171
+ image.addEventListener("error", () => resolve(), { once: true });
172
+ }));
173
+ const assetsReady = Promise.all([document.fonts.ready, ...pendingImages]);
174
+ await Promise.race([
175
+ assetsReady,
176
+ new Promise((resolve) => window.setTimeout(resolve, 3000)),
177
+ ]);
178
+ // requestAnimationFrame may be suspended for an invisible iframe. The timer
179
+ // also lets entrance transitions and canvas-backed backgrounds reach their
180
+ // resting state before the gallery captures them.
181
+ await new Promise((resolve) => window.setTimeout(resolve, 1500));
182
+ }
183
+ async function captureViewport(scale = Math.min(window.devicePixelRatio || 1, 2)) {
184
+ try {
185
+ // Optional peer provided by the prototype, not the rig.
186
+ const mod = (await import("modern-screenshot"));
187
+ const root = document.body;
188
+ const viewportWidth = document.documentElement.clientWidth;
189
+ const viewportHeight = document.documentElement.clientHeight;
190
+ const scrollX = window.scrollX || document.documentElement.scrollLeft;
191
+ const scrollY = window.scrollY || document.documentElement.scrollTop;
192
+ const contentWidth = Math.max(viewportWidth, document.documentElement.scrollWidth, root.scrollWidth);
193
+ const contentHeight = Math.max(viewportHeight, document.documentElement.scrollHeight, root.scrollHeight);
194
+ return await mod.domToDataUrl(root, {
195
+ scale,
196
+ width: viewportWidth,
197
+ height: viewportHeight,
198
+ features: { restoreScrollPosition: true },
199
+ style: {
200
+ margin: "0",
201
+ width: `${contentWidth}px`,
202
+ height: `${contentHeight}px`,
203
+ transform: `translate(${-scrollX}px, ${-scrollY}px)`,
204
+ transformOrigin: "top left",
205
+ overflow: "visible",
206
+ },
207
+ onCloneNode: (cloned) => {
208
+ if (!(cloned instanceof HTMLElement) || (!scrollX && !scrollY))
209
+ return;
210
+ const fixed = [cloned, ...cloned.querySelectorAll("*")].filter((element) => element.style.position === "fixed");
211
+ for (const element of fixed) {
212
+ const existing = element.style.transform;
213
+ element.style.transform =
214
+ `translate(${scrollX}px, ${scrollY}px)` +
215
+ (existing && existing !== "none" ? ` ${existing}` : "");
216
+ }
217
+ },
218
+ });
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ }
224
+ async function captureThumbnail() {
225
+ const restingState = document.createElement("style");
226
+ restingState.textContent = `
227
+ *, *::before, *::after {
228
+ animation-delay: 0s !important;
229
+ animation-duration: 0s !important;
230
+ transition-delay: 0s !important;
231
+ transition-duration: 0s !important;
232
+ }
233
+ .opacity-0,
234
+ [style*="opacity: 0"] {
235
+ opacity: 1 !important;
236
+ transform: none !important;
237
+ }
238
+ `;
239
+ document.head.append(restingState);
240
+ try {
241
+ for (const animation of document.getAnimations()) {
242
+ try {
243
+ animation.finish();
244
+ }
245
+ catch {
246
+ animation.cancel();
247
+ }
248
+ }
249
+ await waitForCaptureReady();
250
+ // Optional peer provided by the prototype, not the rig.
251
+ const mod = (await import("modern-screenshot"));
252
+ const bodyBackground = window.getComputedStyle(document.body).backgroundColor;
253
+ const htmlBackground = window.getComputedStyle(document.documentElement).backgroundColor;
254
+ const backgroundColor = bodyBackground !== "rgba(0, 0, 0, 0)" ? bodyBackground : htmlBackground;
255
+ return await mod.domToDataUrl(document.documentElement, {
256
+ backgroundColor,
257
+ timeout: 5000,
258
+ width: window.innerWidth,
259
+ height: window.innerHeight,
260
+ scale: 1,
261
+ });
262
+ }
263
+ catch {
264
+ return null;
265
+ }
266
+ finally {
267
+ restingState.remove();
268
+ }
269
+ }
270
+ class ElementPicker {
271
+ overlay = null;
272
+ highlight = null;
273
+ cursorStyle = null;
274
+ target = null;
275
+ active = false;
276
+ mode = null;
277
+ enable(mode) {
278
+ if (typeof document === "undefined")
279
+ return;
280
+ if (this.active || this.overlay)
281
+ this.disable();
282
+ this.active = true;
283
+ this.mode = mode;
284
+ this.cursorStyle = document.createElement("style");
285
+ this.cursorStyle.setAttribute("data-proto-picker-cursor", "");
286
+ this.cursorStyle.textContent =
287
+ "html, body, body * { cursor: crosshair !important; }";
288
+ document.head.appendChild(this.cursorStyle);
289
+ const blue = COMMENT_BLUE;
290
+ this.overlay = document.createElement("div");
291
+ this.overlay.setAttribute("data-proto-rig-overlay", "");
292
+ Object.assign(this.overlay.style, {
293
+ position: "fixed",
294
+ inset: "0",
295
+ zIndex: "99997",
296
+ pointerEvents: "none",
297
+ });
298
+ this.highlight = document.createElement("div");
299
+ Object.assign(this.highlight.style, {
300
+ position: "absolute",
301
+ border: `2px solid color-mix(in srgb, ${blue} 55%, transparent)`,
302
+ borderRadius: "4px",
303
+ backgroundColor: `color-mix(in srgb, ${blue} 6%, transparent)`,
304
+ pointerEvents: "none",
305
+ boxSizing: "border-box",
306
+ opacity: "0",
307
+ transition: "opacity 0.12s ease-out",
308
+ willChange: "opacity",
309
+ });
310
+ this.overlay.appendChild(this.highlight);
311
+ document.body.appendChild(this.overlay);
312
+ document.addEventListener("pointermove", this.onMove, true);
313
+ document.addEventListener("click", this.onClick, true);
314
+ document.addEventListener("keydown", this.onKey, true);
315
+ }
316
+ disable() {
317
+ if (!this.active && !this.overlay && !this.cursorStyle)
318
+ return;
319
+ this.active = false;
320
+ document.removeEventListener("pointermove", this.onMove, true);
321
+ document.removeEventListener("click", this.onClick, true);
322
+ document.removeEventListener("keydown", this.onKey, true);
323
+ this.overlay?.remove();
324
+ this.cursorStyle?.remove();
325
+ this.overlay = null;
326
+ this.highlight = null;
327
+ this.cursorStyle = null;
328
+ this.target = null;
329
+ this.mode = null;
330
+ }
331
+ freezeSelection() {
332
+ this.active = false;
333
+ document.removeEventListener("pointermove", this.onMove, true);
334
+ document.removeEventListener("click", this.onClick, true);
335
+ document.removeEventListener("keydown", this.onKey, true);
336
+ this.cursorStyle?.remove();
337
+ this.cursorStyle = null;
338
+ }
339
+ onMove = (e) => {
340
+ const hit = this.targetAt(e.clientX, e.clientY);
341
+ if (hit === this.target)
342
+ return;
343
+ this.target = hit;
344
+ if (!this.highlight)
345
+ return;
346
+ const rect = this.rectFor(hit);
347
+ Object.assign(this.highlight.style, {
348
+ left: `${rect.left}px`,
349
+ top: `${rect.top}px`,
350
+ width: `${rect.width}px`,
351
+ height: `${rect.height}px`,
352
+ opacity: "1",
353
+ });
354
+ };
355
+ onClick = (e) => {
356
+ e.preventDefault();
357
+ e.stopPropagation();
358
+ const clickedTarget = this.targetAt(e.clientX, e.clientY);
359
+ this.target = clickedTarget;
360
+ const targetId = clickedTarget?.getAttribute(TARGET_ATTR) ?? null;
361
+ if (this.mode === "element") {
362
+ const rect = this.rectFor(clickedTarget);
363
+ const text = clickedTarget.textContent?.replace(/\s+/g, " ").trim() || null;
364
+ this.disable();
365
+ postToFrame({
366
+ type: "element-target-picked",
367
+ selector: selectorFor(clickedTarget),
368
+ tagName: clickedTarget.tagName.toLowerCase(),
369
+ text: text?.slice(0, 160) ?? null,
370
+ anchor: {
371
+ x: rect.left,
372
+ y: rect.top,
373
+ width: rect.width,
374
+ height: rect.height,
375
+ },
376
+ });
377
+ return;
378
+ }
379
+ if (this.mode === "variantSet") {
380
+ if (!targetId)
381
+ return;
382
+ const rect = this.rectFor(clickedTarget);
383
+ this.disable();
384
+ postToFrame({
385
+ type: "variant-set-target-picked",
386
+ targetId,
387
+ anchor: {
388
+ x: rect.left,
389
+ y: rect.top,
390
+ width: rect.width,
391
+ height: rect.height,
392
+ },
393
+ });
394
+ return;
395
+ }
396
+ const rect = this.rectFor(clickedTarget);
397
+ if (this.highlight) {
398
+ Object.assign(this.highlight.style, {
399
+ left: `${rect.left}px`,
400
+ top: `${rect.top}px`,
401
+ width: `${rect.width}px`,
402
+ height: `${rect.height}px`,
403
+ opacity: "1",
404
+ });
405
+ }
406
+ this.freezeSelection();
407
+ postToFrame({
408
+ type: "comment-target-picked",
409
+ targetId,
410
+ anchor: {
411
+ x: rect.left,
412
+ y: rect.top,
413
+ width: rect.width,
414
+ height: rect.height,
415
+ },
416
+ });
417
+ };
418
+ targetAt(x, y) {
419
+ const hit = document.elementFromPoint(x, y);
420
+ if (this.mode === "element")
421
+ return hit ?? document.body;
422
+ return hit?.closest(`[${TARGET_ATTR}]`) ?? document.body;
423
+ }
424
+ rectFor(target) {
425
+ if (target === document.body || target === document.documentElement) {
426
+ return {
427
+ left: 0,
428
+ top: 0,
429
+ width: document.documentElement.clientWidth,
430
+ height: document.documentElement.clientHeight,
431
+ };
432
+ }
433
+ return target.getBoundingClientRect();
434
+ }
435
+ onKey = (e) => {
436
+ if (e.key !== "Escape")
437
+ return;
438
+ e.preventDefault();
439
+ const mode = this.mode;
440
+ this.disable();
441
+ postToFrame({
442
+ type: mode === "variantSet"
443
+ ? "variant-set-picker-exited"
444
+ : mode === "element"
445
+ ? "element-picker-exited"
446
+ : "comment-mode-exited",
447
+ });
448
+ };
449
+ async captureSelection(requestId) {
450
+ if (!this.target)
451
+ return;
452
+ const targetId = this.target.getAttribute(TARGET_ATTR);
453
+ const rect = this.rectFor(this.target);
454
+ const anchor = {
455
+ x: rect.left,
456
+ y: rect.top,
457
+ width: rect.width,
458
+ height: rect.height,
459
+ };
460
+ if (this.highlight) {
461
+ Object.assign(this.highlight.style, {
462
+ left: `${rect.left}px`,
463
+ top: `${rect.top}px`,
464
+ width: `${rect.width}px`,
465
+ height: `${rect.height}px`,
466
+ });
467
+ }
468
+ // The page is already interactive when the user picks a comment target.
469
+ // Capture immediately: thumbnail readiness delays can consume the Frame's
470
+ // request timeout on image-heavy prototypes.
471
+ const image = await captureViewport();
472
+ this.disable();
473
+ postToFrame({
474
+ type: "comment-capture",
475
+ targetId,
476
+ image,
477
+ requestId,
478
+ anchor,
479
+ });
480
+ }
481
+ }
482
+ const elementPicker = typeof window === "undefined" ? null : new ElementPicker();
483
+ /**
484
+ * Wire a store to the bridge: listen for Frame messages, announce the rig,
485
+ * and report the boot-time state (the deep-link fix). Returns a disposer.
486
+ * Call client-side only.
487
+ */
488
+ export function wireRig(store, manifest) {
489
+ let controlledTheme = false;
490
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
491
+ const onSystemTheme = () => {
492
+ if (!controlledTheme)
493
+ applyPreviewTheme(resolvedSystemTheme());
494
+ };
495
+ media.addEventListener("change", onSystemTheme);
496
+ const onMessage = (e) => {
497
+ if (!isFrameToRig(e.data))
498
+ return; // tolerate unknown messages silently
499
+ if (e.data.type === "set-state") {
500
+ const state = manifest.states.find((s) => s.id === e.data.id);
501
+ store.setState(e.data.id, state?.variants ?? {});
502
+ }
503
+ else if (e.data.type === "set-theme") {
504
+ controlledTheme = true;
505
+ applyPreviewTheme(e.data.theme);
506
+ }
507
+ else if (e.data.type === "set-variant")
508
+ store.set(variantParam(e.data.component), e.data.variant);
509
+ else if (e.data.type === "measure-target") {
510
+ const target = Array.from(document.querySelectorAll(`[${TARGET_ATTR}]`)).find((candidate) => candidate.getAttribute(TARGET_ATTR) === e.data.id);
511
+ const rect = target?.getBoundingClientRect();
512
+ postToFrame({
513
+ type: "target-measured",
514
+ targetId: e.data.id,
515
+ anchor: rect
516
+ ? {
517
+ x: rect.left,
518
+ y: rect.top,
519
+ width: rect.width,
520
+ height: rect.height,
521
+ }
522
+ : null,
523
+ });
524
+ }
525
+ else if (e.data.type === "set-comment-mode") {
526
+ if (e.data.on)
527
+ elementPicker?.enable("comment");
528
+ else
529
+ elementPicker?.disable();
530
+ }
531
+ else if (e.data.type === "capture-comment")
532
+ void elementPicker?.captureSelection(e.data.requestId);
533
+ else if (e.data.type === "set-variant-set-picker") {
534
+ if (e.data.on)
535
+ elementPicker?.enable("variantSet");
536
+ else
537
+ elementPicker?.disable();
538
+ }
539
+ else if (e.data.type === "set-element-picker") {
540
+ if (e.data.on)
541
+ elementPicker?.enable("element");
542
+ else
543
+ elementPicker?.disable();
544
+ }
545
+ };
546
+ window.addEventListener("message", onMessage);
547
+ postToFrame({
548
+ type: "hello",
549
+ protocolVersion: PROTOCOL_VERSION,
550
+ rigVersion: RIG_VERSION,
551
+ name: manifest.name,
552
+ });
553
+ postToFrame({ type: "state-changed", search: searchForFrame() });
554
+ const thumbnailRequestId = new URLSearchParams(window.location.search).get("_protoThumbnail");
555
+ if (thumbnailRequestId) {
556
+ void captureThumbnail().then((image) => {
557
+ postToFrame({
558
+ type: "thumbnail-capture",
559
+ image,
560
+ requestId: thumbnailRequestId,
561
+ });
562
+ });
563
+ }
564
+ return () => {
565
+ media.removeEventListener("change", onSystemTheme);
566
+ window.removeEventListener("message", onMessage);
567
+ elementPicker?.disable();
568
+ };
569
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@proto-labs-inc/rig-core",
3
+ "version": "0.1.0",
4
+ "description": "Proto's rig, framework-free: URL-state sync, the postMessage bridge to the Frame, and comment mode.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/proto-labs-inc/proto.git",
9
+ "directory": "packages/rig-core"
10
+ },
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "peerDependencies": {
23
+ "modern-screenshot": ">=4"
24
+ },
25
+ "peerDependenciesMeta": {
26
+ "modern-screenshot": {
27
+ "optional": true
28
+ }
29
+ },
30
+ "dependencies": {
31
+ "@proto-labs-inc/wire": "^0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "~6.0.2",
35
+ "modern-screenshot": "^4.7.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "typecheck": "tsc --noEmit"
43
+ },
44
+ "main": "./dist/index.js",
45
+ "types": "./dist/index.d.ts"
46
+ }