grist-widget-sdk 0.2.0 → 0.2.2-rc.0

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/CHANGELOG.md CHANGED
@@ -1,730 +1,23 @@
1
- # Changelog
1
+ # grist-widget-sdk
2
2
 
3
- All notable changes to `grist-widget-sdk` are recorded here. In-flight work is
4
- tracked on [`/TASKS.md`](./TASKS.md) and [`/ROADMAP.md`](./ROADMAP.md); landed
5
- changes accumulate under **Unreleased** until a version is cut.
3
+ ## 0.2.1
6
4
 
7
- This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
8
- and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+ ### Patch Changes
9
6
 
10
- ## Unreleased — 0.2.0 (API surface alignment)
7
+ - 3c0315f: Relicense to MIT and ship the `LICENSE` in the published tarball. No runtime or
8
+ API changes — `dist` is identical to 0.2.0; this release corrects the package
9
+ license metadata (0.2.0 shipped as ISC) and lands the release tooling (OIDC
10
+ Trusted Publishing + external-install smoke test).
11
11
 
12
- ### Changed
12
+ This file is generated by [Changesets](https://github.com/changesets/changesets)
13
+ and is the canonical changelog for the published `grist-widget-sdk` npm
14
+ package, starting at `0.2.1`. Every entry corresponds to a version actually
15
+ published to npm.
13
16
 
14
- - **Unified, release-driven widget deploys.** The seven copy-pasted
15
- `deploy-<widget>.yml` workflows are replaced by one
16
- `.github/workflows/deploy-widgets.yml` + `scripts/deploy/publish.mjs`
17
- (node builtins only). Two channels: push to `main` publishes **immutable**
18
- `/<repo>/<widget>/v<version>/` + a mutable `/<widget>/latest/` alias, and
19
- regenerates a root `manifest.json` (Grist widget-repository format, usable via
20
- `GRIST_WIDGET_LIST_URL`); a widget is (re)built only when its `package.json`
21
- `version` has no `v<version>/` dir yet, so bumping the version *is* the release
22
- and a `packages/core`-only push no longer moves shipped widget URLs. Concurrent
23
- runs are serialized and the publisher rebases-and-retries its push; one failing
24
- widget build no longer blocks the others. Widgets carry a `grist` metadata
25
- block (`name`, `widgetId`, `accessLevel`, …) consumed by the manifest;
26
- `v0-minimal-demo` is intentionally unlisted. The former single mutable
27
- `/<widget>/` URL is kept but deprecated in favor of pinned `v<version>/` /
28
- `latest/`.
17
+ For the fuller project history — widgets, docs, tooling, process — see the
18
+ monorepo root [`/CHANGELOG.md`](https://github.com/ArthurBlanchon/grist-widget-sdk/blob/main/CHANGELOG.md).
29
19
 
30
- ### Added
20
+ ## 0.2.0
31
21
 
32
- - **Dev deploy channel + in-Grist self-reload.** Pushing a `dev/<widget>` branch
33
- publishes to `/<repo>/<widget>/dev/` with a `version.json` and a **dev-only**
34
- self-reload snippet that polls for new builds and hot-swaps the widget inside
35
- an open Grist document (cache-busting `__dev=<sha>` navigation that preserves
36
- the host's query params). Prod builds are unchanged. Enables a push → CI →
37
- review-in-real-Grist loop without a tunnel.
38
-
39
- ### Fixed
40
-
41
- - **Gantt widget dates** — Grist Date cells (UTC midnight) are normalized to local
42
- calendar days before timeline layout, so bars align with table dates in all timezones.
43
- - **Gantt weekly timeline positioning** — event bars and the Today marker now walk
44
- the same week columns as the header (weeks that straddle two months are no longer
45
- skipped in offset math), fixing ~3-month placement drift on the weekly scale.
46
- - **Gantt weekly headers** — columns show ISO week numbers (Monday-based, e.g. W26)
47
- with the week start date below.
48
- - **Upload CORS error message** — includes the widget `location.origin` and notes that
49
- changing deploy URL (e.g. ngrok → GitHub Pages) requires updating server CORS.
50
-
51
- ### Added
52
-
53
- - **`uploadGristAttachment` / `w.uploadAttachment`** — canonical widget upload
54
- (`POST /attachments`, `X-Requested-With`, `?auth=`) returning `{ ids, firstId }`.
55
- - **`parseGristAttachmentUploadResponseIds`**, **`gristAttachmentCellValues`**,
56
- **`mergeGristAttachmentCellValue`** — full id list parsing and Attachments cell
57
- encode/merge helpers aligned with Grist’s `["L", …]` wire format.
58
-
59
- ### Fixed
60
-
61
- - **`fetchWithAuth` CORS on attachment upload** — non-GET REST calls from custom
62
- widgets now attach the access token as `?auth=` (same as downloads) instead of
63
- `Authorization: Bearer`, which many Grist hosts block in CORS preflight.
64
- - **`parseGristAttachmentUploadResponse` / `gristAttachmentCellValue`** — helpers
65
- for `POST /attachments` (response is a JSON array of ids, e.g. `[42]`) and
66
- writing a single id into an Attachments column as `["L", id]`.
67
- - **`mapBack` erased unrelated columns on partial updates** —
68
- `grist-plugin-api.js`'s `mapColumnNamesBack` applies transformations for
69
- *all* mapped columns, injecting `undefined` for fields absent from the
70
- patch. Those `undefined` values JSON-serialise to `null` over RPC,
71
- causing Grist to erase the corresponding cells. `mapBack` now strips
72
- all `undefined` entries from the result so only fields explicitly
73
- included in the patch are sent in the update.
74
- - **Access-insufficient alerts for all SDK hooks** — `useGristSchema`,
75
- `useGristRowsFromTable`, and `useGristAttachmentsRest` now surface
76
- access-insufficient errors through the provider's `readError` so the SDK alert
77
- system displays a smooth "Access level" alert instead of failing silently.
78
- `useGristSchema` and `useGristRowsFromTable` delegate to the provider's guarded
79
- read methods when inside a `GristWidgetProvider`; `useGristAttachmentsRest` uses
80
- its own `guardedRpc` wrapper whose `readError` is merged into `UseGristResult`.
81
- - **Heartbeat false-positive on semantic errors** — `applyActions` failures (e.g.
82
- "No such column") no longer briefly flash the connection-degraded indicator.
83
- RPC failures are no longer coalesced into the heartbeat; the regular probe
84
- detects real transport issues on its own schedule.
85
- - **`fetchTable` / `fetchTableRows` / `fetchRow` / `listColumns` / `buildReplicaDocumentFromDocApi`
86
- access guard** — these methods now check the granted access level before making
87
- an RPC call. When the widget only has `"read table"` access, the SDK throws
88
- immediately with a descriptive message and sets `readError`, preventing a
89
- looping RPC failure cycle. The `access-insufficient` SDK alert is emitted
90
- automatically so the `<GristSdkAlerts>` / `useGristSdkAlertDescriptors` shell
91
- shows actionable instructions.
92
- - **`@access` annotations** — corrected `fetchTable`, `fetchTableRows`, `fetchRow`,
93
- `listColumns`, and `buildReplicaDocumentFromDocApi` from `@access "read table"`
94
- to `@access "full"` in `UseGristResult` JSDoc.
95
-
96
- ### Added
97
-
98
- - **`gristAddVisibleColumnAction(tableId, colId, colInfo)`** — new action builder
99
- that emits `["AddVisibleColumn", ...]`. Unlike `gristAddColumnAction`, it also
100
- adds the column to the current view section so it is immediately visible.
101
- - **`w.listColumns(tableId, options?)`** — new lightweight API to retrieve
102
- column metadata (id, label, type, formula, description) for a given table
103
- without fetching all row data. Noise columns (`id`, `manualSort`,
104
- `gristHelper_*`, logging formulas) are filtered out by default.
105
- - **`w.listTables(options?)` — system-table filtering** — `listTables()` now
106
- accepts `{ includeSystem?: boolean }` and filters system/hidden tables
107
- (`_grist*`, `GristHidden_*`) by default.
108
- - **`useGristWidgetOptionsFromContext<T>()`** — typed widget options hook
109
- designed for use inside `<GristWidgetProvider>`. Provides `options`,
110
- `loading`, `setOptions`, `patchOptions`, and `reset` with debounced writes
111
- and a `namespace` option. Unlike `useGristWidgetOptions()` (advanced), this
112
- hook does not call `grist.ready()` and is fully compatible with the provider.
113
- - **`UseGristResult` JSDoc** — documents that all function-typed fields are
114
- referentially stable (`useCallback`-wrapped) and safe in `useEffect` deps,
115
- while the container object itself is not.
116
- - **`UseGristResult` access-level annotations** — every field now carries an
117
- `@access` JSDoc tag (`"none"`, `"read table"`, or `"full"`) so editors and
118
- documentation show the minimum `requiredAccess` at a glance. Fields are
119
- grouped by access tier in the type definition and in the API reference.
120
- - **`suppressAlerts` on `UseGristOptions`** — widgets that intentionally
121
- operate without a link source can now declare
122
- `suppressAlerts: ["section-not-linked"]` in their `GRIST_OPTIONS`. The alert
123
- system (`useGristSdkAlertDescriptors`) reads it automatically from the widget
124
- slice — no extra wiring needed. A lower-level `suppressKinds` option on
125
- `GetGristSdkAlertDescriptorsOptions` is also available as an override.
126
- - **`source-not-wired` SDK alert** — when a widget declares
127
- `allowSelectBy: true` but no other section is linked to read from it, a
128
- distinct `source-not-wired` alert is emitted instead of `section-not-linked`.
129
- This clearly distinguishes "widget expects an incoming link" from "widget is
130
- a selector but nothing listens yet".
131
- - **`access-insufficient` SDK alert** — when a write or REST call fails because
132
- of insufficient access (`"Access not granted"`, etc.), the alert system now
133
- emits a dedicated `access-insufficient` alert with actionable copy instead
134
- of the generic `action-error`. Hosts render it as an error-severity callout.
135
- - **API reference grouped by access tier** — the `useGrist` API reference page
136
- organizes fields under `"none"`, `"read table"`, and `"full"` headings so
137
- developers can quickly see which features require which access level.
138
- - **create-email-draft widget: diffusion lists** — users can configure a
139
- "diffusion list" table via the widget's _Open configuration_ panel (select
140
- table, display-name column, and emails column). Typing `/` in the Bcc field
141
- opens a picker to insert all emails from a diffusion list at once. Config
142
- panel now uses `listColumns()` for lightweight column discovery.
143
-
144
- ### Changed
145
-
146
- - **Monorepo tooling** — upgrade to pnpm 11 (`packageManager` pin), root
147
- `engines` (Node 22+, pnpm 11+), and `pnpm-workspace.yaml` settings
148
- (`engineStrict`, `minimumReleaseAge` 7 days, `allowBuilds`). README
149
- documents `corepack enable`.
150
-
151
- ### Added
152
-
153
- - **`<GristBoundary>` shell UX** — blocking states (booting, unavailable, error, preparing)
154
- use centered layout via `GristBoundaryScreen` with neutral typography, visible card borders,
155
- and shell background (`#f8f8f8` fallback). Access-denied copy is short and points to Custom
156
- widget settings. Widget HTML templates include inline background styles to reduce the initial
157
- white flash before the bundle loads. Helpers `formatBoundaryUserMessage`, `GRIST_BOUNDARY_PREPARING_COPY`.
158
-
159
- - **Host access level enforcement** — `interaction.access_level` from `grist.onOptions` is
160
- applied to the handshake `authz` axis (`AUTHZ_REPORT`). When Grist grants less than
161
- `requiredAccess` (e.g. widget requests `read table` but the document is set to no access),
162
- `useGrist().status` becomes `error` and `<GristBoundary>` shows the error fallback instead
163
- of widget content. After **Try reconnecting** / `reload()`, the cached `onOptions`
164
- level is re-checked when the handshake goes online so insufficient access stays blocked
165
- even when Grist does not send a fresh `onOptions` event.
166
-
167
- - **`section-not-linked` SDK alert** — `getGristSdkAlertDescriptors` emits a warning when
168
- Grist reports `widgetInteraction.linking.asTarget === null` (including when a stale row
169
- is still shown). `onOptions` settings are normalized (`accessLevel` → `access_level`,
170
- `linking` parsed) before they reach `w.widgetInteraction`.
171
- (section not driven by a linked table/selector). Helpers `isWidgetSectionNotLinked`,
172
- `formatSectionNotLinkedAlertMessage`; type `GristWidgetLinkingInfo`. Older hosts without
173
- `linking` on `onOptions` are unchanged (no false positive).
174
-
175
- - **`grist-widget-sdk/advanced` build export** — `advanced` entry in `tsup` and `package.json` `exports` so documented advanced hooks resolve from npm.
176
- - **`useGrist().capabilities`** — projects handshake `GristCapabilities` (`canRender`, `canWriteRecords`, `missingMappings`, …) on the primary hook; type `GristCapabilities` exported from the main entry.
177
- - **Guide: [Raw plugin API vs SDK](/guide/raw-plugin-api-vs-sdk)** — comparison table and migration snippets vs calling `grist` directly.
178
- - **Docs home** — eight VitePress feature cards (four « One … » product links + four guide links); original hero tagline.
179
- - **Vite template DX** — ESLint `no-restricted-globals` for `grist`, `grist-types.example.ts`, `GristBoundary gate="canRender"` when `columns` are set (no bundled tests — see `/guide/testing`).
180
-
181
- - **Handshake-aware boundary + alert helpers** — `task-070`.
182
- `deriveBoundaryView`, `deriveBoundaryBootLabel`, extended
183
- `getGristSdkAlertDescriptors` (mapping-pending, mapping-unreported,
184
- link-stale, current-table-error; `title` / `severity` on descriptors),
185
- `useGristSdkAlertDescriptors`, and `<GristBoundary gate="canRender">` with
186
- phase-aware boot labels when the manager is mounted.
187
- - **`useGristHandshake()` / `useGristCapabilities()` hooks** — `task-062`.
188
- Exported from `grist-widget-sdk/advanced`. Returns the full
189
- `GristWidgetSnapshot` (lifecycle / link / authz / config / sync), derived
190
- `status`, error message, and pre-computed `GristCapabilities` (`canRead`,
191
- `canRender`, `canWriteRecords`, `canWriteSchema`, `canFetchTable`,
192
- `hasFreshSelection`, …). Includes `reload()` and `restart()` controls.
193
- Independent of the existing `useGrist*` hooks — no breaking change to the
194
- current API surface.
195
- - **`<GristHandshakeProvider>` + `useGristHandshakeContext()` /
196
- `useGristHandshakeContextOptional()`** — `task-064`. Opt-in React provider
197
- that mounts a single `GristHandshakeManager` per app tree and broadcasts
198
- its snapshot to all descendants. Coexists with the legacy
199
- `<GristWidgetProvider>` without interference (ready calls are deduped at
200
- the singleton level).
201
- - **Public snapshot types** — `GristWidgetSnapshot`, `GristCapabilities`,
202
- `GristLifecycle*`, `GristLink*`, `GristAuthz`, `GristConfig`, `GristSync`,
203
- `GristMapping*`, `GristStreamFreshness`, `GristCurrentTableState`,
204
- `GristGeneration`, `GristTerminationReason` re-exported from `/advanced`.
205
-
206
- ### Fixed
207
-
208
- - **`mapBack` injected spurious `id` field** — `grist-plugin-api.js`'s
209
- `mapColumnNamesBack` unconditionally copies `from.id → to.id` (a side-effect
210
- of sharing code with forward mapping). When the input patch has no `id` key,
211
- the result contained `id: undefined`, causing Grist to reject writes with
212
- "Invalid column 'id'". The SDK now strips the injected `id` key.
213
-
214
- - **Playground theme-demo stuck on `"light"`** — `useGristTheme` listens on
215
- `grist.on("message")` only (production `grist-plugin-api.js`). Emulator transports
216
- post the same `msg.theme` object shape (`appearance`, `name`, `colors`); removed
217
- emulator-only `themeInitialChange` / `themeChange`. Playground shell theme (**d**) is
218
- mirrored via `emulator.theme.set`.
219
-
220
- - **`useGrist()` cursor updates in production Grist** — merged widget state is
221
- rebuilt from live slice contexts (`useGristFromProvider`) instead of a
222
- memoized `GristContext` snapshot that could keep `w.record.id` on the first
223
- row after `onRecord` fired again. `recordEvent` also listens for host
224
- `message` events with a new numeric `rowId` and refetches via
225
- `fetchSelectedRecord` (same path as `grist-plugin-api.js` `onRecord`).
226
- - **Playground iframe: row stuck on first selection** — iframe transport now
227
- sends `dataChange: true` on `cursor-change` (same contract as inline
228
- `pushRecord`), so `grist-plugin-api.js` refetches the record when the
229
- inspector changes the cursor. Cursor-only messages left `w.record.id`
230
- frozen on the initial row.
231
- - **Selected row missing after handshake** — `useGristSelection` now binds
232
- `grist.onRecord` / `onRecords` on mount instead of waiting for `docApi`
233
- (`lifecycle.phase === "online"`). The host can push the initial cursor
234
- record during `grist.ready` before `docApi` exists; late binding left
235
- `w.record` / `w.mode` stuck at `null` / `"empty"`. A follow-up mount effect
236
- that cleared stream state whenever `docApi` was falsy ran after the
237
- `recordEvent` replay and wiped the first row; clearing now happens only after
238
- a real disconnect, and cached payloads are re-applied when `docApi` turns
239
- ready. The handshake manager also wires stream subscriptions when
240
- negotiation starts. Regression tests in
241
- `tests/sdk/selection-initial-record.test.tsx`.
242
- - **Selection stuck on the first row** — `useGristSelection` now shallow-copies
243
- `onRecord` / `onRecords` payloads. Grist can reuse one record object and
244
- mutate fields in place; React skipped re-renders when the reference was
245
- unchanged, so `w.record` looked frozen (e.g. always `{ "id": 1 }`).
246
-
247
- ### Changed
248
-
249
- - **Widget Pages deploy concurrency** — GitHub Actions deploy workflows use
250
- `queue: max` on the shared `pages-gh-pages` group so multiple widget deploys
251
- triggered by one push (e.g. `packages/core` changes) queue instead of canceling
252
- each other while waiting.
253
-
254
- - **SDK alerts use classic `severity` only** — `info` / `warning` / `error` on
255
- each descriptor; host shells style from `severity` (template `GristSdkAlerts`
256
- maps warning → amber, info → muted, error → destructive).
257
- - **Full SOTA handshake — no legacy connectivity path** — all SDK hooks now
258
- route through `GristHandshakeManager` only. Removed
259
- `useGristCoreFromLegacy`, the inline `mergeGristStatus()` ladder in the
260
- compose path, and the parallel mapping bootstrap in
261
- `useGristSelection` (mappings + `columnMappingStatus` now project from
262
- `snapshot.config.mappings` via `deriveColumnMappingStatus` /
263
- `extractResolvedMappings`). Standalone `useGrist()` / mid-level hooks
264
- without `<GristWidgetProvider>` share a ref-counted page-level embedded
265
- manager (`acquireEmbeddedHandshakeManager`). `useGristReady` and
266
- `useGristAvailability` are thin wrappers over `useGristCore` (FSM-backed).
267
- `useGristCurrentTable` feeds `CURRENT_TABLE_*` actions into the reducer and
268
- reads `currentTableId` / loading / errors from `snapshot.sync.currentTable`.
269
- Mid-level `useGristTableOps` / `useGristRowsFromTable` participate in
270
- heartbeat coalescence via `useRpcHeartbeatCoalesce`. Deleted
271
- `tests/unit/handshake-legacy-equivalence.test.ts` (obsolete).
272
- - **Heartbeat auto-coalescence across the slice hooks** — `task-066`.
273
- Every successful Grist RPC issued through the SDK's slice hooks
274
- (`useGristWrites().applyActions` / `.table.*` / `.getTable(…)`,
275
- `useGrist().fetchTable` / `.fetchTableRows` / `.fetchRow` /
276
- `.listTables` / `.getDocName` / `.fetchSelectedTable` /
277
- `.fetchSelectedRecord` / `.buildReplicaDocumentFromDocApi`,
278
- `useGrist().getAttachmentUrl` / `.fetchAttachmentBase64` /
279
- `.fetchAttachmentBlob` / `.getAccessToken` / `.fetchWithAuth`,
280
- `useGrist().getWidgetOptions` / `.getWidgetOption` / `.setWidgetOption` /
281
- `.setWidgetOptions` / `.patchWidgetOptions` / `.clearWidgetOptions`,
282
- `useGrist().setCursorPosition` / `.setLinkedRowSelection`,
283
- `useGristSectionApi().configure` / `.refreshMappings`,
284
- `useGrist().refreshCurrentTable`, and `useGristActions().apply` and
285
- derivatives) is now reported to the handshake manager via a single
286
- internal `useRpcHeartbeatCoalesce()` helper. The heartbeat treats each
287
- success as a free `HEARTBEAT_OK` and skips the next scheduled probe;
288
- failures shorten the next probe to ≤ 1 s for fast re-confirmation.
289
- Chatty widgets cost zero extra round-trips; quiet widgets keep their
290
- baseline 30 s health check. Outside `<GristWidgetProvider>` /
291
- `<GristHandshakeProvider>` the helper is a zero-cost passthrough.
292
- - The legacy `<GristWidgetProvider>` was restructured into a two-layer
293
- component so the internal manager context is mounted *before* the
294
- slice composer runs (`GristWidgetContextTree` under the manager). Without this,
295
- `useRpcHeartbeatCoalesce()` resolved to `null` inside the slice hooks and
296
- coalescence silently no-op'd — `tests/sdk/handshake-rpc-coalesce.test.tsx`
297
- pins the contract.
298
- - Defensive `typeof grist === "undefined"` guards added to
299
- `useGristCurrentTable` and `useGristSelection` so late passive
300
- effects firing after a test's `emulator.dispose()` no longer surface
301
- `ReferenceError: grist is not defined`. Pre-existing flake, surfaced
302
- by the new test layout, now 0/10 in the stability loop.
303
- - **`<GristWidgetProvider>` now mounts the handshake manager internally** —
304
- `task-065`. The legacy provider creates a single `GristHandshakeManager` per
305
- instance and exposes it through a private context. `useGristCore` reads from
306
- that manager (via `useSyncExternalStore`) and projects the FSM snapshot into
307
- the same `{ status, isAvailable, isReady, error, docApi, reload }` shape the
308
- slice composer expects, so every existing slice hook
309
- (`useGristSelection`, `useGristReads`, `useGristWrites`, …) transparently
310
- benefits from the FSM-driven timing without any API change. Outside the
311
- provider, `useGristCore` falls back to the pre-FSM
312
- `useGristAvailability` + `useGristReady` chain so standalone escape hatches
313
- keep working byte-for-byte.
314
- - The manager's negotiate effect routes through the existing
315
- `ensureGristReady()` singleton (so any stray `useGristReady()` user
316
- coalesces with the manager's ready call), and the provider passes
317
- `onBeforeReload: resetGristReadySingleton()` so a user-triggered
318
- `reload()` actually re-issues `grist.ready` instead of replaying the
319
- cached promise.
320
- - Heartbeat is on by default for the provider (same defaults as
321
- `<GristHandshakeProvider>`); pass nothing to keep it, or thread
322
- `options.heartbeat = false` through the manager if a widget needs to opt out.
323
- - Migration is transparent — no widget code change required. Two pre-existing
324
- flaky integration tests (`tests/sdk/sdk-react.test.tsx` and
325
- `tests/sdk/column-mapping-pending.test.tsx`) were tightened from a
326
- "snapshot once" assertion to a single `waitFor` block on the fully-settled
327
- state, because `pendingColumnMappingStatus.ok` is `true` (no `missing`
328
- columns reported yet) and the legacy assertion was racing against the
329
- transient pending-but-ok window.
330
-
331
- ### Internal
332
-
333
- - **Handshake state machine (foundation)** — `task-060`. New
334
- `packages/core/src/sdk/internal/handshake/` module models the widget ↔ Grist
335
- relationship as five orthogonal axes (`LIFECYCLE`, `LINK`, `AUTHZ`, `CONFIG`,
336
- `SYNC`) feeding a pure reducer + status projection + capability derivation.
337
- - **Handshake effects layer** — `task-061`. `internal/handshake/effects/`
338
- wires the pure machine to a real (or stubbed) runtime:
339
- - `detect.ts` — exponential-backoff polling for `window.grist`, adaptive
340
- budget driven by `navigator.connection.effectiveType` (30 s on 4g, 60 s
341
- on 3g, 120 s on 2g/slow-2g).
342
- - `negotiate.ts` — issues `grist.ready` with a 30 s timeout and an external
343
- `AbortSignal`; bridges promise/sync ready impls.
344
- - `subscriptions.ts` — pluggable binder; default wires SDK singletons,
345
- `noopSubscriptionsBinder` available for tests.
346
- - `mappings.ts` — declare + `sectionApi.mappings()` fetch + stream-payload
347
- ingestion + 5 s `MAPPING_TIMEOUT` fallback to `unreported`.
348
- - `manager.ts` — `GristHandshakeManager` owns the snapshot, runs effects in
349
- response to lifecycle transitions, bumps generation on `reload()`,
350
- cancels through a per-generation `AbortController`. Implements the
351
- `subscribe` / `getSnapshot` interface React's `useSyncExternalStore`
352
- requires. Exposes `recordRpcSuccess()` / `recordRpcFailure()` for
353
- external coalescing with the heartbeat.
354
- - **Heartbeat effect** — `task-063`. `internal/handshake/effects/heartbeat.ts`:
355
- - Interval probe (default 30 s) calls `grist.docApi.getDocName()` (or any
356
- custom `probe`); per-probe timeout default 10 s.
357
- - `recordRpcSuccess()` coalesces with natural RPC traffic and pushes the
358
- next probe out by a full interval — we don't burn requests.
359
- - `recordRpcFailure()` immediately degrades the link signal and shortens
360
- the next probe to ≤ 1 s for fast re-confirmation.
361
- - Pauses on `visibilitychange:hidden` and resumes (with an immediate probe)
362
- on `visible`. Listens to `online` events for instant network-recovery
363
- re-probe.
364
- - Reducer transitions: `connected → stale` after `staleAfterMissed`
365
- misses, `→ lost` after `lostAfterMissed`. `lost` escalates to global
366
- `"error"` status via `deriveStatus`.
367
- - **Environment abstraction** — `internal/handshake/environment.ts` exposes a
368
- `GristEnvironment` interface (`now`, `setTimeout`, `probeGrist`,
369
- `effectiveTypeHint`, …) so effects are unit-testable with a virtual clock
370
- via `createTestEnvironment`. Production code uses `createBrowserEnvironment`.
371
- - **Mapping resolver** — `MappingResolver` merges column mappings from
372
- `section_api` / `stream_record` / `stream_records` / `stream_new_record`
373
- with a fixed priority order, making the final resolution a pure function of
374
- the set of received payloads. Payloads stamped with a stale generation are
375
- dropped silently.
376
-
377
- ### Tests
378
-
379
- - **Handshake reducer + resolver** — 50 new unit tests covering: 24-permutation
380
- resolver determinism, generation-stamped action drop, reducer idempotence
381
- for duplicate stream payloads, link transitions `connected → stale → lost`,
382
- mapping invalidation/recovery, current-table local-error containment, and
383
- capability gates (`canRender` / `canWriteRecords` / `canWriteSchema` /
384
- `hasFreshSelection`).
385
- - **Manager lifecycle** — 14 unit tests with virtual time: detect budget
386
- exhaustion, NOT_EMBEDDED detection, negotiate success/failure/timeout,
387
- `reload()` generation bump + lifecycle reset, stale-generation drop,
388
- capability transitions through online + mapping completion, no error
389
- escalation on incomplete mappings, and `subscribe` / `getSnapshot`
390
- contract for `useSyncExternalStore`.
391
- - **`useGristHandshake()` integration** — 3 emulator-driven tests in
392
- `tests/sdk/handshake-react.test.tsx` covering ready transition, mapping
393
- state propagation, and stream-subscription wiring against `renderWithGrist`.
394
- - **Heartbeat unit tests** — 11 tests in `tests/unit/handshake-heartbeat.test.ts`:
395
- interval start (no t=0 probe), probe success / reject / timeout dispatches,
396
- repeated firing, RPC coalesce, visibility pause + resume, `online` event,
397
- `cancel()` cleanup.
398
- - **Heartbeat ↔ manager integration** — 5 tests in
399
- `tests/unit/handshake-manager.test.ts`: link degradation `connected → stale
400
- → lost`, `recordRpcSuccess()` reset, heartbeat shutdown on `stop()`,
401
- `heartbeat: false` disablement.
402
- - **`<GristHandshakeProvider>` integration** — 4 tests in
403
- `tests/sdk/handshake-provider.test.tsx`: shared snapshot across consumers,
404
- optional vs throwing context hooks.
405
- - **Property-based / chaos tests** — `task-067`. 12 tests in
406
- `tests/unit/handshake-properties.test.ts` using `fast-check`:
407
- resolver determinism (200 random runs per property), generation gate,
408
- reducer idempotence, `terminated` absorption, fuzz sequences of up to 30
409
- random actions (200 runs) confirming no throws, monotone generation,
410
- and `link.state` stays in its closed domain. Capability gates are
411
- asserted to form a conjunctive chain
412
- `canWriteSchema ⇒ canWriteRecords ⇒ canRender ⇒ canRead` over 300
413
- randomized snapshots.
414
-
415
- ### Dev dependencies
416
-
417
- - Added `fast-check ^4.8.0` (used only by `tests/unit/handshake-properties.test.ts`).
418
-
419
- ### Docs
420
-
421
- - **Handshake module documentation** — `task-068`. New API reference page
422
- at `/api/handshake` covering the public hooks
423
- (`useGristHandshake` / `useGristCapabilities` / `<GristHandshakeProvider>` /
424
- `useGristHandshakeContext` / `useGristHandshakeContextOptional`), the full
425
- snapshot shape (`GristWidgetSnapshot`, `GristLifecycle`, `GristLink`,
426
- `GristAuthz`, `GristConfig`, `GristSync`), derived `GristCapabilities`,
427
- heartbeat coalescence semantics, and `reload()` vs `restart()`. New
428
- conceptual guide page at `/guide/handshake` covering when to use the
429
- new hooks, the five-axis state machine, the capability chain, the
430
- heartbeat, the mapping states, and generation discipline.
431
- - Updated `/api/index.md`, `/api/provider-boundary.md`, `/guide/concepts.md`,
432
- and `/guide/error-handling.md` to cross-link the new module and to
433
- describe the FSM-backed implementation of `<GristWidgetProvider>`.
434
- - VitePress navigation: handshake guide listed under "Advanced topics",
435
- handshake API ref listed in the unified Reference sidebar.
436
-
437
- ### Process
438
-
439
- - **Lightweight workflow.** Dropped the seven-step `/ITERATION.md` cycle;
440
- planning lives in chat. Roadmap + task board + `apps/docs/work.md` replace
441
- the formal spec file.
442
-
443
- ### Changed
444
-
445
- - **Slice hook return stability** — slice hooks memoize their result objects so
446
- React context consumers and `React.memo` children keep stable callable references
447
- (`table`, `mapBack`, `reload`, …) across unrelated slice updates.
448
-
449
- ### Tests
450
-
451
- - **Slice identity** — `slice-identity.test.tsx` asserts zero extra renders for
452
- memoized children when selection, writes, theme, or status slices change in isolation;
453
- 1000-row `records` list stays stable on cursor-only changes.
454
-
455
- - **Render budget bench** — `pnpm --filter grist-widget-sdk bench` runs
456
- `tests/bench/render-budget.test.tsx` and writes `packages/core/bench/results.json`
457
- (full-record / slice / write / schema-fetch render deltas on `presets.todoList()`).
458
-
459
- - **`useGristSchema` snapshots** — `use-grist-schema.snapshot.test.tsx` guards
460
- `blank` / `todoList` / `contacts` × `schema-only` / `schema+samples` /
461
- `schema+data` replica output from the emulator.
462
-
463
- ### Docs
464
-
465
- - **Render budgets** — `/guide/performance` documents measured re-renders per
466
- operation from the bench harness and slice-isolation expectations.
467
-
468
- ### Fixed
469
-
470
- - **`waitForEvent`** no longer resolves immediately from bus history; waits for
471
- the next matching event. Kind overload (`ready`, `record`, `records`, `options`,
472
- `theme`, `cursor`) and clearer timeout errors.
473
- - **Column mapping on load** — `columnMappingStatus.pending` stays true until Grist
474
- reports mappings (`onRecord` / `onRecords` or `sectionApi.mappings()`). Widgets
475
- no longer show a false "Column mapping is incomplete" alert during the brief
476
- window after `status === "ready"`. `getGristSdkAlertDescriptors` ignores
477
- pending mapping status.
478
-
479
- ### Changed
480
-
481
- - **`columns` vs `safeParse` on reads** — `/guide/reading-data` documents the
482
- contract: `columns` alone yields plain decoded rows; `safeParse` adds per-cell
483
- issue tracking. Covered by unit tests in `grist-table-data.test.ts`.
484
- - **Retired `/design/api-surface.md`** — export list lives in
485
- `public-api.test.ts` + `/api/index`; conventions in `/design/principles`.
486
- `tests/docs/structure.test.ts` fails CI if the page returns.
487
- - **Cleared shipped items** from `/design/open-questions` → *Pending API
488
- tightenings* (0.3+ work stays on the task board).
489
-
490
- ### Tests
491
-
492
- - `packages/core/tests/unit/grist-table-data.test.ts` — `columns` vs `safeParse`
493
- materialization shapes.
494
- - `packages/core/tests/docs/structure.test.ts` — docs project in Vitest;
495
- `api-surface.md` must not exist.
496
-
497
- **Learnings:** The duplicate api-surface page was pure drift risk once
498
- `public-api.test.ts` existed; documenting `columns` without `safeParse` stops
499
- readers from wrapping every fetch in safe-parse cell types.
500
-
501
- ### Documentation & DX
502
-
503
- - **Developer pathway is now template-first.** `/guide/getting-started`
504
- opens with a two-minute `degit` TL;DR, then keeps manual install +
505
- hello-world below the fold.
506
- - **Cheat sheet, cookbook, troubleshooting, templates, and demos.**
507
- Four new guide pages under `/guide/` (cookbook = 10 end-to-end
508
- recipes, cheatsheet, troubleshooting, templates) plus a
509
- top-level demo catalogue at `/demos`.
510
- - **Three live demo widgets.** `form-edit`, `task-board`, and
511
- `attachment-gallery` under `apps/playground/src/widgets/`,
512
- reachable at `https://demo.grist-widgets.com/widget.html?id=<id>`
513
- (raw, pasteable into a Grist Custom Widget URL) and
514
- `https://demo.grist-widgets.com/?url=widget.html?id=<id>` (preview
515
- in the playground shell, embedded in `/demos`).
516
- - **Agent guide consolidation.** `/files/*` is reduced to 9 pages
517
- (`AGENT.md`, `architecture.md`, `testing-patterns.md`,
518
- `replica-document.md` deleted). Architecture / replica content
519
- lives canonically under `/design/`. `/files/start-here.md` becomes
520
- the single dense entry: operating contract, eight-step workflow
521
- summary, commands, path map, decision tree, anti-patterns,
522
- hand-off checklist. Every remaining `/files/*` page carries an
523
- Audience / Companion / Verified-in preamble.
524
- - **`llms.txt` + `llms-full.txt`** at `apps/docs/public/`. The first
525
- is the standard llmstxt.org index for AI tool discovery; the
526
- second concatenates every page in the slim `/files/*` set in
527
- alphabetical order with `# <path>` delimiters so a single fetch
528
- primes a model with the entire agent operating manual.
529
- - **"Choose your path" tiles** on the docs home page: four entry
530
- points keyed on intent (writing a widget / AI agent / evaluator /
531
- see-it-work).
532
- - **Code-block contract.** Every fenced `tsx` / `ts` block in
533
- `/guide/getting-started`, `/guide/cookbook`,
534
- `/guide/cheatsheet`, and the landing page (`/`) whose first
535
- line is `// @example` is type-checked against the SDK at test
536
- time. Catches API drift between docs and code automatically.
537
- The contract caught five real bugs on first run
538
- (gristAddTableAction arg shape, safeParseGristTableData result
539
- field name, useGristSchema option name, missing presets.simple,
540
- renderWithGrist option shape) — all fixed.
541
- - **Landing page reworked for one-look DX + agent triage.** Three
542
- hero CTAs (Get started / Reference / Demos), an inline `npx
543
- degit` TL;DR scaffold block, a complete fifteen-line widget
544
- shown as a `// @example` `tsx` block (type-checked alongside the
545
- rest of the docs), and a "Choose your path" markdown table that
546
- routes by intent — writing a widget, AI coding agent (links to
547
- `/files/start-here`), evaluating the SDK, or seeing it work.
548
- Feature cards rewritten to name the concrete APIs each one
549
- represents and link to the most relevant `/api/` page.
550
- - **Reference consolidation.** `Design` is no longer a top-level
551
- nav entry; `Reference` collapses Design + API rationale under
552
- one umbrella in nav and sidebar. URLs unchanged — `/api/*` and
553
- `/design/*` still resolve where they did, and both share the
554
- same sidebar grouping. `apps/docs/design/api-surface.md` is
555
- retired: its conventions (naming / slot / empty-null / promise)
556
- move to `/design/principles.md` as a `## Conventions` section,
557
- and its "breaking changes to do" list moves to
558
- `/design/open-questions.md` as `## Pending API tightenings`.
559
- - **Landing-page polish.** Body region order is now What it looks
560
- like → Start with Vite → Choose your path → Highlights: code
561
- first, scaffold second, routing third, marketing last. The
562
- `npx degit` block lives under a new `## Start with Vite` heading
563
- that explicitly flags more templates (Next.js, plain HTML) are
564
- planned, linking to `/guide/templates` as the running roster.
565
- YAML feature cards (which rendered above the wayfinder and
566
- carried deep links) are replaced by a `## Highlights` markdown
567
- section below the Choose-your-path table — six capability blurbs
568
- with no outbound links so the table is the only navigation
569
- surface on the home page.
570
- - **`/demos` promoted to a top-level URL.** The catalogue page
571
- was previously at `/guide/demos`, miscategorising the showcase
572
- as a learning step. Now lives at `/demos` with its own top-nav
573
- entry, no sidebar (matches the catalogue shape). All cross-
574
- links — landing page CTAs, getting-started "Next steps",
575
- `llms.txt` index, demos.md internal Cookbook links — migrated
576
- in the same commit so the build stays green at every HEAD.
577
- - **Hero image.** The home page hero now has a real screenshot
578
- next to the headline: the `form-edit` demo widget rendered
579
- beside the playground's emulator panel. ~39 KB PNG at
580
- `apps/docs/public/hero.png`, served via VitePress's
581
- `hero.image` frontmatter slot. Real product surface, zero
582
- illustration work, zero external network dep on first paint.
583
-
584
- - Consolidated the docs `/work/` folder (roadmap + task board + guidelines +
585
- release process) into a single iteration workflow described in
586
- [`apps/docs/work.md`](/work).
587
- The source of truth for in-flight scope is now `/ITERATION.md`; this
588
- `CHANGELOG.md` is the source of truth for history.
589
- - Root `pnpm test` now runs the SDK suite **plus** the docs build
590
- **plus** the playground's widget bundle. Dead links and unbound
591
- widget metadata trip CI without a separate workflow change.
592
- - New `packages/core/tests/docs/` vitest project (node env). Four
593
- files: `code-blocks.test.ts` (type-checks `// @example` blocks
594
- in the three guide pages **and** the landing page),
595
- `structure.test.ts` (slim /files/* set, preamble blocks, cookbook
596
- recipe count, cookbook → demo cross-links, demos catalogue
597
- shape, **landing-page hero / TL;DR / @example / table shape,
598
- `/design/api-surface.md` retired, `/design/principles.md`
599
- Conventions block, `/design/open-questions.md` Pending API
600
- tightenings, body order (code-before-scaffold), uniform feature-
601
- card `linkText: "Learn more →"`, hero `image.src: /hero.png`
602
- with the file on disk, `/demos` top-level move**),
603
- `links.test.ts` (resolves every relative link across
604
- `apps/docs/**` and the three root files), `llms-txt.test.ts`
605
- (asserts both `llms.txt` and `llms-full.txt`).
606
-
607
- ### Learnings
608
-
609
- - The single highest-leverage change was the `// @example`
610
- type-check: it caught five real API mismatches between docs and
611
- code on the first run. Future iterations should keep adding the
612
- sentinel to new code blocks rather than relying on review.
613
- - Splitting the docs site into two audiences (`/guide/` for
614
- developers, `/files/` for agents) with strict no-duplication
615
- rules and a slim, deterministic agent set produced a much cleaner
616
- navigation than the previous mixed structure. The `/files/`
617
- preamble (Audience / Companion / Verified-in) is the gate that
618
- keeps the agent surface honest.
619
- - Embedding demos via the existing playground shell (preview URL =
620
- emulated table next to the widget) is a much better
621
- reading-experience than a bare widget — the reader sees both the
622
- data and the widget's reaction to it. Worth keeping that pattern
623
- for any future demo iteration.
624
- - Including agent-facing top-level files (`ITERATION.md`,
625
- `CHANGELOG.md`) into VitePress pages via the `@include`
626
- directive is convenient but couples two link-resolution
627
- contexts: the source file (read from repo root, GitHub, IDE)
628
- and the included page (read from the docs site). Relative
629
- links in the source silently break in one of the two. Rule of
630
- thumb: any link that lives inside the included range must use
631
- an absolute URL — prefer the GitHub permalink so the source
632
- file still works outside the docs build.
633
- - Nav consolidation under `Reference` (instead of separate `API` /
634
- `Design` entries) without moving files is a strict improvement:
635
- unifies the mental model for readers and AI agents, keeps every
636
- external URL stable. Prefer sidebar-level grouping over directory
637
- reshuffles whenever the cost is borne by future external links.
638
- - The opposite intuition holds when a page is genuinely
639
- miscategorised: `/demos` belonged at the top level, not under
640
- `/guide/`. The move broke a handful of cross-links (cookbook,
641
- getting-started Next steps, `llms.txt`, demos.md's own
642
- `./cookbook` references) but every break was caught at build
643
- time by the dead-link detector + the existing `links.test.ts`,
644
- with no manual auditing required. Lesson: trust the safety
645
- net, ship the structural fix, watch the build fail loudly, fix
646
- the breaks. Cheaper than living with the miscategorisation.
647
- - Two-pillar landing-page surface — a code block ("what does this
648
- look like") plus an image ("what does a real widget look like")
649
- — answers more pre-commitment questions than either alone. The
650
- image being a real product screenshot (the form-edit demo +
651
- emulator side by side) rather than an illustration carries
652
- more weight: the reader trusts the surface they're shown is
653
- the one they'll be building. Worth preserving as the SDK
654
- matures — swap the asset, keep the slot.
655
- - When build-green-at-HEAD is a hard constraint and two changes
656
- are coupled (the `/demos` URL was simultaneously consumed by
657
- the landing page and produced by the moved file), the
658
- three-commit plan from the spec collapsed to a two-commit
659
- reality. Cleaner to acknowledge this in the commit message
660
- and bundle than to leave a broken HEAD or carry a fake "the
661
- page exists at the new location but no one links to it" interim
662
- state.
663
-
664
- ### Added
665
-
666
- - `useGristStatus()` now exposes `currentTableId`, `currentTableLoading`,
667
- `refreshCurrentTable`, `tableError`, and the raw `docApi` handle. The hook
668
- is a single subscription point for "status + selected table" UIs.
669
- - `mapBack(patch)` reports skipped logical names via `w.mapBackSkipped`
670
- (and on `useGristSelection().mapBackSkipped`). A new alert descriptor
671
- `kind: "map-back-skip"` surfaces them through
672
- `getGristSdkAlertDescriptors(...)`.
673
- - `formatMapBackSkipMessage(skipped, hint?)` for hosts that build alert copy
674
- themselves.
675
- - `presets` (`blank`, `todoList`, `contacts`) are now re-exported from
676
- `grist-widget-sdk/emulator/testing`.
677
- - `grist-widget-sdk/emulator/testing` re-exports the most-used
678
- `@testing-library/react` primitives (`screen`, `fireEvent`, `waitFor`,
679
- `act`, `cleanup`, `within`, `render`).
680
- - `emulator.theme.set("light" | "dark")` convenience for tests that drive
681
- theme transitions.
682
-
683
- ### Changed
684
-
685
- - **Breaking:** Slice hooks (`useGristStatus`, `useGristSelection`,
686
- `useGristWrites`, `useGristTheme`) now require a parent
687
- `<GristWidgetProvider>` and throw outside of one. Use `useGrist()` for the
688
- standalone single-leaf case.
689
- - The inline emulator transport emits `themeInitialChange` /
690
- `themeChange` instead of a single `theme` event, matching production
691
- `grist-plugin-api.js`. Late listeners get replayed.
692
-
693
- ### Tests
694
-
695
- - Public-API snapshot pins every documented symbol across the four entry
696
- points (`/`, `/advanced`, `/emulator`, `/emulator/testing`).
697
- - Slice-hook integration tests for status / selection / writes / theme using
698
- the emulator (`renderWithGrist`).
699
- - Action-builder shape tests for every supported user action.
700
- - `mapBack` + `allowMultiple` end-to-end test exercising the alert path.
701
-
702
- ### Process
703
-
704
- - Consolidated the docs `/work/` folder (roadmap + task board + guidelines +
705
- release process) into a single iteration workflow described in
706
- [`apps/docs/work.md`](/work).
707
- The source of truth for in-flight scope is now `/ITERATION.md`; this
708
- `CHANGELOG.md` is the source of truth for history.
709
-
710
- ## 0.1.0
711
-
712
- ### Added
713
-
714
- - Slice hooks: `useGristSelection`, `useGristWrites`, `useGristStatus`, `useGristTheme`
715
- - `patchWidgetOptions`, `configure`, `refreshMappings` on `useGrist()`
716
- - `fetchAttachmentBlob`, schema table action builders, brief REST token cache
717
- - Theme subscription via `grist.on("themeInitialChange" | "themeChange")`
718
-
719
- ### Changed
720
-
721
- - **Breaking:** Removed deprecated `updateRecord` / `addRecord` / `bulk*` helpers from `useGrist()`
722
- - **Breaking:** Removed `buildDocument()` — use `buildReplicaDocumentFromDocApi()` only
723
- - `useGristSchema()` defaults to `requiredAccess: "read table"`
724
- - `GristBoundary` unavailable grace period increased to 5s
725
- - Refactored `useGrist` into composable internal hooks + context slices
726
-
727
- ### Fixed
728
-
729
- - `currentTableId` now refreshes when the selected row changes
730
- - `validateColumnMappings` no longer double-counts missing `allowMultiple` columns
22
+ Published manually (pre-Changesets); see the root changelog's **0.1.0** →
23
+ **Unreleased — 0.2.0** history for everything that shipped in this version.