@civitai/blocks-react 0.53.0 → 0.53.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/README.md CHANGED
@@ -35,7 +35,9 @@ export function App() {
35
35
  const rootRef = useRef<HTMLDivElement>(null);
36
36
  useBlockResize(rootRef); // host fits the iframe to content
37
37
 
38
- if (!ready) return <div ref={rootRef}>Loading…</div>;
38
+ // No ref on the pre-init skeleton — useBlockResize observes the real root
39
+ // whenever it mounts, including on a later render.
40
+ if (!ready) return <div>Loading…</div>;
39
41
  // `context` is a union keyed on slotId — narrow with the guard, not a cast.
40
42
  if (!isModelSlotContext(context)) return <div ref={rootRef}>Wrong slot.</div>;
41
43
 
@@ -153,6 +155,12 @@ const rootRef = useRef<HTMLDivElement>(null);
153
155
  useBlockResize(rootRef);
154
156
  ```
155
157
 
158
+ **The element may mount on a later render, and that is the normal case** — a
159
+ block renders a skeleton until `BLOCK_INIT` lands. The hook keys on the observed
160
+ *element*, so you do **not** need to pin the same `ref` to every branch of a
161
+ loading/ready conditional to keep the host resizing. Put it on the root you
162
+ actually want measured, in whichever branch renders it.
163
+
156
164
  > Also set `iframe.minHeight` in your manifest to the block's *real* rendered
157
165
  > height — a too-small minHeight makes the iframe seed short and grow-jump on
158
166
  > `BLOCK_READY` (CLS). Measure it in the dev harness (gotcha #53).
@@ -1039,7 +1047,7 @@ import {
1039
1047
  export function App() {
1040
1048
  const { ready, theme } = useBlockContext();
1041
1049
  const rootRef = useRef<HTMLDivElement>(null);
1042
- if (!ready) return <div ref={rootRef}>Loading…</div>;
1050
+ if (!ready) return <div>Loading…</div>;
1043
1051
 
1044
1052
  return (
1045
1053
  // GOTCHA #60 — theme your OWN root; that's what the pack reads.
@@ -9,11 +9,37 @@ import { type RefObject } from 'react';
9
9
  * - Inline path: `InlineTransport.sendMessage` is a no-op (the host DOM
10
10
  * reflows naturally), so the observer fires but no message goes out.
11
11
  *
12
+ * 🔴 THE ELEMENT MAY MOUNT ON A LATER RENDER, AND THAT IS THE NORMAL CASE.
13
+ * Every block renders a skeleton until `BLOCK_INIT` lands, so on the first
14
+ * render there is nothing to observe. This hook therefore keys on the OBSERVED
15
+ * ELEMENT, not on the ref wrapper's identity — the same reasoning written down
16
+ * at `useBlockBreakpoint.ts`'s effect, with one mechanical difference that
17
+ * matters:
18
+ *
19
+ * `useBlockBreakpoint` can compare `ref.current` read DURING RENDER, because
20
+ * it re-renders its own caller and so always gets another render in which to
21
+ * notice. This hook re-renders nobody. React attaches a ref during COMMIT,
22
+ * i.e. AFTER the render that mounts it, so a `[ref.current]` dependency read
23
+ * during render is a render behind and — with no further render coming —
24
+ * never catches up. Measured: with `[ref.current]` as the dependency, a
25
+ * component that mounts its root on the second render still observes
26
+ * nothing.
27
+ *
28
+ * So the effect runs on every render (no dependency array) and does its own
29
+ * identity check against the element it is already observing. The check is a
30
+ * reference compare; the observer is torn down and rebuilt only when the
31
+ * element actually changes.
32
+ *
33
+ * Because of this, a block does NOT need to pin the same `ref` to every branch
34
+ * of a loading/ready conditional to keep the host resizing. That workaround was
35
+ * load-bearing before this fix and is not any more.
36
+ *
12
37
  * @param ref - Ref to the block's root DOM element to observe.
13
38
  *
14
39
  * @example
15
40
  * const rootRef = useRef<HTMLDivElement>(null);
16
41
  * useBlockResize(rootRef); // host fits the iframe to content
42
+ * if (!ready) return <div>Loading…</div>; // no ref needed on this branch
17
43
  * return <div ref={rootRef}>…</div>;
18
44
  */
19
45
  export declare function useBlockResize(ref: RefObject<HTMLElement | null>): void;
@@ -1 +1 @@
1
- {"version":3,"file":"useBlockResize.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlockResize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIlD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,IAAI,CAiBvE"}
1
+ {"version":3,"file":"useBlockResize.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlockResize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAI1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,IAAI,CAyCvE"}
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useRef } from 'react';
2
2
  import { getTransport } from '../internal/singleton.js';
3
3
  /**
4
4
  * Observes the referenced element's height and asks the host to resize on
@@ -10,20 +10,67 @@ import { getTransport } from '../internal/singleton.js';
10
10
  * - Inline path: `InlineTransport.sendMessage` is a no-op (the host DOM
11
11
  * reflows naturally), so the observer fires but no message goes out.
12
12
  *
13
+ * 🔴 THE ELEMENT MAY MOUNT ON A LATER RENDER, AND THAT IS THE NORMAL CASE.
14
+ * Every block renders a skeleton until `BLOCK_INIT` lands, so on the first
15
+ * render there is nothing to observe. This hook therefore keys on the OBSERVED
16
+ * ELEMENT, not on the ref wrapper's identity — the same reasoning written down
17
+ * at `useBlockBreakpoint.ts`'s effect, with one mechanical difference that
18
+ * matters:
19
+ *
20
+ * `useBlockBreakpoint` can compare `ref.current` read DURING RENDER, because
21
+ * it re-renders its own caller and so always gets another render in which to
22
+ * notice. This hook re-renders nobody. React attaches a ref during COMMIT,
23
+ * i.e. AFTER the render that mounts it, so a `[ref.current]` dependency read
24
+ * during render is a render behind and — with no further render coming —
25
+ * never catches up. Measured: with `[ref.current]` as the dependency, a
26
+ * component that mounts its root on the second render still observes
27
+ * nothing.
28
+ *
29
+ * So the effect runs on every render (no dependency array) and does its own
30
+ * identity check against the element it is already observing. The check is a
31
+ * reference compare; the observer is torn down and rebuilt only when the
32
+ * element actually changes.
33
+ *
34
+ * Because of this, a block does NOT need to pin the same `ref` to every branch
35
+ * of a loading/ready conditional to keep the host resizing. That workaround was
36
+ * load-bearing before this fix and is not any more.
37
+ *
13
38
  * @param ref - Ref to the block's root DOM element to observe.
14
39
  *
15
40
  * @example
16
41
  * const rootRef = useRef<HTMLDivElement>(null);
17
42
  * useBlockResize(rootRef); // host fits the iframe to content
43
+ * if (!ready) return <div>Loading…</div>; // no ref needed on this branch
18
44
  * return <div ref={rootRef}>…</div>;
19
45
  */
20
46
  export function useBlockResize(ref) {
47
+ /** The element the live observer is watching. `null` = watching nothing. */
48
+ const observedRef = useRef(null);
49
+ const observerRef = useRef(null);
50
+ // Unmount-only teardown. Declared FIRST so that on a remount (React
51
+ // StrictMode's deliberate double-invoke) its cleanup — which clears
52
+ // `observedRef` — runs before the observe effect's setup re-runs and finds a
53
+ // clean slate. React runs every cleanup before any setup.
54
+ useEffect(() => {
55
+ return () => {
56
+ observerRef.current?.disconnect();
57
+ observerRef.current = null;
58
+ observedRef.current = null;
59
+ };
60
+ }, []);
61
+ // No dependency array on purpose — see the block comment above. The identity
62
+ // check below, not a dependency list, is what makes this cheap.
21
63
  useEffect(() => {
64
+ if (typeof ResizeObserver === 'undefined')
65
+ return;
22
66
  const el = ref.current;
67
+ if (el === observedRef.current)
68
+ return; // already observing exactly this
69
+ observerRef.current?.disconnect();
70
+ observerRef.current = null;
71
+ observedRef.current = el;
23
72
  if (!el)
24
73
  return;
25
- if (typeof ResizeObserver === 'undefined')
26
- return;
27
74
  const transport = getTransport();
28
75
  let lastHeight = -1;
29
76
  const observer = new ResizeObserver((entries) => {
@@ -34,7 +81,7 @@ export function useBlockResize(ref) {
34
81
  transport.sendMessage({ type: 'RESIZE_IFRAME', payload: { height } });
35
82
  });
36
83
  observer.observe(el);
37
- return () => observer.disconnect();
38
- }, [ref]);
84
+ observerRef.current = observer;
85
+ });
39
86
  }
40
87
  //# sourceMappingURL=useBlockResize.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useBlockResize.js","sourceRoot":"","sources":["../../src/hooks/useBlockResize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkB,MAAM,OAAO,CAAC;AAElD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,GAAkC;IAC/D,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC;QACvB,IAAI,CAAC,EAAE;YAAE,OAAO;QAChB,IAAI,OAAO,cAAc,KAAK,WAAW;YAAE,OAAO;QAElD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;QACjC,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC;QACpB,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,CAAC,OAAO,EAAE,EAAE;YAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,MAAM,IAAI,EAAE,CAAC,YAAY,CAAC,CAAC;YAC5E,IAAI,MAAM,KAAK,UAAU;gBAAE,OAAO;YAClC,UAAU,GAAG,MAAM,CAAC;YACpB,SAAS,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;QACxE,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACrB,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;AACZ,CAAC"}
1
+ {"version":3,"file":"useBlockResize.js","sourceRoot":"","sources":["../../src/hooks/useBlockResize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAkB,MAAM,OAAO,CAAC;AAE1D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,UAAU,cAAc,CAAC,GAAkC;IAC/D,4EAA4E;IAC5E,MAAM,WAAW,GAAG,MAAM,CAAqB,IAAI,CAAC,CAAC;IACrD,MAAM,WAAW,GAAG,MAAM,CAAwB,IAAI,CAAC,CAAC;IAExD,oEAAoE;IACpE,oEAAoE;IACpE,6EAA6E;IAC7E,0DAA0D;IAC1D,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,GAAG,EAAE;YACV,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;YAClC,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;YAC3B,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC7B,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,6EAA6E;IAC7E,gEAAgE;IAChE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,cAAc,KAAK,WAAW;YAAE,OAAO;QAElD,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC;QACvB,IAAI,EAAE,KAAK,WAAW,CAAC,OAAO;YAAE,OAAO,CAAC,iCAAiC;QAEzE,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;QAClC,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC3B,WAAW,CAAC,OAAO,GAAG,EAAE,CAAC;QACzB,IAAI,CAAC,EAAE;YAAE,OAAO;QAEhB,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;QACjC,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC;QACpB,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,CAAC,OAAO,EAAE,EAAE;YAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,MAAM,IAAI,EAAE,CAAC,YAAY,CAAC,CAAC;YAC5E,IAAI,MAAM,KAAK,UAAU;gBAAE,OAAO;YAClC,UAAU,GAAG,MAAM,CAAC;YACpB,SAAS,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;QACxE,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACrB,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAC;IACjC,CAAC,CAAC,CAAC;AACL,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/blocks-react",
3
- "version": "0.53.0",
3
+ "version": "0.53.1",
4
4
  "description": "React hooks and iframe transport for Civitai Apps. Pairs with @civitai/app-sdk/blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -122,7 +122,7 @@
122
122
  "react-dom": "^19.0.0",
123
123
  "typescript": "^5.9.2",
124
124
  "vitest": "^4.1.11",
125
- "@civitai/app-sdk": "^0.45.0"
125
+ "@civitai/app-sdk": "^0.46.0"
126
126
  },
127
127
  "publishConfig": {
128
128
  "access": "public"