@notionhq/custom-blocks 0.1.20 → 0.1.21
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 +2 -2
- package/dist/bridge/SandboxBridge.d.ts +11 -0
- package/dist/bridge/SandboxBridge.d.ts.map +1 -1
- package/dist/bridge/SandboxBridge.js +79 -8
- package/dist/bridge/sandboxClient.d.ts +1 -0
- package/dist/bridge/sandboxClient.d.ts.map +1 -1
- package/dist/bridge/sandboxClient.js +3 -0
- package/dist/protocol/messages/initResult.d.ts +2 -0
- package/dist/protocol/messages/initResult.d.ts.map +1 -1
- package/dist/protocol/messages/initResult.js +3 -0
- package/dist/protocol/messages/sandboxToHost.d.ts +1 -0
- package/dist/protocol/messages/sandboxToHost.d.ts.map +1 -1
- package/dist/react/NotionCustomBlock.d.ts +3 -4
- package/dist/react/NotionCustomBlock.d.ts.map +1 -1
- package/dist/react/NotionCustomBlock.js +12 -2
- package/dist/react/useCustomBlockAutoResize.d.ts +4 -8
- package/dist/react/useCustomBlockAutoResize.d.ts.map +1 -1
- package/dist/react/useCustomBlockAutoResize.js +4 -8
- package/dist/version.js +1 -1
- package/docs/lifecycle.md +18 -23
- package/package.json +1 -1
- package/src/bridge/SandboxBridge.ts +93 -8
- package/src/bridge/sandboxClient.ts +4 -0
- package/src/react/NotionCustomBlock.tsx +16 -6
- package/src/react/useCustomBlockAutoResize.ts +4 -8
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ export function App() {
|
|
|
81
81
|
}
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
`<NotionCustomBlock>` runs the SDK ↔ host handshake (`connect` → `init` → `initResult`) and only mounts `children` once
|
|
84
|
+
`<NotionCustomBlock>` runs the SDK ↔ host handshake (`connect` → `init` → `initResult`) and only mounts `children` once the host state is available. It measures the initial content before sending `initResult`, so the host can reveal the iframe at its correct height. Inside the wrapper, every hook returns non-nullable values — there's no separate gating component to write. It continues observing content height changes automatically.
|
|
85
85
|
|
|
86
86
|
## Notion design tokens
|
|
87
87
|
|
|
@@ -97,7 +97,7 @@ The stylesheet is scoped rather than installed globally. Render portals inside `
|
|
|
97
97
|
|
|
98
98
|
API surface, one page per category. Import framework-neutral APIs from `@notionhq/custom-blocks`; import React hooks and components from `@notionhq/custom-blocks/react`. Hover docs in your editor cover the per-field detail; these pages cover usage shape and the gotchas.
|
|
99
99
|
|
|
100
|
-
- [`docs/lifecycle.md`](./docs/lifecycle.md) — `<NotionCustomBlock>`, `useCustomBlockInit`, `initCustomBlock`, `
|
|
100
|
+
- [`docs/lifecycle.md`](./docs/lifecycle.md) — `<NotionCustomBlock>`, `useCustomBlockInit`, `initCustomBlock`, `NotInIframeError`, `useCustomBlockAutoResize`. The handshake, the React wrapper, sizing.
|
|
101
101
|
- [`docs/block-location.md`](./docs/block-location.md) — `useBlockId`, `useParent`, `usePage`, `useTheme`, and `useContrastMode`. Where the block sits and how to read the host's appearance.
|
|
102
102
|
- [`docs/data-sources.md`](./docs/data-sources.md) — `useDataSource`, `useManifest`, `customBlock.getManifest`, the row, property, and date-value types, plus a worked example.
|
|
103
103
|
- [`docs/pages.md`](./docs/pages.md) — `pages.create / get / update / delete`, parent variants (including the recommended `data_source_key`), property input shapes.
|
|
@@ -26,6 +26,8 @@ export declare class SandboxBridge {
|
|
|
26
26
|
private hasSentConnect;
|
|
27
27
|
private hasReceivedInit;
|
|
28
28
|
private initializationId;
|
|
29
|
+
private hasSentInitResult;
|
|
30
|
+
private pendingOutboundMessages;
|
|
29
31
|
private latestDataSourceBindings;
|
|
30
32
|
private isMockState;
|
|
31
33
|
private isListening;
|
|
@@ -40,6 +42,9 @@ export declare class SandboxBridge {
|
|
|
40
42
|
awaitInit(signal?: AbortSignal): Promise<void>;
|
|
41
43
|
sendConnect(manifestResult: ManifestLoadResult): void;
|
|
42
44
|
private postToHost;
|
|
45
|
+
private sendToHost;
|
|
46
|
+
private flushPendingOutboundMessages;
|
|
47
|
+
private getMessageType;
|
|
43
48
|
private notify;
|
|
44
49
|
private handleMessage;
|
|
45
50
|
subscribe(listener: () => void): () => boolean;
|
|
@@ -51,7 +56,13 @@ export declare class SandboxBridge {
|
|
|
51
56
|
* of why it's being seeded.
|
|
52
57
|
*/
|
|
53
58
|
setMockState(message: InitMessage): void;
|
|
59
|
+
completeInitialization(): void;
|
|
54
60
|
private applyInit;
|
|
61
|
+
/**
|
|
62
|
+
* Lets the initial application render commit before we acknowledge init. The host keeps the
|
|
63
|
+
* iframe covered throughout this frame, so it can apply the measured height before first paint.
|
|
64
|
+
*/
|
|
65
|
+
private scheduleInitResult;
|
|
55
66
|
queryDataSource(key: string, options?: UseDataSourceOptions): void;
|
|
56
67
|
private setDataSourceQueryError;
|
|
57
68
|
postResize(height: number): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SandboxBridge.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/SandboxBridge.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACX,gBAAgB,EAEhB,MAAM,4DAA4D,CAAA;AAWnE,OAAO,EAEN,KAAK,WAAW,EAEhB,MAAM,mDAAmD,CAAA;AAe1D,OAAO,KAAK,EACX,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,aAAa,EACb,eAAe,EACf,8BAA8B,EAC9B,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,MAAM,aAAa,CAAA;AASpB,OAAO,EACN,KAAK,oBAAoB,EAGzB,MAAM,gBAAgB,CAAA;AACvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAG3D;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,GAAG,UAAU,CAAA;IAC9B,IAAI,EAAE,OAAO,CAAA;CACb,CAAA;
|
|
1
|
+
{"version":3,"file":"SandboxBridge.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/SandboxBridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,6 +11,7 @@ import { resolveDataSources } from "./dataSources/resolve.js";
|
|
|
11
11
|
import { resolvePropertyWriteMapForDataSource } from "./dataSources/resolveProperty.js";
|
|
12
12
|
import { createEmptyDataSourceQueryState, } from "./hostState.js";
|
|
13
13
|
import { PendingRequests } from "./pendingRequests.js";
|
|
14
|
+
const INIT_RESULT_TIMER_FALLBACK_MS = 100;
|
|
14
15
|
export class SandboxBridge {
|
|
15
16
|
constructor() {
|
|
16
17
|
this.hostState = {
|
|
@@ -29,6 +30,8 @@ export class SandboxBridge {
|
|
|
29
30
|
this.pendingUpdatePage = new PendingRequests("custom-block-update-page");
|
|
30
31
|
this.hasSentConnect = false;
|
|
31
32
|
this.hasReceivedInit = false;
|
|
33
|
+
this.hasSentInitResult = false;
|
|
34
|
+
this.pendingOutboundMessages = [];
|
|
32
35
|
this.latestDataSourceBindings = {};
|
|
33
36
|
this.isMockState = false;
|
|
34
37
|
this.isListening = false;
|
|
@@ -343,10 +346,32 @@ export class SandboxBridge {
|
|
|
343
346
|
this.postToHost(connectMessage);
|
|
344
347
|
}
|
|
345
348
|
postToHost(message) {
|
|
349
|
+
if (this.hostState.status === "initialized" &&
|
|
350
|
+
!this.hasSentInitResult &&
|
|
351
|
+
!this.isMockState &&
|
|
352
|
+
this.getMessageType(message) !== "initResult") {
|
|
353
|
+
this.pendingOutboundMessages.push(message);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
this.sendToHost(message);
|
|
357
|
+
}
|
|
358
|
+
sendToHost(message) {
|
|
346
359
|
console.debug("[custom-blocks-sdk] outbound postMessage", message);
|
|
347
360
|
this.logMessage("sent", message);
|
|
348
361
|
window.parent.postMessage(message, "*");
|
|
349
362
|
}
|
|
363
|
+
flushPendingOutboundMessages() {
|
|
364
|
+
const pendingMessages = this.pendingOutboundMessages;
|
|
365
|
+
this.pendingOutboundMessages = [];
|
|
366
|
+
for (const message of pendingMessages) {
|
|
367
|
+
this.sendToHost(message);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
getMessageType(message) {
|
|
371
|
+
return typeof message === "object" && message !== null && "type" in message
|
|
372
|
+
? message.type
|
|
373
|
+
: undefined;
|
|
374
|
+
}
|
|
350
375
|
subscribe(listener) {
|
|
351
376
|
this.listeners.add(listener);
|
|
352
377
|
return () => this.listeners.delete(listener);
|
|
@@ -364,6 +389,22 @@ export class SandboxBridge {
|
|
|
364
389
|
this.isMockState = true;
|
|
365
390
|
this.applyInit(v.parse(initMessageSchema, message), false);
|
|
366
391
|
}
|
|
392
|
+
completeInitialization() {
|
|
393
|
+
if (this.hasSentInitResult ||
|
|
394
|
+
this.initializationId === undefined ||
|
|
395
|
+
this.hostState.status !== "initialized") {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const result = {
|
|
399
|
+
type: "initResult",
|
|
400
|
+
initializationId: this.initializationId,
|
|
401
|
+
status: "success",
|
|
402
|
+
initialHeight: getInitialContentHeight(),
|
|
403
|
+
};
|
|
404
|
+
this.hasSentInitResult = true;
|
|
405
|
+
this.postToHost(result);
|
|
406
|
+
this.flushPendingOutboundMessages();
|
|
407
|
+
}
|
|
367
408
|
applyInit(message, postResult) {
|
|
368
409
|
if (postResult) {
|
|
369
410
|
this.isMockState = false;
|
|
@@ -424,14 +465,6 @@ export class SandboxBridge {
|
|
|
424
465
|
contrastMode: message.contrastMode,
|
|
425
466
|
});
|
|
426
467
|
this.notify();
|
|
427
|
-
if (postResult) {
|
|
428
|
-
const result = {
|
|
429
|
-
type: "initResult",
|
|
430
|
-
initializationId: message.initializationId,
|
|
431
|
-
status: "success",
|
|
432
|
-
};
|
|
433
|
-
this.postToHost(result);
|
|
434
|
-
}
|
|
435
468
|
// Resolve the awaitInit promise once. Subsequent `init` messages
|
|
436
469
|
// (the host shouldn't send these, but be tolerant) update state but
|
|
437
470
|
// don't re-resolve.
|
|
@@ -440,6 +473,34 @@ export class SandboxBridge {
|
|
|
440
473
|
this.resolveInit = undefined;
|
|
441
474
|
this.rejectInit = undefined;
|
|
442
475
|
}
|
|
476
|
+
if (postResult) {
|
|
477
|
+
this.scheduleInitResult(message.initializationId);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Lets the initial application render commit before we acknowledge init. The host keeps the
|
|
482
|
+
* iframe covered throughout this frame, so it can apply the measured height before first paint.
|
|
483
|
+
*/
|
|
484
|
+
scheduleInitResult(initializationId) {
|
|
485
|
+
let didAcknowledge = false;
|
|
486
|
+
let fallbackTimerId;
|
|
487
|
+
const acknowledge = () => {
|
|
488
|
+
if (didAcknowledge) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
didAcknowledge = true;
|
|
492
|
+
if (fallbackTimerId !== undefined) {
|
|
493
|
+
window.clearTimeout(fallbackTimerId);
|
|
494
|
+
}
|
|
495
|
+
if (this.initializationId !== initializationId) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
this.completeInitialization();
|
|
499
|
+
};
|
|
500
|
+
fallbackTimerId = window.setTimeout(acknowledge, INIT_RESULT_TIMER_FALLBACK_MS);
|
|
501
|
+
if (typeof requestAnimationFrame === "function") {
|
|
502
|
+
requestAnimationFrame(acknowledge);
|
|
503
|
+
}
|
|
443
504
|
}
|
|
444
505
|
queryDataSource(key, options = {}) {
|
|
445
506
|
if (this.hostState.status !== "initialized") {
|
|
@@ -712,6 +773,16 @@ export class SandboxBridge {
|
|
|
712
773
|
}
|
|
713
774
|
}
|
|
714
775
|
SandboxBridge.MAX_LOG_ENTRIES = 100;
|
|
776
|
+
function getInitialContentHeight() {
|
|
777
|
+
if (typeof document === "undefined") {
|
|
778
|
+
return 0;
|
|
779
|
+
}
|
|
780
|
+
const root = document.getElementById("root");
|
|
781
|
+
if (root === null) {
|
|
782
|
+
return 0;
|
|
783
|
+
}
|
|
784
|
+
return Math.ceil(root.getBoundingClientRect().height);
|
|
785
|
+
}
|
|
715
786
|
/**
|
|
716
787
|
* Makes the document canvas match the host appearance before acknowledging init.
|
|
717
788
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxClient.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/sandboxClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mDAAmD,CAAA;AACpF,OAAO,KAAK,EACX,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,aAAa,EACb,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,MAAM,aAAa,CAAA;AACpB,OAAO,EACN,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EAExB,MAAM,gBAAgB,CAAA;AACvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,EAAE,KAAK,eAAe,EAAiB,MAAM,oBAAoB,CAAA;AAExE,YAAY,EAAE,eAAe,EAAE,CAAA;AAW/B,eAAO,MAAM,eAAe;;kCAKG,kBAAkB;yBAI3B,WAAW,KAAG,OAAO,CAAC,IAAI,CAAC;0BAI1B,MAAM,IAAI;oBAIlB,oBAAoB;IAIlC;;;OAGG;4BACqB,WAAW
|
|
1
|
+
{"version":3,"file":"sandboxClient.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/sandboxClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mDAAmD,CAAA;AACpF,OAAO,KAAK,EACX,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,aAAa,EACb,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,MAAM,aAAa,CAAA;AACpB,OAAO,EACN,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EAExB,MAAM,gBAAgB,CAAA;AACvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,EAAE,KAAK,eAAe,EAAiB,MAAM,oBAAoB,CAAA;AAExE,YAAY,EAAE,eAAe,EAAE,CAAA;AAW/B,eAAO,MAAM,eAAe;;kCAKG,kBAAkB;yBAI3B,WAAW,KAAG,OAAO,CAAC,IAAI,CAAC;0BAI1B,MAAM,IAAI;oBAIlB,oBAAoB;IAIlC;;;OAGG;4BACqB,WAAW;;yBAQd,MAAM;CAG3B,CAAA;AAED,eAAO,MAAM,sBAAsB;iBACrB,MAAM,YAAY,oBAAoB;yBAKvC,oBAAoB,OAC1B,MAAM,KACT,mBAAmB;CAKtB,CAAA;AAED,eAAO,MAAM,UAAU;uBACL,SAAS,eAAe,EAAE;0BAIrB,MAAM,IAAI;CAGhC,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,KAAK;IACjB;;OAEG;oBACa,cAAc,KAAG,OAAO,CAAC,gBAAgB,CAAC;IAI1D;;OAEG;kBACW,YAAY,KAAG,OAAO,CAAC,aAAa,CAAC;IAInD;;OAEG;oBACa,cAAc,KAAG,OAAO,CAAC,gBAAgB,CAAC;IAI1D;;OAEG;qBACc,YAAY,KAAG,OAAO,CAAC,gBAAgB,CAAC;CAGzD,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,KAAK;IACjB;;OAEG;mBACY,aAAa,KAAG,OAAO,CAAC,eAAe,CAAC;IAIvD;;OAEG;kBACW,YAAY,KAAG,OAAO,CAAC,aAAa,CAAC;CAGnD,CAAA"}
|
|
@@ -30,6 +30,9 @@ export const customBlockHost = {
|
|
|
30
30
|
setMockState: (message) => {
|
|
31
31
|
getBridge().setMockState(message);
|
|
32
32
|
},
|
|
33
|
+
completeInitialization: () => {
|
|
34
|
+
getBridge().completeInitialization();
|
|
35
|
+
},
|
|
33
36
|
postResize: (height) => {
|
|
34
37
|
getBridge().postResize(height);
|
|
35
38
|
},
|
|
@@ -15,6 +15,8 @@ export declare const initResultMessageSchema: v.VariantSchema<"status", [v.Objec
|
|
|
15
15
|
readonly type: v.LiteralSchema<"initResult", undefined>;
|
|
16
16
|
readonly initializationId: v.StringSchema<undefined>;
|
|
17
17
|
readonly status: v.LiteralSchema<"success", undefined>;
|
|
18
|
+
/** Height of the initial rendered custom-block content, in CSS pixels. */
|
|
19
|
+
readonly initialHeight: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.FiniteAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
18
20
|
}, undefined>, v.ObjectSchema<{
|
|
19
21
|
readonly type: v.LiteralSchema<"initResult", undefined>;
|
|
20
22
|
readonly initializationId: v.StringSchema<undefined>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initResult.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/protocol/src/messages/initResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAC5B,OAAO,KAAK,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAI9E,eAAO,MAAM,oCAAoC,2BAAa,CAAA;AAE9D,MAAM,MAAM,8BAA8B,GACzC,oBAAoB,CAAC,uBAAuB,CAAC,CAAA;AAE9C,MAAM,MAAM,8BAA8B,GACzC,oBAAoB,CAAC,8BAA8B,CAAC,CAAA;AAErD,eAAO,MAAM,oCAAoC;;;;aAI/C,CAAA;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB
|
|
1
|
+
{"version":3,"file":"initResult.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/protocol/src/messages/initResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAC5B,OAAO,KAAK,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAI9E,eAAO,MAAM,oCAAoC,2BAAa,CAAA;AAE9D,MAAM,MAAM,8BAA8B,GACzC,oBAAoB,CAAC,uBAAuB,CAAC,CAAA;AAE9C,MAAM,MAAM,8BAA8B,GACzC,oBAAoB,CAAC,8BAA8B,CAAC,CAAA;AAErD,eAAO,MAAM,oCAAoC;;;;aAI/C,CAAA;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB;;;;IAKlC,0EAA0E;;;;;;;;;;;0BAU1E,CAAA;AAEF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,uBAAuB,CAAC,CAAA"}
|
|
@@ -15,6 +15,9 @@ export const initResultMessageSchema = v.variant("status", [
|
|
|
15
15
|
type: v.literal("initResult"),
|
|
16
16
|
initializationId: v.string(),
|
|
17
17
|
status: v.literal("success"),
|
|
18
|
+
/** Height of the initial rendered custom-block content, in CSS pixels. */
|
|
19
|
+
// TODO(custom-blocks): Make this required when bumping bridge protocol version 4.
|
|
20
|
+
initialHeight: v.optional(v.pipe(v.number(), v.finite(), v.minValue(0))),
|
|
18
21
|
}),
|
|
19
22
|
v.object({
|
|
20
23
|
type: v.literal("initResult"),
|
|
@@ -43,6 +43,7 @@ export declare const sandboxToHostMessageSchema: v.UnionSchema<[v.VariantSchema<
|
|
|
43
43
|
readonly type: v.LiteralSchema<"initResult", undefined>;
|
|
44
44
|
readonly initializationId: v.StringSchema<undefined>;
|
|
45
45
|
readonly status: v.LiteralSchema<"success", undefined>;
|
|
46
|
+
readonly initialHeight: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.FiniteAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
46
47
|
}, undefined>, v.ObjectSchema<{
|
|
47
48
|
readonly type: v.LiteralSchema<"initResult", undefined>;
|
|
48
49
|
readonly initializationId: v.StringSchema<undefined>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxToHost.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/protocol/src/messages/sandboxToHost.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAY5B;;;GAGG;AACH,eAAO,MAAM,0BAA0B
|
|
1
|
+
{"version":3,"file":"sandboxToHost.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/protocol/src/messages/sandboxToHost.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAY5B;;;GAGG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAWrC,CAAA;AAEF,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,WAAW,CAC/C,OAAO,0BAA0B,CACjC,CAAA"}
|
|
@@ -18,10 +18,9 @@ export type NotionCustomBlockProps = InitCustomBlockOptions & {
|
|
|
18
18
|
*/
|
|
19
19
|
errorFallback?: ReactNode | ((error: Error) => ReactNode);
|
|
20
20
|
/**
|
|
21
|
-
* Whether
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* with scrollbars within the Notion client.
|
|
21
|
+
* Whether to post resize messages after initialization and dynamically resize the block.
|
|
22
|
+
*
|
|
23
|
+
* @deprecated TODO(custom-blocks): Remove this prop when bumping the SDK version to 0.2.0.
|
|
25
24
|
*
|
|
26
25
|
* @default true
|
|
27
26
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotionCustomBlock.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/react/NotionCustomBlock.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"NotionCustomBlock.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/react/NotionCustomBlock.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAwC,MAAM,OAAO,CAAA;AAE5E,OAAO,EAAE,KAAK,sBAAsB,EAAoB,MAAM,aAAa,CAAA;AAM3E,OAAO,yBAAyB,CAAA;AAEhC;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,sBAAsB,GAAG;IAC7D,QAAQ,EAAE,SAAS,CAAA;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,SAAS,CAAC,CAAA;IACzD;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,EACjC,QAAQ,EACR,SAAS,EACT,QAAe,EACf,aAAa,EACb,UAAiB,GACjB,EAAE,sBAAsB,2CAsExB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
2
|
+
import { useEffect, useLayoutEffect, useState } from "react";
|
|
3
|
+
import { customBlockHost } from "../bridge/sandboxClient.js";
|
|
3
4
|
import { NotInIframeError } from "../index.js";
|
|
4
5
|
import { DebugMessageLog } from "./DebugMessageLog.js";
|
|
5
6
|
import { seedStandalonePreviewState } from "./standalonePreview.js";
|
|
@@ -28,7 +29,8 @@ import "./NotionCustomBlock.css";
|
|
|
28
29
|
*/
|
|
29
30
|
export function NotionCustomBlock({ children, timeoutMs, fallback = null, errorFallback, autoResize = true, }) {
|
|
30
31
|
const init = useCustomBlockInit({ timeoutMs });
|
|
31
|
-
useCustomBlockAutoResize({ enabled: autoResize });
|
|
32
|
+
useCustomBlockAutoResize({ enabled: init.isLoaded && autoResize });
|
|
33
|
+
// True if the block has no host (i.e. it's running in standalone preview).
|
|
32
34
|
const isStandalone = init.error instanceof NotInIframeError;
|
|
33
35
|
const host = useCustomBlockHost();
|
|
34
36
|
const [debugOpen, setDebugOpen] = useState(false);
|
|
@@ -48,6 +50,14 @@ export function NotionCustomBlock({ children, timeoutMs, fallback = null, errorF
|
|
|
48
50
|
console.warn(`[custom-blocks-sdk] ${init.error?.message}`);
|
|
49
51
|
seedStandalonePreviewState();
|
|
50
52
|
}, [isStandalone, init.error]);
|
|
53
|
+
useLayoutEffect(() => {
|
|
54
|
+
if (!init.isLoaded || isStandalone) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Send `initResult` *after* React renders the block so the SDK can report its rendered
|
|
58
|
+
// height to the host as part of the `initResult` message.
|
|
59
|
+
customBlockHost.completeInitialization();
|
|
60
|
+
}, [init.isLoaded, isStandalone]);
|
|
51
61
|
if (debugOpen) {
|
|
52
62
|
return _jsx(DebugMessageLog, {});
|
|
53
63
|
}
|
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
* Measures the sandbox's `#root` element and posts `resize` messages so the host iframe
|
|
3
3
|
* matches the block's border-box height. Unchanged values are deduped.
|
|
4
4
|
*
|
|
5
|
-
* `<NotionCustomBlock>` calls this hook for you
|
|
6
|
-
*
|
|
7
|
-
* pass `autoResize={false}` to the provider to avoid running it twice. For full-bleed
|
|
8
|
-
* views that should fill their slot, pass `autoResize={false}` and skip the hook.
|
|
5
|
+
* `<NotionCustomBlock>` calls this hook for you. Only use it directly in a custom
|
|
6
|
+
* initialization wrapper that does not render `<NotionCustomBlock>`.
|
|
9
7
|
*
|
|
10
|
-
* @
|
|
11
|
-
* <NotionCustomBlock autoResize={false}>
|
|
12
|
-
* <App />
|
|
13
|
-
* </NotionCustomBlock>
|
|
8
|
+
* @deprecated Use CSS `max-height` and `overflow` to constrain block content instead.
|
|
14
9
|
*
|
|
10
|
+
* @example
|
|
15
11
|
* function App() {
|
|
16
12
|
* const [enabled, setEnabled] = useState(true)
|
|
17
13
|
* useCustomBlockAutoResize({ enabled })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCustomBlockAutoResize.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/react/useCustomBlockAutoResize.ts"],"names":[],"mappings":"AAGA
|
|
1
|
+
{"version":3,"file":"useCustomBlockAutoResize.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/react/useCustomBlockAutoResize.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CACvC,IAAI,GAAE;IACL;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CACZ,GACJ,IAAI,CAYN"}
|
|
@@ -4,16 +4,12 @@ import { customBlock } from "../index.js";
|
|
|
4
4
|
* Measures the sandbox's `#root` element and posts `resize` messages so the host iframe
|
|
5
5
|
* matches the block's border-box height. Unchanged values are deduped.
|
|
6
6
|
*
|
|
7
|
-
* `<NotionCustomBlock>` calls this hook for you
|
|
8
|
-
*
|
|
9
|
-
* pass `autoResize={false}` to the provider to avoid running it twice. For full-bleed
|
|
10
|
-
* views that should fill their slot, pass `autoResize={false}` and skip the hook.
|
|
7
|
+
* `<NotionCustomBlock>` calls this hook for you. Only use it directly in a custom
|
|
8
|
+
* initialization wrapper that does not render `<NotionCustomBlock>`.
|
|
11
9
|
*
|
|
12
|
-
* @
|
|
13
|
-
* <NotionCustomBlock autoResize={false}>
|
|
14
|
-
* <App />
|
|
15
|
-
* </NotionCustomBlock>
|
|
10
|
+
* @deprecated Use CSS `max-height` and `overflow` to constrain block content instead.
|
|
16
11
|
*
|
|
12
|
+
* @example
|
|
17
13
|
* function App() {
|
|
18
14
|
* const [enabled, setEnabled] = useState(true)
|
|
19
15
|
* useCustomBlockAutoResize({ enabled })
|
package/dist/version.js
CHANGED
package/docs/lifecycle.md
CHANGED
|
@@ -4,13 +4,13 @@ The SDK ↔ host handshake, the React wrapper that runs it, and the auto-resize
|
|
|
4
4
|
|
|
5
5
|
## Handshake
|
|
6
6
|
|
|
7
|
-
`initCustomBlock()`
|
|
7
|
+
`initCustomBlock()` handles three messages:
|
|
8
8
|
|
|
9
|
-
1. The SDK sends `connect` with a new initialization ID and
|
|
10
|
-
2. The host
|
|
11
|
-
3. The SDK
|
|
9
|
+
1. The SDK sends `connect` with a new initialization ID and version information.
|
|
10
|
+
2. The host sends `init`. It includes the same ID, the manifest, block context, current user, and data source bindings.
|
|
11
|
+
3. The SDK sends `initResult` with the ID and the initial block height.
|
|
12
12
|
|
|
13
|
-
The promise resolves
|
|
13
|
+
The promise resolves when the SDK applies the host state. Await it before React renders so hooks read initialized state. The SDK sends `initResult` after the initial render.
|
|
14
14
|
|
|
15
15
|
- Rejects with `CustomBlockInitializationError` code `init_timeout` if the host doesn't respond.
|
|
16
16
|
- In a top-level browser tab (no parent frame), rejects with `NotInIframeError` code `not_in_iframe`. `<NotionCustomBlock>` catches this, seeds placeholders, and renders `children` behind a warning banner so dev-time previews still work.
|
|
@@ -19,10 +19,16 @@ The promise resolves only after the SDK sends `initResult.success`. Await it bef
|
|
|
19
19
|
|
|
20
20
|
## Sizing
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
All blocks resize to fit their content. The Notion app limits block height to 10,000 pixels.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
To set a shorter height, set `max-height` and `overflow-y` on `#root`:
|
|
25
|
+
|
|
26
|
+
```css
|
|
27
|
+
#root {
|
|
28
|
+
max-height: 600px;
|
|
29
|
+
overflow-y: auto;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
26
32
|
|
|
27
33
|
## API
|
|
28
34
|
|
|
@@ -35,11 +41,10 @@ type NotionCustomBlockProps = InitCustomBlockOptions & {
|
|
|
35
41
|
children: ReactNode;
|
|
36
42
|
fallback?: ReactNode;
|
|
37
43
|
errorFallback?: ReactNode | ((error: Error) => ReactNode);
|
|
38
|
-
autoResize?: boolean; // defaults to true
|
|
39
44
|
};
|
|
40
45
|
```
|
|
41
46
|
|
|
42
|
-
Top-level wrapper.
|
|
47
|
+
Top-level wrapper. It starts the handshake and renders `children` after init. It measures `#root` before it shows the iframe and when content height changes. `fallback` replaces the loading view. Its default is `null`. `errorFallback` replaces the error message if init fails. `timeoutMs` passes to `initCustomBlock`.
|
|
43
48
|
|
|
44
49
|
### `useCustomBlockInit(opts?)`
|
|
45
50
|
|
|
@@ -120,23 +125,13 @@ Thrown when `initCustomBlock` is called in a top-level tab (no parent frame). It
|
|
|
120
125
|
|
|
121
126
|
### `useCustomBlockAutoResize({ enabled? })`
|
|
122
127
|
|
|
128
|
+
Deprecated. Blocks resize automatically. Use CSS `max-height` and `overflow` to set a shorter height.
|
|
129
|
+
|
|
123
130
|
```ts
|
|
124
131
|
function useCustomBlockAutoResize(args?: { enabled?: boolean }): void;
|
|
125
132
|
```
|
|
126
133
|
|
|
127
|
-
React wrapper around `customBlock.autoResize({ target: document.getElementById("root") })`.
|
|
128
|
-
|
|
129
|
-
```tsx
|
|
130
|
-
<NotionCustomBlock autoResize={false}>
|
|
131
|
-
<App />
|
|
132
|
-
</NotionCustomBlock>;
|
|
133
|
-
|
|
134
|
-
function App() {
|
|
135
|
-
const [enabled, setEnabled] = useState(true);
|
|
136
|
-
useCustomBlockAutoResize({ enabled });
|
|
137
|
-
return <div>…</div>;
|
|
138
|
-
}
|
|
139
|
-
```
|
|
134
|
+
Deprecated React wrapper around `customBlock.autoResize({ target: document.getElementById("root") })`. It measures `#root`'s height and posts `resize` messages, deduping unchanged values. `<NotionCustomBlock>` runs this automatically.
|
|
140
135
|
|
|
141
136
|
## Debug console
|
|
142
137
|
|
package/package.json
CHANGED
|
@@ -73,6 +73,8 @@ export type MessageLogEntry = {
|
|
|
73
73
|
data: unknown
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
const INIT_RESULT_TIMER_FALLBACK_MS = 100
|
|
77
|
+
|
|
76
78
|
export class SandboxBridge {
|
|
77
79
|
private hostState: CustomBlockHostState = {
|
|
78
80
|
status: "uninitialized",
|
|
@@ -101,6 +103,8 @@ export class SandboxBridge {
|
|
|
101
103
|
private hasSentConnect = false
|
|
102
104
|
private hasReceivedInit = false
|
|
103
105
|
private initializationId: string | undefined
|
|
106
|
+
private hasSentInitResult = false
|
|
107
|
+
private pendingOutboundMessages: unknown[] = []
|
|
104
108
|
private latestDataSourceBindings: NotionDataSourceBindings = {}
|
|
105
109
|
private isMockState = false
|
|
106
110
|
private isListening = false
|
|
@@ -205,11 +209,38 @@ export class SandboxBridge {
|
|
|
205
209
|
}
|
|
206
210
|
|
|
207
211
|
private postToHost(message: unknown) {
|
|
212
|
+
if (
|
|
213
|
+
this.hostState.status === "initialized" &&
|
|
214
|
+
!this.hasSentInitResult &&
|
|
215
|
+
!this.isMockState &&
|
|
216
|
+
this.getMessageType(message) !== "initResult"
|
|
217
|
+
) {
|
|
218
|
+
this.pendingOutboundMessages.push(message)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
this.sendToHost(message)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private sendToHost(message: unknown) {
|
|
208
225
|
console.debug("[custom-blocks-sdk] outbound postMessage", message)
|
|
209
226
|
this.logMessage("sent", message)
|
|
210
227
|
window.parent.postMessage(message, "*")
|
|
211
228
|
}
|
|
212
229
|
|
|
230
|
+
private flushPendingOutboundMessages() {
|
|
231
|
+
const pendingMessages = this.pendingOutboundMessages
|
|
232
|
+
this.pendingOutboundMessages = []
|
|
233
|
+
for (const message of pendingMessages) {
|
|
234
|
+
this.sendToHost(message)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private getMessageType(message: unknown): unknown {
|
|
239
|
+
return typeof message === "object" && message !== null && "type" in message
|
|
240
|
+
? message.type
|
|
241
|
+
: undefined
|
|
242
|
+
}
|
|
243
|
+
|
|
213
244
|
private notify = () => {
|
|
214
245
|
for (const listener of this.listeners) {
|
|
215
246
|
listener()
|
|
@@ -504,6 +535,25 @@ export class SandboxBridge {
|
|
|
504
535
|
this.applyInit(v.parse(initMessageSchema, message), false)
|
|
505
536
|
}
|
|
506
537
|
|
|
538
|
+
completeInitialization() {
|
|
539
|
+
if (
|
|
540
|
+
this.hasSentInitResult ||
|
|
541
|
+
this.initializationId === undefined ||
|
|
542
|
+
this.hostState.status !== "initialized"
|
|
543
|
+
) {
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
const result: InitResultMessage = {
|
|
547
|
+
type: "initResult",
|
|
548
|
+
initializationId: this.initializationId,
|
|
549
|
+
status: "success",
|
|
550
|
+
initialHeight: getInitialContentHeight(),
|
|
551
|
+
}
|
|
552
|
+
this.hasSentInitResult = true
|
|
553
|
+
this.postToHost(result)
|
|
554
|
+
this.flushPendingOutboundMessages()
|
|
555
|
+
}
|
|
556
|
+
|
|
507
557
|
private applyInit(message: InitMessage, postResult: boolean) {
|
|
508
558
|
if (postResult) {
|
|
509
559
|
this.isMockState = false
|
|
@@ -568,14 +618,6 @@ export class SandboxBridge {
|
|
|
568
618
|
contrastMode: message.contrastMode,
|
|
569
619
|
})
|
|
570
620
|
this.notify()
|
|
571
|
-
if (postResult) {
|
|
572
|
-
const result: InitResultMessage = {
|
|
573
|
-
type: "initResult",
|
|
574
|
-
initializationId: message.initializationId,
|
|
575
|
-
status: "success",
|
|
576
|
-
}
|
|
577
|
-
this.postToHost(result)
|
|
578
|
-
}
|
|
579
621
|
// Resolve the awaitInit promise once. Subsequent `init` messages
|
|
580
622
|
// (the host shouldn't send these, but be tolerant) update state but
|
|
581
623
|
// don't re-resolve.
|
|
@@ -584,6 +626,38 @@ export class SandboxBridge {
|
|
|
584
626
|
this.resolveInit = undefined
|
|
585
627
|
this.rejectInit = undefined
|
|
586
628
|
}
|
|
629
|
+
if (postResult) {
|
|
630
|
+
this.scheduleInitResult(message.initializationId)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Lets the initial application render commit before we acknowledge init. The host keeps the
|
|
636
|
+
* iframe covered throughout this frame, so it can apply the measured height before first paint.
|
|
637
|
+
*/
|
|
638
|
+
private scheduleInitResult(initializationId: string) {
|
|
639
|
+
let didAcknowledge = false
|
|
640
|
+
let fallbackTimerId: number | undefined
|
|
641
|
+
const acknowledge = () => {
|
|
642
|
+
if (didAcknowledge) {
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
didAcknowledge = true
|
|
646
|
+
if (fallbackTimerId !== undefined) {
|
|
647
|
+
window.clearTimeout(fallbackTimerId)
|
|
648
|
+
}
|
|
649
|
+
if (this.initializationId !== initializationId) {
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
this.completeInitialization()
|
|
653
|
+
}
|
|
654
|
+
fallbackTimerId = window.setTimeout(
|
|
655
|
+
acknowledge,
|
|
656
|
+
INIT_RESULT_TIMER_FALLBACK_MS,
|
|
657
|
+
)
|
|
658
|
+
if (typeof requestAnimationFrame === "function") {
|
|
659
|
+
requestAnimationFrame(acknowledge)
|
|
660
|
+
}
|
|
587
661
|
}
|
|
588
662
|
|
|
589
663
|
queryDataSource(key: string, options: UseDataSourceOptions = {}) {
|
|
@@ -902,6 +976,17 @@ export class SandboxBridge {
|
|
|
902
976
|
}
|
|
903
977
|
}
|
|
904
978
|
|
|
979
|
+
function getInitialContentHeight(): number {
|
|
980
|
+
if (typeof document === "undefined") {
|
|
981
|
+
return 0
|
|
982
|
+
}
|
|
983
|
+
const root = document.getElementById("root")
|
|
984
|
+
if (root === null) {
|
|
985
|
+
return 0
|
|
986
|
+
}
|
|
987
|
+
return Math.ceil(root.getBoundingClientRect().height)
|
|
988
|
+
}
|
|
989
|
+
|
|
905
990
|
/**
|
|
906
991
|
* Makes the document canvas match the host appearance before acknowledging init.
|
|
907
992
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type ReactNode, useEffect, useState } from "react"
|
|
1
|
+
import { type ReactNode, useEffect, useLayoutEffect, useState } from "react"
|
|
2
|
+
import { customBlockHost } from "../bridge/sandboxClient.js"
|
|
2
3
|
import { type InitCustomBlockOptions, NotInIframeError } from "../index.js"
|
|
3
4
|
import { DebugMessageLog } from "./DebugMessageLog.js"
|
|
4
5
|
import { seedStandalonePreviewState } from "./standalonePreview.js"
|
|
@@ -24,10 +25,9 @@ export type NotionCustomBlockProps = InitCustomBlockOptions & {
|
|
|
24
25
|
*/
|
|
25
26
|
errorFallback?: ReactNode | ((error: Error) => ReactNode)
|
|
26
27
|
/**
|
|
27
|
-
* Whether
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* with scrollbars within the Notion client.
|
|
28
|
+
* Whether to post resize messages after initialization and dynamically resize the block.
|
|
29
|
+
*
|
|
30
|
+
* @deprecated TODO(custom-blocks): Remove this prop when bumping the SDK version to 0.2.0.
|
|
31
31
|
*
|
|
32
32
|
* @default true
|
|
33
33
|
*/
|
|
@@ -61,7 +61,8 @@ export function NotionCustomBlock({
|
|
|
61
61
|
autoResize = true,
|
|
62
62
|
}: NotionCustomBlockProps) {
|
|
63
63
|
const init = useCustomBlockInit({ timeoutMs })
|
|
64
|
-
useCustomBlockAutoResize({ enabled: autoResize })
|
|
64
|
+
useCustomBlockAutoResize({ enabled: init.isLoaded && autoResize })
|
|
65
|
+
// True if the block has no host (i.e. it's running in standalone preview).
|
|
65
66
|
const isStandalone = init.error instanceof NotInIframeError
|
|
66
67
|
const host = useCustomBlockHost()
|
|
67
68
|
const [debugOpen, setDebugOpen] = useState(false)
|
|
@@ -84,6 +85,15 @@ export function NotionCustomBlock({
|
|
|
84
85
|
seedStandalonePreviewState()
|
|
85
86
|
}, [isStandalone, init.error])
|
|
86
87
|
|
|
88
|
+
useLayoutEffect(() => {
|
|
89
|
+
if (!init.isLoaded || isStandalone) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
// Send `initResult` *after* React renders the block so the SDK can report its rendered
|
|
93
|
+
// height to the host as part of the `initResult` message.
|
|
94
|
+
customBlockHost.completeInitialization()
|
|
95
|
+
}, [init.isLoaded, isStandalone])
|
|
96
|
+
|
|
87
97
|
if (debugOpen) {
|
|
88
98
|
return <DebugMessageLog />
|
|
89
99
|
}
|
|
@@ -5,16 +5,12 @@ import { customBlock } from "../index.js"
|
|
|
5
5
|
* Measures the sandbox's `#root` element and posts `resize` messages so the host iframe
|
|
6
6
|
* matches the block's border-box height. Unchanged values are deduped.
|
|
7
7
|
*
|
|
8
|
-
* `<NotionCustomBlock>` calls this hook for you
|
|
9
|
-
*
|
|
10
|
-
* pass `autoResize={false}` to the provider to avoid running it twice. For full-bleed
|
|
11
|
-
* views that should fill their slot, pass `autoResize={false}` and skip the hook.
|
|
8
|
+
* `<NotionCustomBlock>` calls this hook for you. Only use it directly in a custom
|
|
9
|
+
* initialization wrapper that does not render `<NotionCustomBlock>`.
|
|
12
10
|
*
|
|
13
|
-
* @
|
|
14
|
-
* <NotionCustomBlock autoResize={false}>
|
|
15
|
-
* <App />
|
|
16
|
-
* </NotionCustomBlock>
|
|
11
|
+
* @deprecated Use CSS `max-height` and `overflow` to constrain block content instead.
|
|
17
12
|
*
|
|
13
|
+
* @example
|
|
18
14
|
* function App() {
|
|
19
15
|
* const [enabled, setEnabled] = useState(true)
|
|
20
16
|
* useCustomBlockAutoResize({ enabled })
|