ncblock 0.0.4 → 0.0.6

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 (102) hide show
  1. package/HOST.md +68 -0
  2. package/dist/bridge/SandboxBridge.d.ts.map +1 -1
  3. package/dist/bridge/context.d.ts.map +1 -1
  4. package/dist/bridge/dataSources/dataSource.d.ts.map +1 -1
  5. package/dist/bridge/dataSources/dataSourcePage.d.ts.map +1 -1
  6. package/dist/bridge/dataSources/dataSourceValue.d.ts.map +1 -1
  7. package/dist/bridge/dataSources/dateValue.d.ts.map +1 -1
  8. package/dist/bridge/dataSources/propertySchema.d.ts.map +1 -1
  9. package/dist/bridge/dataSources/recordPointer.d.ts.map +1 -1
  10. package/dist/bridge/dataSources/resolve.d.ts.map +1 -1
  11. package/dist/bridge/dataSources/resolveProperty.d.ts.map +1 -1
  12. package/dist/bridge/hostState.d.ts.map +1 -1
  13. package/dist/bridge/ids.d.ts.map +1 -1
  14. package/dist/bridge/incomingType.d.ts.map +1 -1
  15. package/dist/bridge/loadManifest.d.ts.map +1 -1
  16. package/dist/bridge/manifest.d.ts.map +1 -1
  17. package/dist/bridge/messages/contextChanged.d.ts.map +1 -1
  18. package/dist/bridge/messages/createPage.d.ts.map +1 -1
  19. package/dist/bridge/messages/createPageResult.d.ts.map +1 -1
  20. package/dist/bridge/messages/dataSourcesChanged.d.ts.map +1 -1
  21. package/dist/bridge/messages/getPage.d.ts.map +1 -1
  22. package/dist/bridge/messages/getUser.d.ts.map +1 -1
  23. package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -1
  24. package/dist/bridge/messages/init.d.ts.map +1 -1
  25. package/dist/bridge/messages/invalidHostMessage.d.ts.map +1 -1
  26. package/dist/bridge/messages/invalidSandboxMessage.d.ts.map +1 -1
  27. package/dist/bridge/messages/listUsers.d.ts.map +1 -1
  28. package/dist/bridge/messages/queryDataSource.d.ts.map +1 -1
  29. package/dist/bridge/messages/queryDataSourceResult.d.ts.map +1 -1
  30. package/dist/bridge/messages/ready.d.ts.map +1 -1
  31. package/dist/bridge/messages/resize.d.ts.map +1 -1
  32. package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -1
  33. package/dist/bridge/messages/themeChanged.d.ts.map +1 -1
  34. package/dist/bridge/messages/updatePage.d.ts.map +1 -1
  35. package/dist/bridge/messages/updatePageResult.d.ts.map +1 -1
  36. package/dist/bridge/pages/page.d.ts.map +1 -1
  37. package/dist/bridge/pendingRequests.d.ts.map +1 -1
  38. package/dist/bridge/sandboxClient.d.ts.map +1 -1
  39. package/dist/bridge/theme.d.ts.map +1 -1
  40. package/dist/bridge/users/user.d.ts.map +1 -1
  41. package/dist/host.d.ts.map +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/init.d.ts.map +1 -1
  44. package/dist/react.d.ts.map +1 -1
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/users.d.ts.map +1 -1
  47. package/dist/utils.d.ts.map +1 -1
  48. package/docs/context.md +45 -0
  49. package/docs/data-sources.md +161 -0
  50. package/docs/lifecycle.md +92 -0
  51. package/docs/manifest.md +42 -0
  52. package/docs/pages.md +143 -0
  53. package/docs/users.md +61 -0
  54. package/package.json +12 -9
  55. package/src/bridge/SandboxBridge.ts +659 -0
  56. package/src/bridge/context.ts +58 -0
  57. package/src/bridge/dataSources/dataSource.ts +63 -0
  58. package/src/bridge/dataSources/dataSourcePage.ts +69 -0
  59. package/src/bridge/dataSources/dataSourceValue.ts +19 -0
  60. package/src/bridge/dataSources/dateValue.ts +96 -0
  61. package/src/bridge/dataSources/propertySchema.ts +186 -0
  62. package/src/bridge/dataSources/recordPointer.ts +13 -0
  63. package/src/bridge/dataSources/resolve.ts +96 -0
  64. package/src/bridge/dataSources/resolveProperty.ts +96 -0
  65. package/src/bridge/hostState.ts +146 -0
  66. package/src/bridge/ids.ts +30 -0
  67. package/src/bridge/incomingType.ts +19 -0
  68. package/src/bridge/loadManifest.ts +54 -0
  69. package/src/bridge/manifest.ts +53 -0
  70. package/src/bridge/messages/contextChanged.ts +15 -0
  71. package/src/bridge/messages/createPage.ts +64 -0
  72. package/src/bridge/messages/createPageResult.ts +25 -0
  73. package/src/bridge/messages/dataSourcesChanged.ts +18 -0
  74. package/src/bridge/messages/getPage.ts +32 -0
  75. package/src/bridge/messages/getUser.ts +32 -0
  76. package/src/bridge/messages/hostToSandbox.ts +33 -0
  77. package/src/bridge/messages/init.ts +20 -0
  78. package/src/bridge/messages/invalidHostMessage.ts +16 -0
  79. package/src/bridge/messages/invalidSandboxMessage.ts +18 -0
  80. package/src/bridge/messages/listUsers.ts +33 -0
  81. package/src/bridge/messages/queryDataSource.ts +16 -0
  82. package/src/bridge/messages/queryDataSourceResult.ts +18 -0
  83. package/src/bridge/messages/ready.ts +25 -0
  84. package/src/bridge/messages/resize.ts +13 -0
  85. package/src/bridge/messages/sandboxToHost.ts +30 -0
  86. package/src/bridge/messages/themeChanged.ts +15 -0
  87. package/src/bridge/messages/updatePage.ts +21 -0
  88. package/src/bridge/messages/updatePageResult.ts +24 -0
  89. package/src/bridge/pages/page.ts +314 -0
  90. package/src/bridge/pendingRequests.ts +28 -0
  91. package/src/bridge/sandboxClient.ts +112 -0
  92. package/src/bridge/theme.ts +5 -0
  93. package/src/bridge/users/user.ts +31 -0
  94. package/src/host.ts +67 -0
  95. package/src/index.ts +86 -0
  96. package/src/init.ts +92 -0
  97. package/src/react.tsx +418 -0
  98. package/src/types.ts +157 -0
  99. package/src/users.ts +26 -0
  100. package/src/utils.ts +13 -0
  101. package/vite-plugin/index.d.ts +46 -0
  102. package/vite-plugin/index.js +115 -0
