create-ncblock 0.0.21 → 0.0.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ncblock",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "Create a Notion custom view block project.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,7 @@ Hooks at a glance:
22
22
  - `useCustomBlockContext()` — `{ customBlockId, parent, page }`.
23
23
  - `useTheme()` — `"light" | "dark"`.
24
24
  - `useDataSource(key, initialLimit?)` — `{ items, isLoading, hasMore, fetchMore, error }`.
25
- - `useDataSourceDefinitions()` — resolved data-source definitions (for debug/schema-driven UIs).
25
+ - `useManifest()` — the declared manifest: data-source keys plus their property declarations.
26
26
  - `pages.create(input)` — creates a page; pass `parent: { type: "data_source_key", key }` to target the block's wired data source.
27
27
 
28
28
  `<NotionCustomBlock>` runs `useCustomBlockAutoResize` for you by default — no extra wiring needed. Pass `autoResize={false}` for full-bleed views. Full signatures live in `node_modules/ncblock/docs/*.md` and the `.d.ts` files.
package/sdk-version.json CHANGED
@@ -1 +1 @@
1
- {"version":"0.0.19"}
1
+ {"version":"0.0.21"}
@@ -32,5 +32,5 @@ The debug template includes a message log that intercepts and displays all incom
32
32
 
33
33
  - **`useCustomBlockContext()`** -- returns `{ customBlockId, parent, page }` describing the block's location in the document tree
34
34
  - **`useTheme()`** -- returns the host's current theme (`"light"` or `"dark"`)
35
- - **`useDataSourceDefinitions()`** -- returns resolved data-source definitions
35
+ - **`useManifest()`** -- returns the declared manifest (data-source keys + property declarations)
36
36
  - **`useDataSource(key)`** -- returns `{ items, collectionSchema, propertySchemasById, propertySchemasByKey, isLoading, hasMore, fetchMore, error }`. Each `item` has `{ id, propertiesById, propertiesByKey }`. The four built-ins (`created_time`, `last_edited_time`, `created_by`, `last_edited_by`) appear in `propertiesById` / `propertySchemasById`, never in the `*ByKey` views.
@@ -2,14 +2,13 @@ import {
2
2
  type NotionCreatePagePosition,
3
3
  NotionCustomBlock,
4
4
  type NotionCustomBlockContext,
5
- type NotionDataSource,
6
5
  type NotionDataSourceId,
7
6
  type NotionPage,
8
7
  type NotionPageId,
9
8
  pages,
10
9
  useCurrentUser,
11
10
  useCustomBlockContext,
12
- useDataSourceDefinitions,
11
+ useManifest,
13
12
  useTheme,
14
13
  } from "ncblock"
