@proto-labs-inc/rig-core 0.1.0 → 0.1.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.
@@ -0,0 +1,20 @@
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
+ }
@@ -0,0 +1,95 @@
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
+ }
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export declare const INTERNAL_PREVIEW_THEME_PARAM = "__protoTheme";
10
10
  export declare function applyPreviewTheme(theme: ResolvedPreviewTheme): void;
11
11
  /** All addressable state, mirrored to the URL. Keys: "state" | "v.<component>". */
12
12
  export declare class RigStore {
13
+ private manifest;
13
14
  private values;
14
15
  private listeners;
15
16
  constructor(manifest: Manifest);
package/dist/index.js CHANGED
@@ -4,9 +4,10 @@
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
- import { PROTOCOL_VERSION, STATE_PARAM, variantParam, isFrameToRig, } from "@proto-labs-inc/wire";
7
+ import { PROTOCOL_VERSION, STATE_PARAM, variantParam, applyVariantSelection, isFrameToRig, } from "@proto-labs-inc/wire";
8
+ import { ElementPreview } from "./element-preview.js";
8
9
  export { STATE_PARAM, variantParam } from "@proto-labs-inc/wire";
9
- const RIG_VERSION = "0.1.0";
10
+ const RIG_VERSION = "0.1.1";
10
11
  export const INTERNAL_PREVIEW_THEME_PARAM = "__protoTheme";
11
12
  export function applyPreviewTheme(theme) {
12
13
  if (typeof document === "undefined")
@@ -29,9 +30,11 @@ function searchForFrame() {
29
30
  }
30
31
  /** All addressable state, mirrored to the URL. Keys: "state" | "v.<component>". */
31
32
  export class RigStore {
33
+ manifest;
32
34
  values = new Map();
33
35
  listeners = new Set();
34
36
  constructor(manifest) {
37
+ this.manifest = manifest;
35
38
  if (typeof window === "undefined")
36
39
  return;
37
40
  const params = new URLSearchParams(window.location.search);
@@ -61,6 +64,13 @@ export class RigStore {
61
64
  return this.values.get(key);
62
65
  }
63
66
  set(key, value) {
67
+ if (key.startsWith("v.")) {
68
+ const params = new URLSearchParams([...this.values]);
69
+ if (!applyVariantSelection(this.manifest.variantSets, params, key.slice(2), value))
70
+ return;
71
+ this.setMany([...params.entries()]);
72
+ return;
73
+ }
64
74
  this.setMany([[key, value]]);
65
75
  }
66
76
  setMany(entries) {
@@ -267,6 +277,7 @@ async function captureThumbnail() {
267
277
  restingState.remove();
268
278
  }
269
279
  }
280
+ const elementPreview = new ElementPreview(postToFrame);
270
281
  class ElementPicker {
271
282
  overlay = null;
272
283
  highlight = null;
@@ -277,6 +288,7 @@ class ElementPicker {
277
288
  enable(mode) {
278
289
  if (typeof document === "undefined")
279
290
  return;
291
+ elementPreview.release();
280
292
  if (this.active || this.overlay)
281
293
  this.disable();
282
294
  this.active = true;
@@ -361,9 +373,11 @@ class ElementPicker {
361
373
  if (this.mode === "element") {
362
374
  const rect = this.rectFor(clickedTarget);
363
375
  const text = clickedTarget.textContent?.replace(/\s+/g, " ").trim() || null;
376
+ const inspection = elementPreview.inspect(clickedTarget);
364
377
  this.disable();
365
378
  postToFrame({
366
379
  type: "element-target-picked",
380
+ inspection,
367
381
  selector: selectorFor(clickedTarget),
368
382
  tagName: clickedTarget.tagName.toLowerCase(),
369
383
  text: text?.slice(0, 160) ?? null,
@@ -494,8 +508,20 @@ export function wireRig(store, manifest) {
494
508
  };
495
509
  media.addEventListener("change", onSystemTheme);
496
510
  const onMessage = (e) => {
511
+ if (e.source !== window.parent)
512
+ return;
497
513
  if (!isFrameToRig(e.data))
498
514
  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();
499
525
  if (e.data.type === "set-state") {
500
526
  const state = manifest.states.find((s) => s.id === e.data.id);
501
527
  store.setState(e.data.id, state?.variants ?? {});
@@ -565,5 +591,6 @@ export function wireRig(store, manifest) {
565
591
  media.removeEventListener("change", onSystemTheme);
566
592
  window.removeEventListener("message", onMessage);
567
593
  elementPicker?.disable();
594
+ elementPreview.release();
568
595
  };
569
596
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proto-labs-inc/rig-core",
3
- "version": "0.1.0",
3
+ "version": "0.1.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.1.0"
31
+ "@proto-labs-inc/wire": "^0.1.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "typescript": "~6.0.2",