@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 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 it resolves. Inside the wrapper, every hook returns non-nullable values — there's no separate gating component to write. It also runs `useCustomBlockAutoResize` for you by default; pass `autoResize={false}` to opt out.
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`, `customBlock.autoResize`, `NotInIframeError`, `useCustomBlockAutoResize`. The handshake, the React wrapper, sizing.
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":"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"}
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
  *
@@ -15,6 +15,7 @@ export declare const customBlockHost: {
15
15
  * standalone preview state.
16
16
  */
17
17
  setMockState: (message: InitMessage) => void;
18
+ completeInitialization: () => void;
18
19
  postResize: (height: number) => void;
19
20
  };
20
21
  export declare const customBlockDataSources: {
@@ -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;yBAId,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"}
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;;;;;;;;;;;;;0BAYlC,CAAA;AAEF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,uBAAuB,CAAC,CAAA"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAWrC,CAAA;AAEF,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,WAAW,CAC/C,OAAO,0BAA0B,CACjC,CAAA"}
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 the provider should automatically post resize messages so the
22
- * host iframe matches the content height of `#root`. Defaults to `true`.
23
- * Pass `false` when you want to use the default block size and are ok
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,EAAuB,MAAM,OAAO,CAAA;AAC3D,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;;;;;;;OAOG;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,2CA4DxB"}
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 by default only reach for it directly
6
- * when you need to drive `enabled` yourself (e.g. behind a debug toggle). In that case,
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
- * @example
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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,wBAAwB,CACvC,IAAI,GAAE;IACL;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CACZ,GACJ,IAAI,CAYN"}
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 by default only reach for it directly
8
- * when you need to drive `enabled` yourself (e.g. behind a debug toggle). In that case,
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
- * @example
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
@@ -4,4 +4,4 @@
4
4
  *
5
5
  * WARNING: Generated during SDK publish. Do not edit in the published package.
6
6
  */
7
- export const CUSTOM_BLOCKS_SDK_VERSION = "0.1.20"
7
+ export const CUSTOM_BLOCKS_SDK_VERSION = "0.1.21"
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()` completes a three-message handshake:
7
+ `initCustomBlock()` handles three messages:
8
8
 
9
- 1. The SDK sends `connect` with a new initialization ID and some version info.
10
- 2. The host replies with `init`, echoing that initialization ID and providing the manifest, block context, current user, and data source bindings.
11
- 3. The SDK confirms that state is compatible and sends `initResult` with the same initialization ID.
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 only after the SDK sends `initResult.success`. Await it before mounting React so hooks always see initialized state.
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
- The host owns width and height. Inside the iframe, `100vh` ≠ a screen and there's no meaningful "device width" — only iframe width. Layouts must reflow from a phone column to a desktop block.
22
+ All blocks resize to fit their content. The Notion app limits block height to 10,000 pixels.
23
23
 
24
- - **Self-sizing content** is the default — `<NotionCustomBlock>` measures `#root` and posts `resize` messages so the iframe tracks your content. Pass `autoResize={false}` for full-bleed views, or to drive `useCustomBlockAutoResize` yourself.
25
- - Prefer container queries (`@container`) over viewport queries.
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. Runs the handshake, gates `children`, and (by default) drives auto-resize. `fallback` replaces the loading view (default `null`); `errorFallback` replaces the inline `<p role="alert">` shown if init rejects. `timeoutMs` flows through to `initCustomBlock`. Pass `autoResize={false}` for full-bleed views or to call `useCustomBlockAutoResize` yourself.
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") })`. Measures `#root`'s height and posts `resize` messages, deduping unchanged values. `<NotionCustomBlock>` runs this for you — only call it directly when you want to drive `enabled` yourself (e.g. a debug toggle), and pair with `autoResize={false}` so it doesn't run twice. The target must have intrinsic height; do not give it `height: 100%` or `100vh`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -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
  *
@@ -60,6 +60,10 @@ export const customBlockHost = {
60
60
  getBridge().setMockState(message)
61
61
  },
62
62
 
63
+ completeInitialization: () => {
64
+ getBridge().completeInitialization()
65
+ },
66
+
63
67
  postResize: (height: number) => {
64
68
  getBridge().postResize(height)
65
69
  },
@@ -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 the provider should automatically post resize messages so the
28
- * host iframe matches the content height of `#root`. Defaults to `true`.
29
- * Pass `false` when you want to use the default block size and are ok
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 by default only reach for it directly
9
- * when you need to drive `enabled` yourself (e.g. behind a debug toggle). In that case,
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
- * @example
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 })