@@ -0,0 +1,161 @@
1
+ # Data sources
2
+
3
+ A custom block declares its **data sources** — semantic keys like `people` or `tasks` — in `custom_blocks.json`. When the block is added to a Notion page, a user maps each key to a real Notion database. Inside the block, you read by the semantic key and the SDK handles the lookup for you. Use `useDataSource(key)` for the rows themselves, and `useDataSourceDefinitions()` when you need the schema (debug panels, schema-driven UIs).
4
+
5
+ ## Pages within a data source
6
+
7
+ Each row returned by `useDataSource` is a `NotionDataSourcePage` — `{ id, propertiesById, propertiesByKey, update }`. Read property values through either of the two views:
8
+
9
+ - `propertiesByKey[key]` — keyed by the semantic property keys you declared in the manifest.
10
+ - `propertiesById[propertyId]` — keyed by the raw Notion property ID.
11
+
12
+ The four built-ins (`created_time`, `last_edited_time`, `created_by`, `last_edited_by`) are always present in `propertiesById` (and `collectionSchema.propertiesById`), never in the `*ByKey` views — they don't have semantic keys.
13
+
14
+ ### Updating a row
15
+
16
+ Each page carries its own `update` helper:
17
+
18
+ ```ts
19
+ await row.update({
20
+ properties: {
21
+ score: { type: "number", number: 8 }, // semantic key
22
+ },
23
+ icon: { type: "emoji", emoji: "✅" },
24
+ });
25
+ ```
26
+
27
+ Because the helper is bound to its data source, you can write property values keyed by **either** semantic keys or raw IDs — the SDK resolves them before sending the request. The input and result shapes are `NotionDataSourcePageUpdateInput` and `NotionDataSourcePageUpdateResult`.
28
+
29
+ Use `row.update(...)` whenever you already have a row in hand. For pages you don't have a row for (e.g. you only have a `pageId`), drop down to the top-level [`pages` API](./pages.md) — it covers create / get / update / delete and accepts raw property IDs only.
30
+
31
+ ## API
32
+
33
+ ### `useDataSource(key, initialLimit?)`
34
+
35
+ ```ts
36
+ function useDataSource(key: string, initialLimit?: number): UseDataSourceResult;
37
+
38
+ type UseDataSourceResult = {
39
+ items: NotionDataSourcePage[];
40
+ collectionSchema?: NotionCollectionSchema;
41
+ propertySchemasById: { [propertyId: string]: NotionPropertySchema };
42
+ propertySchemasByKey: { [key: string]: NotionPropertySchema | undefined };
43
+ isLoading: boolean;
44
+ hasMore: boolean;
45
+ fetchMore: () => void;
46
+ error?: string;
47
+ };
48
+ ```
49
+
50
+ Reads the data source mapped to `key`. `initialLimit` defaults to 20. `fetchMore()` re-requests a larger prefix (no cursors yet); page growth tracks `initialLimit`, so pass a larger value to opt into bigger batches. `propertySchemasByKey` is `undefined` for declared-but-unbound slots. The hook resets to `initialLimit` when the underlying definition changes.
51
+
52
+ ### `useDataSourceDefinitions()`
53
+
54
+ ```ts
55
+ function useDataSourceDefinitions(): NotionDataSource[];
56
+ ```
57
+
58
+ Resolved data-source definitions (semantic key plus `collectionSchema`, `propertyIdsByKey`, derived `propertySchemasById`). Most templates use `useDataSource` instead — reach for this hook only when rendering the configuration itself (debug panels, schema-driven UIs).
59
+
60
+ ## Example: querying a data source
61
+
62
+ A typical data-driven view picks a key, calls `useDataSource`, schema-checks the rows, and surfaces a setup hint when the mapped collection is missing the expected fields. Trimmed from `templates/radar-chart`:
63
+
64
+ ```tsx
65
+ import {
66
+ type NotionDataSourcePage,
67
+ useDataSource,
68
+ } from "ncblock";
69
+
70
+ const KEY = "people";
71
+
72
+ function isComplete(item: NotionDataSourcePage): boolean {
73
+ return (
74
+ typeof item.propertiesByKey.name === "string" &&
75
+ typeof item.propertiesByKey.score === "number" &&
76
+ Number.isFinite(item.propertiesByKey.score)
77
+ );
78
+ }
79
+
80
+ export function ScoreList() {
81
+ const { items, isLoading, hasMore, fetchMore, error } = useDataSource(KEY);
82
+
83
+ if (error) return <div role="alert">Couldn't load: {error}</div>;
84
+ if (isLoading && items.length === 0) return <div>Loading…</div>;
85
+
86
+ const ready = items.filter(isComplete);
87
+ if (ready.length === 0) {
88
+ return (
89
+ <div>
90
+ Map a data source with key <code>{KEY}</code> exposing <code>name</code>{" "}
91
+ (text) and <code>score</code> (number).
92
+ </div>
93
+ );
94
+ }
95
+
96
+ return (
97
+ <div>
98
+ <ul>
99
+ {ready.map((item) => (
100
+ <li key={item.id}>
101
+ {String(item.propertiesByKey.name)} —{" "}
102
+ {Number(item.propertiesByKey.score)}
103
+ </li>
104
+ ))}
105
+ </ul>
106
+ {hasMore ? (
107
+ <button type="button" onClick={fetchMore} disabled={isLoading}>
108
+ {isLoading ? "Loading…" : "Load more"}
109
+ </button>
110
+ ) : null}
111
+ </div>
112
+ );
113
+ }
114
+ ```
115
+
116
+ ## Types
117
+
118
+ ### Rows & values
119
+
120
+ - `NotionDataSource` — a resolved data source: semantic key, `collectionSchema`, `propertyIdsByKey`, `propertySchemasById`.
121
+ - `NotionDataSourcePage` — a single row exposed to app code: `{ id, propertiesById, propertiesByKey, update }`.
122
+ - `NotionDataSourceValue` — the discriminated union of values that can appear inside `propertiesById[propertyId]`. Date values branch into the `NotionDateValue` union below.
123
+ - `NotionDataSourcePageUpdateInput` / `NotionDataSourcePageUpdateResult` — input and result for the per-page `update` helper.
124
+
125
+ ### Property schemas
126
+
127
+ - `NotionPropertySchema` — schema for a single property (type plus type-specific config like `select` options).
128
+ - `NotionPropertyType` — string-literal union of every supported property type.
129
+ - `NOTION_PROPERTY_TYPES` — runtime list of those literals (handy for switch coverage and validation).
130
+ - `NotionPropertyOption` — a single `select` / `multi_select` / `status` option (`{ id, name, color }`).
131
+ - `NotionPropertyColor` — the color literal used by options and groups.
132
+ - `NotionStatusGroup` — the `status` property's "To do / In progress / Done" grouping.
133
+ - `NotionDualProperty` — properties that have both a primary and a secondary axis (e.g. `unique_id` prefix + number).
134
+ - `NotionBuiltinPropertyId` — string-literal union of the four synthetic property IDs (`created_time`, `last_edited_time`, `created_by`, `last_edited_by`).
135
+ - `NOTION_BUILTIN_PROPERTY_IDS` — runtime list of the four built-in IDs.
136
+
137
+ ### Collection schema & pointers
138
+
139
+ - `NotionCollectionSchema` — host-supplied schema for the bound collection, including raw property schemas.
140
+ - `NotionRecordPointer` — `{ id, table }`. Generic reference to any Notion record (page, block, collection row). Exported for convenience; `useDataSource` and the `pages` API don't take one as input.
141
+
142
+ ### IDs
143
+
144
+ Branded string types — they're plain strings at runtime but TypeScript distinguishes them.
145
+
146
+ - `NotionDataSourceId`
147
+ - `NotionSpaceId`
148
+
149
+ ### Date values
150
+
151
+ Returned wherever a date / date-range value appears (e.g. inside `propertiesByKey` for a `date` property). All-day values use `NotionDate*` shapes; values with a time component use `NotionDateTime*`.
152
+
153
+ - `NotionDateValue` — discriminated union covering every date-shaped value below.
154
+ - `NotionDate`
155
+ - `NotionDateRange`
156
+ - `NotionDateTime`
157
+ - `NotionDateTimeRange`
158
+ - `NotionDateReminder`
159
+ - `NotionDateTimeReminder`
160
+ - `NotionTimeReminder`
161
+ - `NotionNoReminder`
@@ -0,0 +1,92 @@
1
+ # Lifecycle
2
+
3
+ The SDK ↔ host handshake, the React wrapper that runs it, and the auto-resize hook that keeps the iframe in sync with your content.
4
+
5
+ ## Handshake
6
+
7
+ `initCustomBlock()` posts `ready` to `window.parent` and awaits the host's `init` (theme, context, and `dataSources: { bindings }` keyed by semantic data-source key, which the SDK resolves against the manifest). The promise resolves with the normalized initial state, captured in `CustomBlockInitial` — `await` it before mounting React so hooks always see populated state.
8
+
9
+ - Default `timeoutMs` is 2000; rejects with `TimeoutError` if the host doesn't respond.
10
+ - In a top-level browser tab (no parent frame), rejects with `NotInIframeError`. `<NotionCustomBlock>` catches this, seeds placeholders, and renders `children` behind a warning banner so dev-time previews still work.
11
+ - After init, `themeChanged`, `contextChanged`, and `dataSourcesChanged` push updates and the relevant hooks re-render.
12
+ - `initCustomBlock` is idempotent; subsequent calls return the same promise.
13
+
14
+ ## Sizing
15
+
16
+ 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.
17
+
18
+ - **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.
19
+ - Prefer container queries (`@container`) over viewport queries.
20
+
21
+ ## API
22
+
23
+ All hooks below assume `initCustomBlock()` has resolved — they throw if called before that. Inside `<NotionCustomBlock>` (or past the `isLoaded` gate of `useCustomBlockInit`), single-value hooks return non-nullable values.
24
+
25
+ ### `<NotionCustomBlock>`
26
+
27
+ ```ts
28
+ type NotionCustomBlockProps = InitCustomBlockOptions & {
29
+ children: ReactNode;
30
+ fallback?: ReactNode;
31
+ errorFallback?: ReactNode | ((error: Error) => ReactNode);
32
+ autoResize?: boolean; // defaults to true
33
+ };
34
+ ```
35
+
36
+ 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.
37
+
38
+ ### `useCustomBlockInit(opts?)`
39
+
40
+ ```ts
41
+ function useCustomBlockInit(opts?: InitCustomBlockOptions): UseCustomBlockInitResult;
42
+
43
+ type UseCustomBlockInitResult =
44
+ | { isLoaded: false; error: undefined }
45
+ | { isLoaded: false; error: Error }
46
+ | { isLoaded: true; error: undefined; initial: CustomBlockInitial };
47
+ ```
48
+
49
+ React wrapper around `initCustomBlock` for templates that prefer not to use top-level `await`. Multiple components calling it share the same handshake.
50
+
51
+ ```tsx
52
+ function Root() {
53
+ const init = useCustomBlockInit();
54
+ if (init.error) return <p role="alert">Init failed: {init.error.message}</p>;
55
+ if (!init.isLoaded) return null;
56
+ return <App />;
57
+ }
58
+ ```
59
+
60
+ ### `initCustomBlock(opts?)`
61
+
62
+ ```ts
63
+ function initCustomBlock(opts?: InitCustomBlockOptions): Promise<CustomBlockInitial>;
64
+
65
+ type InitCustomBlockOptions = { timeoutMs?: number };
66
+ ```
67
+
68
+ The lower-level promise API. `<NotionCustomBlock>` and `useCustomBlockInit` both call it for you. Reach for it directly only when you want to `await` init at module scope (e.g. before `ReactDOM.createRoot`).
69
+
70
+ ### `NotInIframeError`
71
+
72
+ Thrown when `initCustomBlock` is called in a top-level tab (no parent frame). `<NotionCustomBlock>` catches it and falls back to a standalone preview with a warning banner; direct callers can `instanceof NotInIframeError` to apply their own policy.
73
+
74
+ ### `useCustomBlockAutoResize({ enabled? })`
75
+
76
+ ```ts
77
+ function useCustomBlockAutoResize(args?: { enabled?: boolean }): void;
78
+ ```
79
+
80
+ Measures `#root` with `Math.ceil(scrollHeight)` 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.
81
+
82
+ ```tsx
83
+ <NotionCustomBlock autoResize={false}>
84
+ <App />
85
+ </NotionCustomBlock>;
86
+
87
+ function App() {
88
+ const [enabled, setEnabled] = useState(true);
89
+ useCustomBlockAutoResize({ enabled });
90
+ return <div>…</div>;
91
+ }
92
+ ```
@@ -0,0 +1,42 @@
1
+ # Manifest
2
+
3
+ A custom view declares its required data sources in `custom_blocks.json` at the project root. Notion uses the manifest to know what semantic keys the block expects, what shape each property should be, and what to show when an admin is configuring the block.
4
+
5
+ ```json
6
+ {
7
+ "version": 1,
8
+ "dataSources": {
9
+ "tasks": {
10
+ "name": "Tasks",
11
+ "description": "The collection of tasks to render",
12
+ "properties": {
13
+ "title": { "name": "Title", "type": "title" },
14
+ "dueDate": { "name": "Due date", "type": "date" }
15
+ }
16
+ }
17
+ }
18
+ }
19
+ ```
20
+
21
+ `initCustomBlock()` fetches `custom_blocks.json` and forwards it with `ready`. The `notionCustomBlock()` Vite plugin from `ncblock/vite` serves it in dev and emits it into `dist/` on build. If the file is missing or invalid, the SDK sends `manifest: null`.
22
+
23
+ ## Vite plugin
24
+
25
+ ```ts
26
+ import { defineConfig } from "vite";
27
+ import react from "@vitejs/plugin-react";
28
+ import { notionCustomBlock } from "ncblock/vite";
29
+
30
+ export default defineConfig({
31
+ plugins: [react(), notionCustomBlock()],
32
+ });
33
+ ```
34
+
35
+ In dev, the plugin serves `custom_blocks.json` from the project root so HMR + the SDK handshake see the same file. On `vite build`, it emits `custom_blocks.json` into `dist/` as a separate asset alongside the bundled HTML and JS.
36
+
37
+ ## Types
38
+
39
+ - `CustomBlockManifest` — the parsed shape of `custom_blocks.json`.
40
+ - `ManifestDataSource` — a single entry in `dataSources` (name, description, properties).
41
+ - `ManifestProperty` — a single property declaration inside a `ManifestDataSource`.
42
+ - `ManifestIcon` — the icon variant accepted on a `ManifestDataSource`.
package/docs/pages.md ADDED
@@ -0,0 +1,143 @@
1
+ # `pages` API
2
+
3
+ Helpers for creating, reading, updating, and archiving Notion pages from inside a custom block. The SDK forwards each call to Notion on your behalf.
4
+
5
+ ```ts
6
+ import { pages } from "ncblock";
7
+ ```
8
+
9
+ Every helper returns a discriminated result instead of throwing:
10
+
11
+ ```ts
12
+ const result = await pages.get(pageId);
13
+ if (result.status === "success") {
14
+ // result.page is a NotionPage
15
+ } else {
16
+ // result.error is a human-readable string
17
+ }
18
+ ```
19
+
20
+ Always check `result.status` before reading `result.page`.
21
+
22
+ ## Creating pages
23
+
24
+ `pages.create` mirrors Notion's [`POST /v1/pages`](https://developers.notion.com/reference/post-page). Pass a parent, a property map, and (optionally) an `icon`, `cover`, or `position`:
25
+
26
+ ```ts
27
+ const result = await pages.create({
28
+ parent: { type: "data_source_key", key: "tasks" },
29
+ properties: {
30
+ title: {
31
+ type: "title",
32
+ title: [{ type: "text", text: { content: "New task" } }],
33
+ },
34
+ dueDate: { type: "date", date: { start: "2026-06-01" } },
35
+ },
36
+ icon: { type: "emoji", emoji: "📝" },
37
+ position: { type: "end" },
38
+ });
39
+
40
+ if (result.status === "success") {
41
+ console.log("created", result.page.id);
42
+ }
43
+ ```
44
+
45
+ ### Choosing a parent
46
+
47
+ `parent` is a `CreatePageParent`:
48
+
49
+ ```ts
50
+ type CreatePageParent =
51
+ | { type: "page_id"; page_id: NotionPageId }
52
+ | { type: "data_source_id"; data_source_id: NotionDataSourceId }
53
+ | { type: "data_source_key"; key: string };
54
+ ```
55
+
56
+ `type: "data_source_key"` is the recommended form inside a custom view. Pass the semantic key you declared in `custom_blocks.json` (e.g. `"tasks"`) and the SDK looks up the corresponding data source for you. The other two variants exist for the rarer case where you already have a raw Notion ID in hand.
57
+
58
+ ### Property keys
59
+
60
+ `properties` is a `NotionPagePropertyInputMap`. Two niceties versus the raw API:
61
+
62
+ - **Keys** can be either raw Notion property IDs _or_ the data-source property keys you declared in the manifest. The SDK resolves keys → IDs before forwarding the request.
63
+ - **Values** (`NotionPagePropertyInputValue`) may omit `id` — the SDK fills in the final raw ID for you.
64
+
65
+ So if your manifest declares `title` and `dueDate`, you can write them by name (as in the example above) instead of looking up the raw IDs.
66
+
67
+ ### Where the new page lands
68
+
69
+ `position` is a `NotionCreatePagePosition` and controls placement inside the parent:
70
+
71
+ - `{ type: "start" }` / `{ type: "end" }` — prepend or append (default).
72
+ - `{ type: "before", blockId }` / `{ type: "after", blockId }` — insert as a sibling of the given block (which can be nested anywhere under the parent).
73
+
74
+ ## Reading pages
75
+
76
+ `pages.get(pageId)` fetches a single page by ID:
77
+
78
+ ```ts
79
+ const result = await pages.get(pageId);
80
+ if (result.status === "error") return;
81
+
82
+ const page = result.page; // NotionPage
83
+ console.log(page.properties);
84
+ ```
85
+
86
+ `page` mirrors Notion's public API shape, with `id`, `parent`, `properties`, optional `icon` / `cover`, etc. — see `NotionPage` in the types list below.
87
+
88
+ ## Updating pages
89
+
90
+ `pages.update` writes back to a page. The optional fields (`properties`, `icon`, `cover`, `archived`) are independent — supply only what you want to change:
91
+
92
+ ```ts
93
+ const result = await pages.update({
94
+ pageId,
95
+ properties: {
96
+ "%5C%3FX%3D": {
97
+ id: "%5C%3FX%3D",
98
+ type: "checkbox",
99
+ checkbox: true,
100
+ },
101
+ },
102
+ icon: { type: "emoji", emoji: "✅" },
103
+ });
104
+ ```
105
+
106
+ The `properties` map (a `NotionPagePropertyWriteMap`) is keyed by **raw Notion property ID**, and each value must repeat that ID as its own `id` field — semantic data-source keys aren't accepted here. To update a row by configured custom-block key without writing out the raw IDs, use the `update` helper on the row returned from `useDataSource` instead; the SDK handles the key → ID resolution for you.
107
+
108
+ If you call `pages.update` with no fields to change, it short-circuits and resolves with `{ status: "error", error: "updatePage requires at least one of: properties, icon, cover, archived." }` — no request is sent.
109
+
110
+ ## Deleting (archiving) pages
111
+
112
+ `pages.delete(pageId)` is a thin wrapper around `pages.update({ pageId, archived: true })`. Notion treats archive and trash the same way for pages:
113
+
114
+ ```ts
115
+ await pages.delete(pageId);
116
+ ```
117
+
118
+ To restore a page, call `pages.update({ pageId, archived: false })`.
119
+
120
+ ## Icons, covers, and file uploads
121
+
122
+ File-upload references aren't enabled for custom blocks yet. For icons, covers, and file properties, use one of:
123
+
124
+ - An emoji icon — `{ type: "emoji", emoji: "🟢" }`
125
+ - An external URL — `{ type: "external", external: { url: "https://example.com/cover.png" } }`
126
+ - An existing hosted file URL returned by Notion — `{ type: "file", file: { url: existingUrl } }`
127
+
128
+ Do **not** send `{ type: "file_upload", file_upload: { id } }`; the host will reject it.
129
+
130
+ ## Types
131
+
132
+ - `NotionPage` — the page record returned by every successful call.
133
+ - `NotionPageId` — branded string ID for a page.
134
+ - `NotionPageIcon` / `NotionPageCover` — icon and cover variants Notion supports.
135
+ - `NotionPageParent` — the page's parent reference as returned by the API.
136
+ - `NotionPagePropertyValue` — a single property value as returned on a `NotionPage`.
137
+ - `NotionPagePropertyInputValue` — a single property value as accepted by `pages.create` / `pages.update` (may omit `id` for `create`).
138
+ - `NotionPagePropertyInputMap` — input map for `pages.create` (keys can be raw property IDs or semantic keys).
139
+ - `NotionPagePropertyWriteMap` — input map for `pages.update` (raw property IDs only).
140
+ - `NotionCreatePagePosition` — `start` / `end` / `before` / `after` insertion variants.
141
+ - `CreatePageInput` / `CreatePageParent` / `CreatePageResult`.
142
+ - `GetPageResult`.
143
+ - `UpdatePageInput` / `UpdatePageResult`.
package/docs/users.md ADDED
@@ -0,0 +1,61 @@
1
+ # `users` API
2
+
3
+ Helpers for reading workspace users from inside a custom block. The SDK forwards each call to Notion on your behalf.
4
+
5
+ ```ts
6
+ import { users, type NotionUserId } from "ncblock"
7
+ ```
8
+
9
+ Every helper returns a discriminated result instead of throwing:
10
+
11
+ ```ts
12
+ const result = await users.get(userId)
13
+ if (result.status === "success") {
14
+ // result.user is a NotionUser
15
+ } else {
16
+ // result.error is a human-readable string
17
+ }
18
+ ```
19
+
20
+ Always check `result.status` before reading `result.user` / `result.list`.
21
+
22
+ ## Listing users
23
+
24
+ `users.list(input?)` returns workspace users visible to the current custom block, mirroring Notion's [`GET /v1/users`](https://developers.notion.com/reference/get-users) shape.
25
+
26
+ ```ts
27
+ const result = await users.list({ pageSize: 50 })
28
+ if (result.status === "error") return
29
+
30
+ for (const user of result.list.results) {
31
+ console.log(user.id, user.name, user.person.email)
32
+ }
33
+
34
+ if (result.list.has_more && result.list.next_cursor) {
35
+ const next = await users.list({ startCursor: result.list.next_cursor })
36
+ // ...
37
+ }
38
+ ```
39
+
40
+ `ListUsersInput` accepts `pageSize` and `startCursor`; both are optional. `ListUsersResult` resolves to either `{ status: "success", list }` or `{ status: "error", error }`, where `list` is a `NotionUserList` (with `results`, `next_cursor`, `has_more`).
41
+
42
+ ## Reading a single user
43
+
44
+ `users.get(userId)` fetches one `NotionUser` by `NotionUserId`:
45
+
46
+ ```ts
47
+ const result = await users.get(userId)
48
+ if (result.status === "success") {
49
+ console.log(result.user.name)
50
+ }
51
+ ```
52
+
53
+ `GetUserResult` follows the same success/error shape and returns `{ status: "success", user }` when the host resolves the user.
54
+
55
+ ## Types
56
+
57
+ - `NotionUser` — the user record returned by `users.get` and inside `NotionUserList.results`.
58
+ - `NotionUserId` — branded string ID for a user.
59
+ - `NotionUserList` — paginated list shape returned by `users.list`.
60
+ - `ListUsersInput` / `ListUsersResult`.
61
+ - `GetUserResult`.
package/package.json CHANGED
@@ -1,30 +1,33 @@
1
1
  {
2
2
  "name": "ncblock",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "import": "./dist/./index.js",
11
- "types": "./dist/./index.d.ts"
10
+ "import": "././dist/index.js",
11
+ "types": "././dist/index.d.ts"
12
12
  },
13
13
  "./host": {
14
- "import": "./dist/./host.js",
15
- "types": "./dist/./host.d.ts"
14
+ "import": "././dist/host.js",
15
+ "types": "././dist/host.d.ts"
16
16
  },
17
17
  "./vite": {
18
- "import": "./dist/./vite-plugin/index.js",
19
- "types": "./dist/./vite-plugin/index.d.d.ts"
18
+ "import": "./vite-plugin/index.js",
19
+ "types": "./vite-plugin/index.d.ts"
20
20
  }
21
21
  },
22
22
  "scripts": {
23
23
  "test": "vitest run --environment jsdom"
24
24
  },
25
25
  "files": [
26
- "dist",
27
- "README.md"
26
+ "src",
27
+ "HOST.md",
28
+ "docs",
29
+ "vite-plugin",
30
+ "dist"
28
31
  ],
29
32
  "peerDependencies": {
30
33
  "react": "^19.2.5"