ncblock 0.0.4 → 0.0.5

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.
Files changed (56) hide show
  1. package/HOST.md +68 -0
  2. package/bridge/SandboxBridge.ts +659 -0
  3. package/bridge/context.ts +58 -0
  4. package/bridge/dataSources/dataSource.ts +63 -0
  5. package/bridge/dataSources/dataSourcePage.ts +69 -0
  6. package/bridge/dataSources/dataSourceValue.ts +19 -0
  7. package/bridge/dataSources/dateValue.ts +96 -0
  8. package/bridge/dataSources/propertySchema.ts +186 -0
  9. package/bridge/dataSources/recordPointer.ts +13 -0
  10. package/bridge/dataSources/resolve.ts +96 -0
  11. package/bridge/dataSources/resolveProperty.ts +96 -0
  12. package/bridge/hostState.ts +146 -0
  13. package/bridge/ids.ts +30 -0
  14. package/bridge/incomingType.ts +19 -0
  15. package/bridge/loadManifest.ts +54 -0
  16. package/bridge/manifest.ts +53 -0
  17. package/bridge/messages/contextChanged.ts +15 -0
  18. package/bridge/messages/createPage.ts +64 -0
  19. package/bridge/messages/createPageResult.ts +25 -0
  20. package/bridge/messages/dataSourcesChanged.ts +18 -0
  21. package/bridge/messages/getPage.ts +32 -0
  22. package/bridge/messages/getUser.ts +32 -0
  23. package/bridge/messages/hostToSandbox.ts +33 -0
  24. package/bridge/messages/init.ts +20 -0
  25. package/bridge/messages/invalidHostMessage.ts +16 -0
  26. package/bridge/messages/invalidSandboxMessage.ts +18 -0
  27. package/bridge/messages/listUsers.ts +33 -0
  28. package/bridge/messages/queryDataSource.ts +16 -0
  29. package/bridge/messages/queryDataSourceResult.ts +18 -0
  30. package/bridge/messages/ready.ts +25 -0
  31. package/bridge/messages/resize.ts +13 -0
  32. package/bridge/messages/sandboxToHost.ts +30 -0
  33. package/bridge/messages/themeChanged.ts +15 -0
  34. package/bridge/messages/updatePage.ts +21 -0
  35. package/bridge/messages/updatePageResult.ts +24 -0
  36. package/bridge/pages/page.ts +314 -0
  37. package/bridge/pendingRequests.ts +28 -0
  38. package/bridge/sandboxClient.ts +112 -0
  39. package/bridge/theme.ts +5 -0
  40. package/bridge/users/user.ts +31 -0
  41. package/docs/context.md +45 -0
  42. package/docs/data-sources.md +161 -0
  43. package/docs/lifecycle.md +92 -0
  44. package/docs/manifest.md +42 -0
  45. package/docs/pages.md +143 -0
  46. package/docs/users.md +61 -0
  47. package/host.ts +67 -0
  48. package/index.ts +86 -0
  49. package/init.ts +92 -0
  50. package/package.json +15 -5
  51. package/react.tsx +418 -0
  52. package/types.ts +157 -0
  53. package/users.ts +26 -0
  54. package/utils.ts +13 -0
  55. package/vite-plugin/index.d.ts +46 -0
  56. package/vite-plugin/index.js +115 -0