15
14
  import React, {
@@ -33,6 +32,11 @@ type LogEntry = {
33
32
  }
34
33
 
35
34
  type ThemeOverride = "host" | "light" | "dark"
35
+ type NotionInitFixture =
36
+ | "normal"
37
+ | "no-ready"
38
+ | "invalid-ready"
39
+ | "invalid-manifest"
36
40
  type TrackedPage = {
37
41
  page: NotionPage
38
42
  draftTitle: string
@@ -45,12 +49,14 @@ const labelClass =
45
49
  "text-[10px] font-semibold uppercase tracking-[0.06em] text-(--muted)"
46
50
  const metaTextClass =
47
51
  "font-mono text-[11px] font-normal normal-case tracking-normal text-(--muted)"
52
+ const HOST_NO_READY_ERROR_DELAY_SECONDS = 5
48
53
 
49
54
  function App() {
50
55
  const ctx = useCustomBlockContext()
51
56
  const currentUser = useCurrentUser()
52
57
  const hostThemeValue = useTheme()
53
- const dataSources = useDataSourceDefinitions()
58
+ const manifest = useManifest()
59
+ const dataSourceKeys = Object.keys(manifest?.dataSources ?? {})
54
60
  const [isPaused, setIsPaused] = useState(false)
55
61
  const { log, initData, send, clearLog } = useMessageLog(isPaused)
56
62
  const [themeOverride, setThemeOverride] = useState<ThemeOverride>("host")
@@ -81,7 +87,7 @@ function App() {
81
87
  parent: ctx?.parent,
82
88
  page: ctx?.page,
83
89
  currentUser,
84
- dataSources,
90
+ manifest,
85
91
  }
86
92
 
87
93
  const metadataRows: Array<[string, string]> = [
@@ -174,7 +180,7 @@ function App() {
174
180
  </CollapsibleCard>
175
181
 
176
182
  {/* Create page */}
177
- <CreatePageCard context={ctx} dataSources={dataSources} />
183
+ <CreatePageCard context={ctx} dataSourceKeys={dataSourceKeys} />
178
184
 
179
185
  {/* Send message */}
180
186
  <SendMessageCard onSend={send} />
@@ -198,6 +204,162 @@ function App() {
198
204
  )
199
205
  }
200
206
 
207
+ function DevOnlyNoReadyFixture() {
208
+ return (
209
+ <div className="min-h-screen bg-(--app-bg) p-8 text-(--foreground)">
210
+ <div className={cardClass}>
211
+ <div className={labelClass}>Local init fixture</div>
212
+ <h1 className="mt-2 text-[24px] font-semibold">No ready message</h1>
213
+ <p className="mt-2 text-sm leading-6 text-(--muted)">
214
+ This dev-only fixture intentionally skips the SDK provider, so the
215
+ sandbox loads but never sends the initial <code>ready</code> message.
216
+ Use it only for local manual testing of Notion host init error UI.
217
+ </p>
218
+ <DevOnlyHostErrorExpectation>
219
+ This page is the pre-error fixture, not the Notion error state. In the
220
+ Notion host, wait about {HOST_NO_READY_ERROR_DELAY_SECONDS} seconds
221
+ after the iframe loads; the host-owned{" "}
222
+ <span className="font-mono">Custom block didn't connect</span> error
223
+ should then cover this page.
224
+ </DevOnlyHostErrorExpectation>
225
+ </div>
226
+ </div>
227
+ )
228
+ }
229
+
230
+ function DevOnlyInvalidReadyFixture() {
231
+ useEffect(() => {
232
+ if (window.parent === window) {
233
+ return
234
+ }
235
+
236
+ // Dev-only fixture for local manual testing of Notion host init error UI.
237
+ // This intentionally bypasses the SDK so the host receives a malformed
238
+ // ready message that is missing the manifest and protocol fields.
239
+ window.parent.postMessage({ type: "ready" }, "*")
240
+ }, [])
241
+
242
+ return (
243
+ <div className="min-h-screen bg-(--app-bg) p-8 text-(--foreground)">
244
+ <div className={cardClass}>
245
+ <div className={labelClass}>Local init fixture</div>
246
+ <h1 className="mt-2 text-[24px] font-semibold">
247
+ Invalid ready message
248
+ </h1>
249
+ <p className="mt-2 text-sm leading-6 text-(--muted)">
250
+ This dev-only fixture posts malformed{" "}
251
+ <code>{'{ type: "ready" }'}</code> to the parent window and does not
252
+ mount the SDK provider. Use it only for local manual testing of Notion
253
+ host init error UI.
254
+ </p>
255
+ <DevOnlyHostErrorExpectation>
256
+ This page is the pre-error fixture, not the Notion error state. In the
257
+ Notion host, the host-owned invalid setup message error should cover
258
+ this page as soon as Notion processes the malformed <code>ready</code>{" "}
259
+ message.
260
+ </DevOnlyHostErrorExpectation>
261
+ </div>
262
+ </div>
263
+ )
264
+ }
265
+
266
+ function DevOnlyInvalidManifestFixture() {
267
+ useEffect(() => {
268
+ if (window.parent === window) {
269
+ return
270
+ }
271
+
272
+ // Dev-only fixture for local manual testing of Notion host init error UI.
273
+ // This intentionally bypasses the SDK so the host receives a ready message
274
+ // with a manifest schema the bridge validator must reject.
275
+ window.parent.postMessage(
276
+ {
277
+ type: "ready",
278
+ bridgeProtocolVersion: 1,
279
+ manifest: {
280
+ version: 1,
281
+ dataSources: {
282
+ default: {
283
+ name: "Invalid fixture data source",
284
+ properties: {
285
+ unsupportedProperty: {
286
+ name: "Unsupported property",
287
+ type: "unsupported_manual_fixture_property_type",
288
+ },
289
+ },
290
+ },
291
+ },
292
+ },
293
+ },
294
+ "*",
295
+ )
296
+ }, [])
297
+
298
+ return (
299
+ <div className="min-h-screen bg-(--app-bg) p-8 text-(--foreground)">
300
+ <div className={cardClass}>
301
+ <div className={labelClass}>Local init fixture</div>
302
+ <h1 className="mt-2 text-[24px] font-semibold">Invalid manifest</h1>
303
+ <p className="mt-2 text-sm leading-6 text-(--muted)">
304
+ This dev-only fixture posts a <code>ready</code> message with an
305
+ unsupported manifest property type and does not mount the SDK
306
+ provider. Use it only for local manual testing of Notion host init
307
+ error UI.
308
+ </p>
309
+ <DevOnlyHostErrorExpectation>
310
+ This page is the pre-error fixture, not the Notion error state. In the
311
+ Notion host, the host-owned invalid manifest error should cover this
312
+ page as soon as Notion validates the <code>ready</code> message.
313
+ </DevOnlyHostErrorExpectation>
314
+ </div>
315
+ </div>
316
+ )
317
+ }
318
+
319
+ function DevOnlyHostErrorExpectation(props: { children: React.ReactNode }) {
320
+ return (
321
+ <div className="mt-3 rounded-md border border-(--border) bg-(--app-bg) px-3 py-2 text-sm leading-6 text-(--foreground)">
322
+ <span className="font-semibold">Expected host behavior: </span>
323
+ {props.children}
324
+ </div>
325
+ )
326
+ }
327
+
328
+ function DebugInitErrorFallback(error: Error) {
329
+ return (
330
+ <div className="min-h-screen bg-(--app-bg) p-8 text-(--foreground)">
331
+ <div className={cardClass} role="alert">
332
+ <div className={labelClass}>Custom block init</div>
333
+ <h1 className="mt-2 text-[24px] font-semibold">
334
+ Couldn&apos;t connect to Notion
335
+ </h1>
336
+ <p className="mt-2 text-sm leading-6 text-(--muted)">
337
+ The debug template waited for the host handshake, but it did not
338
+ complete. This fallback is only for local manual testing; host-owned
339
+ error UI should normally cover known Notion init failures first.
340
+ </p>
341
+ <pre className="mt-3 overflow-auto rounded-md border border-(--border) bg-(--app-bg) p-3 font-mono text-[11px] text-(--muted)">
342
+ {error.message}
343
+ </pre>
344
+ </div>
345
+ </div>
346
+ )
347
+ }
348
+
349
+ function getNotionInitFixture(): NotionInitFixture {
350
+ const fixture = new URLSearchParams(window.location.search).get(
351
+ "notionInitFixture",
352
+ )
353
+ if (
354
+ fixture === "no-ready" ||
355
+ fixture === "invalid-ready" ||
356
+ fixture === "invalid-manifest"
357
+ ) {
358
+ return fixture
359
+ }
360
+ return "normal"
361
+ }
362
+
201
363
  // --- Hooks ---
202
364
 
203
365
  function useMessageLog(isPaused: boolean) {
@@ -1448,7 +1610,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
1448
1610
 
1449
1611
  type CreatePageCardProps = {
1450
1612
  context: NotionCustomBlockContext | undefined
1451
- dataSources: NotionDataSource[]
1613
+ dataSourceKeys: string[]
1452
1614
  }
1453
1615
 
1454
1616
  type CreatePagePreset = {
@@ -1576,7 +1738,7 @@ function pageIconFromInput(raw: string): NotionPage["icon"] | undefined {
1576
1738
  * SDK resolve the key to the configured data source ID locally before sending the request to the
1577
1739
  * Notion host.
1578
1740
  */
1579
- function CreatePageCard({ context, dataSources }: CreatePageCardProps) {
1741
+ function CreatePageCard({ context, dataSourceKeys }: CreatePageCardProps) {
1580
1742
  const [status, setStatus] = useState<string | null>(null)
1581
1743
  const [busy, setBusy] = useState(false)
1582
1744
  const [dataSourceId, setDataSourceId] = useState("")
@@ -1813,27 +1975,23 @@ function CreatePageCard({ context, dataSources }: CreatePageCardProps) {
1813
1975
  Create in data source
1814
1976
  </button>
1815
1977
  </div>
1816
- {dataSources.length > 0 && (
1978
+ {dataSourceKeys.length > 0 && (
1817
1979
  <div className="mb-2 flex flex-wrap items-center gap-1.5">
1818
1980
  <span className="font-mono text-[11px] text-(--muted)">
1819
1981
  by data source key:
1820
1982
  </span>
1821
- {dataSources.map(source => (
1983
+ {dataSourceKeys.map(key => (
1822
1984
  <button
1823
- key={source.key}
1985
+ key={key}
1824
1986
  type="button"
1825
1987
  disabled={disabled}
1826
- title={
1827
- source.collectionPointer !== undefined
1828
- ? `Create a page in data source ${source.collectionPointer.id} via key "${source.key}"`
1829
- : `Key "${source.key}" has no collection pointer yet; request will resolve with an error`
1830
- }
1988
+ title={`Create a page via data source key "${key}" (resolves to an error if the key is not mapped to a database yet)`}
1831
1989
  className="rounded-md border border-(--border) bg-transparent px-2 py-1 font-mono text-[11px] text-(--muted) transition-colors hover:border-(--foreground)/20 hover:bg-(--hover-bg) hover:text-(--foreground) disabled:cursor-not-allowed disabled:opacity-50"
1832
1990
  onClick={() => {
1833
- void handleCreateForKey(source.key)
1991
+ void handleCreateForKey(key)
1834
1992
  }}
1835
1993
  >
1836
- {source.key}
1994
+ {key}
1837
1995
  </button>
1838
1996
  ))}
1839
1997
  </div>
@@ -1960,10 +2118,19 @@ function CreatePageCard({ context, dataSources }: CreatePageCardProps) {
1960
2118
 
1961
2119
  // --- Mount ---
1962
2120
 
2121
+ const notionInitFixture = getNotionInitFixture()
2122
+
1963
2123
  ReactDOM.createRoot(document.getElementById("root")!).render(
1964
2124
  <ErrorBoundary>
1965
- <NotionCustomBlock>
1966
- <App />
1967
- </NotionCustomBlock>
2125
+ {notionInitFixture === "no-ready" && <DevOnlyNoReadyFixture />}
2126
+ {notionInitFixture === "invalid-ready" && <DevOnlyInvalidReadyFixture />}
2127
+ {notionInitFixture === "invalid-manifest" && (
2128
+ <DevOnlyInvalidManifestFixture />
2129
+ )}
2130
+ {notionInitFixture === "normal" && (
2131
+ <NotionCustomBlock errorFallback={DebugInitErrorFallback}>
2132
+ <App />
2133
+ </NotionCustomBlock>
2134
+ )}
1968
2135
  </ErrorBoundary>,
1969
2136
  )
@@ -51,5 +51,5 @@ If the mapped collection is missing any of those fields, the template shows a se
51
51
 
52
52
  - **`useCustomBlockContext()`** -- returns `{ customBlockId, parent, page }` describing the block's location in the document tree
53
53
  - **`useTheme()`** -- returns the host's current theme (`"light"` or `"dark"`)
54
- - **`useDataSourceDefinitions()`** -- returns resolved data-source definitions
54
+ - **`useManifest()`** -- returns the declared manifest (data-source keys + property declarations)
55
55
  - **`useDataSource(key)`** -- returns `{ items, collectionSchema, propertySchemasById, propertySchemasByKey, isLoading, hasMore, fetchMore, error }`. Each `item` has `{ id, propertiesById, propertiesByKey }`. The four built-ins (`created_time`, `last_edited_time`, `created_by`, `last_edited_by`) appear in `propertiesById` / `propertySchemasById`, never in the `*ByKey` views.
@@ -3,7 +3,7 @@ import {
3
3
  type NotionDataSourcePage,
4
4
  type NotionPageId,
5
5
  useDataSource,
6
- useDataSourceDefinitions,
6
+ useManifest,
7
7
  useTheme,
8
8
  } from "ncblock"
9
9
  import React from "react"
@@ -445,8 +445,9 @@ function SetupHint(props: {
445
445
 
446
446
  function App() {
447
447
  const theme = useTheme()
448
- const dataSources = useDataSourceDefinitions()
449
- const activeDataSourceKey = dataSources[0]?.key ?? "default"
448
+ const manifest = useManifest()
449
+ const activeDataSourceKey =
450
+ Object.keys(manifest?.dataSources ?? {})[0] ?? "default"
450
451
  const query = useDataSource(activeDataSourceKey)
451
452
  const colorTheme = theme === "dark" ? "dark" : "light"
452
453
  const mappedAnalysis = analyzeRadarSchema(query.items)
@@ -39,5 +39,5 @@ related page is loaded or `-> uuid` when it is not.
39
39
 
40
40
  - **`useCustomBlockContext()`** -- returns `{ customBlockId, parent, page }` describing the block's location in the document tree
41
41
  - **`useTheme()`** -- returns the host's current theme (`"light"` or `"dark"`)
42
- - **`useDataSourceDefinitions()`** -- returns resolved data-source definitions
42
+ - **`useManifest()`** -- returns the declared manifest (data-source keys + property declarations)
43
43
  - **`useDataSource(key)`** -- returns `{ items, collectionSchema, propertySchemasById, propertySchemasByKey, isLoading, hasMore, fetchMore, error }`. Each `item` has `{ id, propertiesById, propertiesByKey }`. The four built-ins (`created_time`, `last_edited_time`, `created_by`, `last_edited_by`) appear in `propertiesById` / `propertySchemasById`, never in the `*ByKey` views.
@@ -8,13 +8,12 @@ import {
8
8
  } from "@tanstack/react-table"
9
9
  import {
10
10
  NotionCustomBlock,
11
- type NotionDataSource,
12
11
  type NotionPagePropertyInputMap,
13
12
  type NotionPagePropertyInputValue,
14
13
  type NotionPropertySchema,
15
14
  pages,
16
15
  useDataSource,
17
- useDataSourceDefinitions,
16
+ useManifest,
18
17
  useTheme,
19
18
  } from "ncblock"
20
19
  import React from "react"
@@ -1004,7 +1003,7 @@ function DataWorkspace(props: {
1004
1003
  onColumnOrderChange: (next: string[]) => void
1005
1004
  onHiddenColumnsChange: (next: Set<string>) => void
1006
1005
  activeDataSourceKey: string
1007
- dataSources: NotionDataSource[]
1006
+ dataSourceKeys: string[]
1008
1007
  onSelectDataSource: (key: string) => void
1009
1008
  hasMore: boolean
1010
1009
  isLoading: boolean
@@ -1023,7 +1022,7 @@ function DataWorkspace(props: {
1023
1022
  onColumnOrderChange,
1024
1023
  onHiddenColumnsChange,
1025
1024
  activeDataSourceKey,
1026
- dataSources,
1025
+ dataSourceKeys,
1027
1026
  onSelectDataSource,
1028
1027
  hasMore,
1029
1028
  isLoading,
@@ -1417,7 +1416,7 @@ function DataWorkspace(props: {
1417
1416
  <span className="eyebrow ml-1.5">rows</span>
1418
1417
  </div>
1419
1418
 
1420
- {dataSources.length > 1 ? (
1419
+ {dataSourceKeys.length > 1 ? (
1421
1420
  <label className="block">
1422
1421
  <span className="sr-only">Choose data source</span>
1423
1422
  <select
@@ -1425,9 +1424,9 @@ function DataWorkspace(props: {
1425
1424
  onChange={event => onSelectDataSource(event.target.value)}
1426
1425
  className="field-control min-h-9 w-full rounded-md border border-(--line) bg-(--surface-bg) px-2 text-sm text-(--foreground) outline-none hover:border-(--line-strong)"
1427
1426
  >
1428
- {dataSources.map(dataSource => (
1429
- <option key={dataSource.key} value={dataSource.key}>
1430
- {dataSource.key}
1427
+ {dataSourceKeys.map(key => (
1428
+ <option key={key} value={key}>
1429
+ {key}
1431
1430
  </option>
1432
1431
  ))}
1433
1432
  </select>
@@ -1682,30 +1681,32 @@ function DataWorkspace(props: {
1682
1681
 
1683
1682
  function App() {
1684
1683
  const theme = useTheme()
1685
- const dataSources = useDataSourceDefinitions()
1684
+ const manifest = useManifest()
1685
+ const dataSourceKeys = React.useMemo(
1686
+ () => Object.keys(manifest?.dataSources ?? {}),
1687
+ [manifest],
1688
+ )
1686
1689
  const [activeDataSourceKey, setActiveDataSourceKey] = React.useState(
1687
- dataSources[0]?.key ?? "default",
1690
+ dataSourceKeys[0] ?? "default",
1688
1691
  )
1689
1692
 
1690
1693
  React.useEffect(() => {
1691
- if (dataSources.length === 0) {
1694
+ if (dataSourceKeys.length === 0) {
1692
1695
  setActiveDataSourceKey("default")
1693
1696
  return
1694
1697
  }
1695
1698
 
1696
- if (
1697
- !dataSources.some(dataSource => dataSource.key === activeDataSourceKey)
1698
- ) {
1699
- setActiveDataSourceKey(dataSources[0].key)
1699
+ if (!dataSourceKeys.includes(activeDataSourceKey)) {
1700
+ setActiveDataSourceKey(dataSourceKeys[0])
1700
1701
  }
1701
- }, [activeDataSourceKey, dataSources])
1702
+ }, [activeDataSourceKey, dataSourceKeys])
1702
1703
 
1703
- const queryKey = dataSources.length === 0 ? "default" : activeDataSourceKey
1704
+ const queryKey = dataSourceKeys.length === 0 ? "default" : activeDataSourceKey
1704
1705
  const query = useDataSource(queryKey)
1705
1706
  const colorTheme = theme === "dark" ? "dark" : "light"
1706
1707
 
1707
1708
  const mappedItems = query.items as TableRow[]
1708
- const isUsingFallbackData = dataSources.length === 0
1709
+ const isUsingFallbackData = dataSourceKeys.length === 0
1709
1710
  const isCollectionEmpty =
1710
1711
  !isUsingFallbackData && !query.isLoading && mappedItems.length === 0
1711
1712
  const items = isUsingFallbackData ? SAMPLE_ITEMS : mappedItems
@@ -1793,7 +1794,7 @@ function App() {
1793
1794
  onColumnOrderChange={setColumnOrder}
1794
1795
  onHiddenColumnsChange={setHiddenColumns}
1795
1796
  activeDataSourceKey={queryKey}
1796
- dataSources={dataSources}
1797
+ dataSourceKeys={dataSourceKeys}
1797
1798
  onSelectDataSource={setActiveDataSourceKey}
1798
1799
  hasMore={query.hasMore}
1799
1800
  isLoading={query.isLoading}