@@ -0,0 +1,25 @@
1
+ import * as v from "valibot"
2
+ import { manifestSchema } from "../manifest"
3
+
4
+ /**
5
+ * First message the sandbox sends after mount, kicking off the bridge handshake. The host replies
6
+ * with `init`.
7
+ */
8
+ export const readyMessageSchema = v.object({
9
+ type: v.literal("ready"),
10
+ /**
11
+ * Used to ensure that the host and client are using the same version of the bridge protocol. A
12
+ * single host needs to support multiple custom blocks built with different versions of the bridge
13
+ * protocol. Increment this number any time a breaking change is made to the bridge protocol.
14
+ */
15
+ bridgeProtocolVersion: v.optional(v.number()),
16
+ /**
17
+ * The data sources and settings the custom block expects. A sandbox sends `null` if it has no
18
+ * manifest-declared data requirements. `null` is used (rather than `undefined`) so the
19
+ * wire-level distinction between "explicitly no manifest" and "field omitted" survives
20
+ * `postMessage` serialization and runtime validation on the host.
21
+ */
22
+ manifest: v.union([manifestSchema, v.null_()]),
23
+ })
24
+
25
+ export type ReadyMessage = v.InferOutput<typeof readyMessageSchema>
@@ -0,0 +1,13 @@
1
+ import * as v from "valibot"
2
+
3
+ /**
4
+ * Message sent by the sandbox whenever its measured content height changes, so the host can resize
5
+ * the surrounding iframe to match. The host is free to clamp the value or ignore it. The sandbox
6
+ * always reports the most recent measurement.
7
+ */
8
+ export const resizeMessageSchema = v.object({
9
+ type: v.literal("resize"),
10
+ height: v.pipe(v.number(), v.finite(), v.minValue(0)),
11
+ })
12
+
13
+ export type ResizeMessage = v.InferOutput<typeof resizeMessageSchema>
@@ -0,0 +1,30 @@
1
+ import * as v from "valibot"
2
+ import { createPageMessageSchema } from "./createPage"
3
+ import { getPageMessageSchema } from "./getPage"
4
+ import { getUserMessageSchema } from "./getUser"
5
+ import { invalidHostMessageSchema } from "./invalidHostMessage"
6
+ import { listUsersMessageSchema } from "./listUsers"
7
+ import { queryDataSourceMessageSchema } from "./queryDataSource"
8
+ import { readyMessageSchema } from "./ready"
9
+ import { resizeMessageSchema } from "./resize"
10
+ import { updatePageMessageSchema } from "./updatePage"
11
+
12
+ /**
13
+ * Discriminated union of every message the sandbox is allowed to send the host. Useful for hosts
14
+ * to validate inbound traffic from sandboxes without re-implementing validation logic.
15
+ */
16
+ export const sandboxToHostMessageSchema = v.variant("type", [
17
+ readyMessageSchema,
18
+ queryDataSourceMessageSchema,
19
+ createPageMessageSchema,
20
+ getPageMessageSchema,
21
+ getUserMessageSchema,
22
+ listUsersMessageSchema,
23
+ updatePageMessageSchema,
24
+ resizeMessageSchema,
25
+ invalidHostMessageSchema,
26
+ ])
27
+
28
+ export type SandboxToHostMessage = v.InferOutput<
29
+ typeof sandboxToHostMessageSchema
30
+ >
@@ -0,0 +1,15 @@
1
+ import * as v from "valibot"
2
+ import { notionThemeSchema } from "../theme"
3
+
4
+ /**
5
+ * Message sent by the host every time the theme changes after the initial
6
+ * `init` handshake.
7
+ */
8
+ export const themeChangedMessageSchema = v.object({
9
+ type: v.literal("themeChanged"),
10
+ theme: notionThemeSchema,
11
+ })
12
+
13
+ export type ThemeChangedMessage = v.InferOutput<
14
+ typeof themeChangedMessageSchema
15
+ >
@@ -0,0 +1,21 @@
1
+ import * as v from "valibot"
2
+ import {
3
+ notionPageCoverSchema,
4
+ notionPageIconSchema,
5
+ notionPagePropertyWriteMapSchema,
6
+ } from "../pages/page"
7
+
8
+ /**
9
+ * Sandbox -> host: patch an existing page.
10
+ */
11
+ export const updatePageMessageSchema = v.object({
12
+ type: v.literal("updatePage"),
13
+ requestId: v.string(),
14
+ pageId: v.string(),
15
+ properties: v.optional(notionPagePropertyWriteMapSchema),
16
+ icon: v.optional(notionPageIconSchema),
17
+ cover: v.optional(notionPageCoverSchema),
18
+ archived: v.optional(v.boolean()),
19
+ })
20
+
21
+ export type UpdatePageMessage = v.InferOutput<typeof updatePageMessageSchema>
@@ -0,0 +1,24 @@
1
+ import * as v from "valibot"
2
+ import { notionPageSchema } from "../pages/page"
3
+
4
+ /**
5
+ * Host -> sandbox: result for a page update request.
6
+ */
7
+ export const updatePageResultMessageSchema = v.variant("status", [
8
+ v.object({
9
+ type: v.literal("updatePageResult"),
10
+ requestId: v.string(),
11
+ status: v.literal("success"),
12
+ page: notionPageSchema,
13
+ }),
14
+ v.object({
15
+ type: v.literal("updatePageResult"),
16
+ requestId: v.string(),
17
+ status: v.literal("error"),
18
+ error: v.string(),
19
+ }),
20
+ ])
21
+
22
+ export type UpdatePageResultMessage = v.InferOutput<
23
+ typeof updatePageResultMessageSchema
24
+ >
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Notion page shapes exchanged over the custom block bridge.
3
+ *
4
+ * These mirror a narrow subset of Notion's public `POST /v1/pages` API, since that's the bridge
5
+ * shape the host delivers for messages like `createPageResult`.
6
+ */
7
+
8
+ import * as v from "valibot"
9
+ import type { NotionDataSourceId } from "../ids"
10
+
11
+ declare const notionPageIdBrand: unique symbol
12
+
13
+ /**
14
+ * Branded Notion page ID. Host payloads and SDK APIs use plain strings at runtime,
15
+ * but the brand keeps page IDs from being accidentally mixed with other IDs in TypeScript.
16
+ */
17
+ export type NotionPageId = string & {
18
+ readonly [notionPageIdBrand]: "NotionPageId"
19
+ }
20
+
21
+ /**
22
+ * Parent reference accepted by `pages.create` / `pages.update` inputs.
23
+ *
24
+ * Mirrors the parent shape Notion's public `POST /v1/pages` API accepts: `page_id` for a
25
+ * page-parented child, `data_source_id` for a database row. `data_source_id` is the internal
26
+ * collection ID.
27
+ */
28
+ export const notionPageParentInputSchema = v.variant("type", [
29
+ v.object({ type: v.literal("page_id"), page_id: v.string() }),
30
+ v.object({
31
+ type: v.literal("data_source_id"),
32
+ data_source_id: v.string(),
33
+ }),
34
+ ])
35
+ export type NotionPageParentInput =
36
+ | { type: "page_id"; page_id: NotionPageId }
37
+ | { type: "data_source_id"; data_source_id: NotionDataSourceId }
38
+
39
+ /**
40
+ * Parent reference returned on a `Page` object from the host.
41
+ *
42
+ * Wider than the input shape: in addition to `page_id` / `data_source_id`, this includes
43
+ * `workspace` (for top-level/team-rooted pages) and `block_id` (for pages nested under a
44
+ * non-page block, e.g. a column or toggle). This mirrors the parent variants the public API
45
+ * surfaces on `Page` responses.
46
+ */
47
+ export const notionPageParentSchema = v.variant("type", [
48
+ v.object({ type: v.literal("page_id"), page_id: v.string() }),
49
+ v.object({
50
+ type: v.literal("data_source_id"),
51
+ data_source_id: v.string(),
52
+ }),
53
+ v.object({ type: v.literal("workspace"), workspace: v.literal(true) }),
54
+ v.object({ type: v.literal("block_id"), block_id: v.string() }),
55
+ ])
56
+ export type NotionPageParent =
57
+ | { type: "page_id"; page_id: NotionPageId }
58
+ | { type: "data_source_id"; data_source_id: NotionDataSourceId }
59
+ | { type: "workspace"; workspace: true }
60
+ | { type: "block_id"; block_id: string }
61
+
62
+ export const notionEmojiIconSchema = v.object({
63
+ type: v.literal("emoji"),
64
+ emoji: v.string(),
65
+ })
66
+ export const notionCustomEmojiIconSchema = v.object({
67
+ type: v.literal("custom_emoji"),
68
+ custom_emoji: v.object({
69
+ id: v.string(),
70
+ name: v.optional(v.string()),
71
+ url: v.optional(v.string()),
72
+ }),
73
+ })
74
+ export const notionExternalFileSchema = v.object({
75
+ type: v.literal("external"),
76
+ external: v.object({ url: v.string() }),
77
+ })
78
+ export const notionHostedFileSchema = v.object({
79
+ type: v.literal("file"),
80
+ file: v.object({
81
+ url: v.string(),
82
+ expiry_time: v.optional(v.string()),
83
+ }),
84
+ })
85
+
86
+ // TODO: Add file upload support.
87
+ export const notionPageIconSchema = v.variant("type", [
88
+ notionEmojiIconSchema,
89
+ notionCustomEmojiIconSchema,
90
+ notionExternalFileSchema,
91
+ notionHostedFileSchema,
92
+ ])
93
+ export type NotionPageIcon = v.InferOutput<typeof notionPageIconSchema>
94
+
95
+ export const notionPageCoverSchema = v.variant("type", [
96
+ notionExternalFileSchema,
97
+ notionHostedFileSchema,
98
+ ])
99
+ export type NotionPageCover = v.InferOutput<typeof notionPageCoverSchema>
100
+
101
+ const nullableStringSchema = v.nullable(v.string())
102
+
103
+ const notionRichTextItemSchema = v.record(v.string(), v.unknown())
104
+ export type NotionRichTextItem = v.InferOutput<typeof notionRichTextItemSchema>
105
+
106
+ const notionSelectOptionInputSchema = v.union([
107
+ v.object({
108
+ id: v.string(),
109
+ name: v.optional(v.string()),
110
+ color: v.optional(v.string()),
111
+ description: v.optional(nullableStringSchema),
112
+ }),
113
+ v.object({
114
+ name: v.string(),
115
+ id: v.optional(v.string()),
116
+ color: v.optional(v.string()),
117
+ description: v.optional(nullableStringSchema),
118
+ }),
119
+ ])
120
+ export type NotionSelectOptionInput = v.InferOutput<
121
+ typeof notionSelectOptionInputSchema
122
+ >
123
+
124
+ const notionDateInputSchema = v.object({
125
+ start: v.string(),
126
+ end: v.optional(nullableStringSchema),
127
+ time_zone: v.optional(nullableStringSchema),
128
+ })
129
+ export type NotionDateInput = v.InferOutput<typeof notionDateInputSchema>
130
+
131
+ const notionUserInputSchema = v.union([
132
+ v.object({
133
+ object: v.optional(v.literal("user")),
134
+ id: v.string(),
135
+ }),
136
+ v.object({
137
+ object: v.literal("group"),
138
+ id: v.string(),
139
+ name: v.optional(nullableStringSchema),
140
+ }),
141
+ ])
142
+ export type NotionUserInput = v.InferOutput<typeof notionUserInputSchema>
143
+
144
+ const notionRelationInputSchema = v.object({ id: v.string() })
145
+ export type NotionRelationInput = v.InferOutput<
146
+ typeof notionRelationInputSchema
147
+ >
148
+
149
+ const notionFileInputSchema = v.union([
150
+ v.object({
151
+ type: v.literal("external"),
152
+ name: v.string(),
153
+ external: v.object({ url: v.string() }),
154
+ }),
155
+ v.object({
156
+ type: v.literal("file"),
157
+ name: v.string(),
158
+ file: v.object({
159
+ url: v.string(),
160
+ expiry_time: v.optional(v.string()),
161
+ }),
162
+ }),
163
+ ])
164
+ export type NotionFileInput = v.InferOutput<typeof notionFileInputSchema>
165
+
166
+ const notionPlaceInputSchema = v.object({
167
+ lat: v.number(),
168
+ lon: v.number(),
169
+ name: v.optional(nullableStringSchema),
170
+ address: v.optional(nullableStringSchema),
171
+ aws_place_id: v.optional(nullableStringSchema),
172
+ google_place_id: v.optional(nullableStringSchema),
173
+ })
174
+ export type NotionPlaceInput = v.InferOutput<typeof notionPlaceInputSchema>
175
+
176
+ const pagePropertyBaseSchema = {
177
+ id: v.string(),
178
+ }
179
+
180
+ /**
181
+ * A page property value.
182
+ */
183
+ export const notionPagePropertyValueSchema = v.variant("type", [
184
+ v.object({
185
+ ...pagePropertyBaseSchema,
186
+ type: v.literal("title"),
187
+ title: v.array(notionRichTextItemSchema),
188
+ }),
189
+ v.object({
190
+ ...pagePropertyBaseSchema,
191
+ type: v.literal("rich_text"),
192
+ rich_text: v.array(notionRichTextItemSchema),
193
+ }),
194
+ v.object({
195
+ ...pagePropertyBaseSchema,
196
+ type: v.literal("number"),
197
+ number: v.nullable(v.number()),
198
+ }),
199
+ v.object({
200
+ ...pagePropertyBaseSchema,
201
+ type: v.literal("url"),
202
+ url: nullableStringSchema,
203
+ }),
204
+ v.object({
205
+ ...pagePropertyBaseSchema,
206
+ type: v.literal("email"),
207
+ email: nullableStringSchema,
208
+ }),
209
+ v.object({
210
+ ...pagePropertyBaseSchema,
211
+ type: v.literal("phone_number"),
212
+ phone_number: nullableStringSchema,
213
+ }),
214
+ v.object({
215
+ ...pagePropertyBaseSchema,
216
+ type: v.literal("checkbox"),
217
+ checkbox: v.boolean(),
218
+ }),
219
+ v.object({
220
+ ...pagePropertyBaseSchema,
221
+ type: v.literal("select"),
222
+ select: v.nullable(notionSelectOptionInputSchema),
223
+ }),
224
+ v.object({
225
+ ...pagePropertyBaseSchema,
226
+ type: v.literal("status"),
227
+ status: v.nullable(notionSelectOptionInputSchema),
228
+ }),
229
+ v.object({
230
+ ...pagePropertyBaseSchema,
231
+ type: v.literal("multi_select"),
232
+ multi_select: v.array(notionSelectOptionInputSchema),
233
+ }),
234
+ v.object({
235
+ ...pagePropertyBaseSchema,
236
+ type: v.literal("date"),
237
+ date: v.nullable(notionDateInputSchema),
238
+ }),
239
+ v.object({
240
+ ...pagePropertyBaseSchema,
241
+ type: v.literal("people"),
242
+ people: v.array(notionUserInputSchema),
243
+ }),
244
+ v.object({
245
+ ...pagePropertyBaseSchema,
246
+ type: v.literal("relation"),
247
+ has_more: v.optional(v.boolean()),
248
+ relation: v.array(notionRelationInputSchema),
249
+ }),
250
+ v.object({
251
+ ...pagePropertyBaseSchema,
252
+ type: v.literal("files"),
253
+ files: v.array(notionFileInputSchema),
254
+ }),
255
+ v.object({
256
+ ...pagePropertyBaseSchema,
257
+ type: v.literal("place"),
258
+ place: v.nullable(notionPlaceInputSchema),
259
+ }),
260
+ ])
261
+ export type NotionPagePropertyValue = v.InferOutput<
262
+ typeof notionPagePropertyValueSchema
263
+ >
264
+
265
+ type WithOptionalPropertyId<T> = T extends { id: string }
266
+ ? Omit<T, "id"> & { id?: string }
267
+ : never
268
+
269
+ /**
270
+ * Public SDK page property write input. Keys may be raw property IDs or data-source
271
+ * property keys. `id` is optional because the SDK resolves the final raw property ID
272
+ * from the map key before sending the bridge message.
273
+ */
274
+ export type NotionPagePropertyInputValue =
275
+ WithOptionalPropertyId<NotionPagePropertyValue>
276
+
277
+ export type NotionPagePropertyInputMap = {
278
+ [propertyIdOrKey: string]: NotionPagePropertyInputValue
279
+ }
280
+
281
+ export const notionPagePropertyWriteMapSchema = v.record(
282
+ v.string(),
283
+ notionPagePropertyValueSchema,
284
+ )
285
+ export type NotionPagePropertyWriteMap = v.InferOutput<
286
+ typeof notionPagePropertyWriteMapSchema
287
+ >
288
+
289
+ /**
290
+ * The `Page` object returned in `createPageResult` messages.
291
+ */
292
+ export const notionPageSchema = v.object({
293
+ object: v.literal("page"),
294
+ id: v.string(),
295
+ parent: notionPageParentSchema,
296
+ properties: notionPagePropertyWriteMapSchema,
297
+ created_time: v.optional(v.string()),
298
+ last_edited_time: v.optional(v.string()),
299
+ icon: v.optional(notionPageIconSchema),
300
+ cover: v.optional(notionPageCoverSchema),
301
+ url: v.optional(v.string()),
302
+ public_url: v.optional(v.string()),
303
+ in_trash: v.optional(v.boolean()),
304
+ })
305
+ export type NotionPage = Omit<
306
+ v.InferOutput<typeof notionPageSchema>,
307
+ "id" | "parent" | "properties" | "icon" | "cover"
308
+ > & {
309
+ id: NotionPageId
310
+ parent: NotionPageParent
311
+ properties: NotionPagePropertyWriteMap
312
+ icon?: NotionPageIcon
313
+ cover?: NotionPageCover
314
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Tracks one-shot request/response pairs over the `postMessage` bridge. Each `allocate` reserves a
3
+ * unique `requestId` and stores its resolver. `resolve` finds the pending resolver by `requestId`
4
+ * and calls it, then removes the entry so the same `requestId` cannot be resolved twice.
5
+ */
6
+ export class PendingRequests<T> {
7
+ private nextId = 1
8
+ private readonly entries = new Map<string, (value: T) => void>()
9
+
10
+ constructor(private readonly prefix: string) {}
11
+
12
+ allocate(resolver: (value: T) => void): string {
13
+ const requestId = `${this.prefix}-${this.nextId}`
14
+ this.nextId += 1
15
+ this.entries.set(requestId, resolver)
16
+ return requestId
17
+ }
18
+
19
+ resolve(requestId: string, value: T): boolean {
20
+ const resolver = this.entries.get(requestId)
21
+ if (resolver === undefined) {
22
+ return false
23
+ }
24
+ this.entries.delete(requestId)
25
+ resolver(value)
26
+ return true
27
+ }
28
+ }
@@ -0,0 +1,112 @@
1
+ import type {
2
+ CreatePageInput,
3
+ CreatePageResult,
4
+ GetPageResult,
5
+ GetUserResult,
6
+ ListUsersInput,
7
+ ListUsersResult,
8
+ NotionPageId,
9
+ NotionUserId,
10
+ UpdatePageInput,
11
+ UpdatePageResult,
12
+ } from "../types"
13
+ import {
14
+ type CustomBlockHostState,
15
+ type DataSourceQueryView,
16
+ getDataSourceQueryView as getDataSourceQueryViewWithBridge,
17
+ } from "./hostState"
18
+ import type { CustomBlockManifest } from "./manifest"
19
+ import type { InitMessage } from "./messages/init"
20
+ import { SandboxBridge } from "./SandboxBridge"
21
+
22
+ const bridge = new SandboxBridge()
23
+
24
+ export function sendCustomBlockReady(manifest: CustomBlockManifest | null) {
25
+ bridge.sendReady(manifest)
26
+ }
27
+
28
+ export function awaitCustomBlockInit(
29
+ signal?: AbortSignal,
30
+ ): Promise<InitMessage> {
31
+ return bridge.awaitInit(signal)
32
+ }
33
+
34
+ export function subscribeToCustomBlockHost(listener: () => void) {
35
+ return bridge.subscribe(listener)
36
+ }
37
+
38
+ export function getCustomBlockHostState(): CustomBlockHostState {
39
+ return bridge.getHostState()
40
+ }
41
+
42
+ export function queryCustomBlockDataSource(key: string, limit: number) {
43
+ bridge.queryDataSource(key, limit)
44
+ }
45
+
46
+ /**
47
+ * Apply an `init` payload to the bridge directly, bypassing the postMessage
48
+ * handshake. Used by the React provider when it needs to seed placeholder
49
+ * state (e.g. the standalone preview fallback when not embedded in Notion).
50
+ * The bridge applies the payload through the same code path as a real host.
51
+ */
52
+ export function setMockCustomBlockState(message: InitMessage) {
53
+ bridge.setMockState(message)
54
+ }
55
+
56
+ export function postCustomBlockResize(height: number) {
57
+ bridge.postResize(height)
58
+ }
59
+
60
+ export function getUser(userId: NotionUserId): Promise<GetUserResult> {
61
+ return bridge.getUser(userId)
62
+ }
63
+
64
+ export function listUsers(input?: ListUsersInput): Promise<ListUsersResult> {
65
+ return bridge.listUsers(input)
66
+ }
67
+
68
+ /**
69
+ * Resolves the host state for a given data-source key into the public {@link DataSourceQueryView},
70
+ * baking in the singleton bridge so per-row `update` calls flow through it.
71
+ */
72
+ export function getDataSourceQueryView(
73
+ hostState: CustomBlockHostState,
74
+ key: string,
75
+ ): DataSourceQueryView {
76
+ return getDataSourceQueryViewWithBridge(hostState, key, args =>
77
+ bridge.updateDataSourcePage(args),
78
+ )
79
+ }
80
+
81
+ /**
82
+ * Page-related SDK APIs exposed under `sdk.pages.*`.
83
+ */
84
+ export const pages = {
85
+ /**
86
+ * Creates a new Notion page.
87
+ */
88
+ create(input: CreatePageInput): Promise<CreatePageResult> {
89
+ return bridge.createPage(input)
90
+ },
91
+
92
+ /**
93
+ * Fetches a page by id.
94
+ */
95
+ get(pageId: NotionPageId): Promise<GetPageResult> {
96
+ return bridge.getPage(pageId)
97
+ },
98
+
99
+ /**
100
+ * Updates an existing page.
101
+ */
102
+ update(input: UpdatePageInput): Promise<UpdatePageResult> {
103
+ return bridge.updatePage(input)
104
+ },
105
+
106
+ /**
107
+ * Soft-delete a page by archiving it.
108
+ */
109
+ delete(pageId: NotionPageId): Promise<UpdatePageResult> {
110
+ return bridge.updatePage({ pageId, archived: true })
111
+ },
112
+ }
@@ -0,0 +1,5 @@
1
+ import * as v from "valibot"
2
+
3
+ export const notionThemeSchema = v.picklist(["light", "dark"])
4
+
5
+ export type NotionTheme = v.InferOutput<typeof notionThemeSchema>
@@ -0,0 +1,31 @@
1
+ import * as v from "valibot"
2
+
3
+ declare const notionUserIdBrand: unique symbol
4
+
5
+ export type NotionUserId = string & {
6
+ readonly [notionUserIdBrand]: "NotionUserId"
7
+ }
8
+
9
+ export const notionUserSchema = v.object({
10
+ object: v.literal("user"),
11
+ id: v.string(),
12
+ name: v.optional(v.string()),
13
+ avatar_url: v.nullable(v.string()),
14
+ type: v.literal("person"),
15
+ person: v.object({
16
+ email: v.string(),
17
+ }),
18
+ })
19
+
20
+ export type NotionUser = v.InferOutput<typeof notionUserSchema>
21
+
22
+ export const notionUserListSchema = v.object({
23
+ object: v.literal("list"),
24
+ results: v.array(notionUserSchema),
25
+ next_cursor: v.nullable(v.string()),
26
+ has_more: v.boolean(),
27
+ type: v.literal("user"),
28
+ user: v.record(v.string(), v.unknown()),
29
+ })
30
+
31
+ export type NotionUserList = v.InferOutput<typeof notionUserListSchema>
@@ -0,0 +1,45 @@
1
+ # Context & theme
2
+
3
+ Every custom block runs inside a larger Notion document. These hooks expose that environment: the block itself, the containers it sits inside, and how the surrounding Notion app is currently presented (e.g. light vs. dark theme).
4
+
5
+ ## API
6
+
7
+ ### `useCustomBlockContext()`
8
+
9
+ Returns the block's location in the document tree:
10
+
11
+ - `customBlockId` — the block's own ID.
12
+ - `parent` — the immediate container (e.g., toggle, column, callout, the page itself).
13
+ - `page` — the closest enclosing `page` / `collection_view_page` ancestor.
14
+
15
+ Re-renders on `contextChanged` (e.g. the block moves containers).
16
+
17
+ ```ts
18
+ function useCustomBlockContext(): NotionCustomBlockContext;
19
+
20
+ type NotionCustomBlockContext = {
21
+ customBlockId: string;
22
+ parent: { id: string; type: string };
23
+ page: { id: string; type: string };
24
+ };
25
+ ```
26
+
27
+ ### `useTheme()`
28
+
29
+ Returns the host's current theme.
30
+
31
+ Re-renders on every `themeChanged` message.
32
+
33
+ ```ts
34
+ function useTheme(): NotionTheme; // "light" | "dark"
35
+ ```
36
+
37
+ ```tsx
38
+ import { useCustomBlockContext, useTheme } from "ncblock";
39
+
40
+ export function Header() {
41
+ const { page } = useCustomBlockContext();
42
+ const theme = useTheme();
43
+ return <header data-theme={theme}>Page: {page.id}</header>;
44
+ }
45
+ ```