@colixsystems/widget-sdk 0.133.0 → 0.134.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/README.md CHANGED
@@ -35,32 +35,32 @@ The data layer lives in **four separate domain-client packages**, each instantia
35
35
  | **CORE** | `useRefresh(handler)` | `void` | `ctx.refresh.subscribe` — no scope. Subscribes the handler to the page-level refresh tick (pull-to-refresh on mobile). Handler may return a Promise — the host waits for `allSettled` before clearing the spinner. The three datastore hooks auto-subscribe their own `refetch`; widgets only call this directly to re-run non-datastore work. No-op on a host that doesn't implement refresh. |
36
36
  | **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
37
37
  | **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` — wired by the Player and the Expo export; an authoring preview omits it and the call is a no-op — no scope |
38
- | **CORE** | `useGeolocation(options?)` | `{ latitude, longitude, accuracy, loading, error, getCurrentPosition, backgroundSupported, backgroundWatching, startBackgroundWatch, stopBackgroundWatch }` | `ctx.device.geolocation` — no scope. Capture is IMPERATIVE: call `getCurrentPosition()` from a user gesture (a tap), never on mount. Resolves to `{ latitude, longitude, accuracy }`; rejects with `GeolocationError` (`.code` in `PERMISSION_DENIED \| UNAVAILABLE \| TIMEOUT \| UNSUPPORTED \| INTERNAL`). Identical on web (`navigator.geolocation`) and the Expo export (`expo-location`). **Background watch (sc-6450)** — `startBackgroundWatch({ enableHighAccuracy, distanceIntervalMeters, timeIntervalMs })` keeps positions arriving while the app is backgrounded; `stopBackgroundWatch()` releases it. NATIVE-ONLY and opt-in per app: gate the control on `backgroundSupported` (false on web, and in an export whose workspace did not opt in). The watch outlives the widget's mount, and its positions land in the same `latitude`/`longitude`/`accuracy` slots. |
39
- | **CORE** | `useSpeechToText(options?)` | `{ transcript, partial, listening, supported, error, start, stop, abort, reset }` | `ctx.device.speech` — no scope. Dictation with the device's **on-device** recogniser: no audio is uploaded and no AI credit is spent. Capture is IMPERATIVE: call `start()` from a user gesture (a tap), never on mount. `transcript` accumulates finalised speech, `partial` holds the uncommitted guess (needs `options.interimResults`); `stop()` keeps it, `abort()` discards it. Rejects with `SpeechToTextError` (`.code` in `PERMISSION_DENIED \| NO_SPEECH \| LANGUAGE_UNSUPPORTED \| NETWORK \| ABORTED \| UNSUPPORTED \| INTERNAL`). **Gate your mic button on `supported`** — Firefox ships no `SpeechRecognition`. Identical on web (`SpeechRecognition`) and the Expo export (`expo-speech-recognition`). |
40
- | **CORE** | `useCamera(options?)` | `{ asset, loading, error, supported, capture, pick, reset }` | `ctx.device.camera` — no scope. Take a photo (`capture()`) or choose one (`pick()`). Capture is IMPERATIVE: call from a user gesture (a tap), never on mount. Both resolve a normalised `{ uri, name, mimeType, width, height, size, file }`, or **`null` when the user dismisses the picker** — dismissal is not an error, so no `try/catch` is needed on the happy path. Rejects with `CameraError` (`.code` in `PERMISSION_DENIED \| UNSUPPORTED \| INTERNAL`). `asset.file` is already the right upload part for the host, so `fd.append("file", asset.file)` → `ctx.assets.upload(fd)` is ONE code path on both. **Gate your camera button on `supported`.** Identical on web (a live `getUserMedia` preview, phones included; a file input only where getUserMedia is absent) and the Expo export (`expo-image-picker`). |
38
+ | **CORE** | `useGeolocation(options)` (optional: options) | `{ latitude, longitude, accuracy, loading, error, getCurrentPosition, backgroundSupported, backgroundWatching, startBackgroundWatch, stopBackgroundWatch }` | `ctx.device.geolocation` — no scope. Capture is IMPERATIVE: call `getCurrentPosition()` from a user gesture (a tap), never on mount. Resolves to `{ latitude, longitude, accuracy }`; rejects with `GeolocationError` (`.code` in `PERMISSION_DENIED \| UNAVAILABLE \| TIMEOUT \| UNSUPPORTED \| INTERNAL`). Identical on web (`navigator.geolocation`) and the Expo export (`expo-location`). **Background watch (sc-6450)** — `startBackgroundWatch({ enableHighAccuracy, distanceIntervalMeters, timeIntervalMs })` keeps positions arriving while the app is backgrounded; `stopBackgroundWatch()` releases it. NATIVE-ONLY and opt-in per app: gate the control on `backgroundSupported` (false on web, and in an export whose workspace did not opt in). The watch outlives the widget's mount, and its positions land in the same `latitude`/`longitude`/`accuracy` slots. |
39
+ | **CORE** | `useSpeechToText(options)` (optional: options) | `{ transcript, partial, listening, supported, error, start, stop, abort, reset }` | `ctx.device.speech` — no scope. Dictation with the device's **on-device** recogniser: no audio is uploaded and no AI credit is spent. Capture is IMPERATIVE: call `start()` from a user gesture (a tap), never on mount. `transcript` accumulates finalised speech, `partial` holds the uncommitted guess (needs `options.interimResults`); `stop()` keeps it, `abort()` discards it. Rejects with `SpeechToTextError` (`.code` in `PERMISSION_DENIED \| NO_SPEECH \| LANGUAGE_UNSUPPORTED \| NETWORK \| ABORTED \| UNSUPPORTED \| INTERNAL`). **Gate your mic button on `supported`** — Firefox ships no `SpeechRecognition`. Identical on web (`SpeechRecognition`) and the Expo export (`expo-speech-recognition`). |
40
+ | **CORE** | `useCamera(options)` (optional: options) | `{ asset, loading, error, supported, capture, pick, reset }` | `ctx.device.camera` — no scope. Take a photo (`capture()`) or choose one (`pick()`). Capture is IMPERATIVE: call from a user gesture (a tap), never on mount. Both resolve a normalised `{ uri, name, mimeType, width, height, size, file }`, or **`null` when the user dismisses the picker** — dismissal is not an error, so no `try/catch` is needed on the happy path. Rejects with `CameraError` (`.code` in `PERMISSION_DENIED \| UNSUPPORTED \| INTERNAL`). `asset.file` is already the right upload part for the host, so `fd.append("file", asset.file)` → `ctx.assets.upload(fd)` is ONE code path on both. **Gate your camera button on `supported`.** Identical on web (a live `getUserMedia` preview, phones included; a file input only where getUserMedia is absent) and the Expo export (`expo-image-picker`). |
41
41
  | **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) first, then a **predefined shared key** (`shared.<key>`) when `key` is one of the standard strings (`submit`, `cancel`, `save`, `loading`, …), then the raw key. Use a shared key for an identical default string so it translates once and any per-instance `widget.<id>.<key>` override still wins. |
42
42
  | **CORE** | `useTranslate()` | `{ translate, translating, error, language, available }` | `ctx.i18n.translate` — no scope. Machine-translates **user-generated content** (record text, file names, API payloads) into the app user's language; `useI18n().t()` is still the answer for your own copy. `translate(str)` → `Promise<string>`, `translate(str[])` → `Promise<string[]>` in ONE request. Target defaults to the app user's language. Cached per session, per pod, and durably per workspace, so repeat text is free. Limits: 50 segments / 5 000 chars each / 20 000 total. Rejects with `TranslateError`; `available` is false where the host cannot translate. |
43
43
  | **CORE** | `useStableQuery(buildQuery)` | `T \| undefined` (whatever `buildQuery()` returns) | No context slice, no scope. Keeps `buildQuery()`'s result at a STABLE reference across renders when its (JSON-serialised) content hasn't changed, so `useDatastoreQuery(tableId, useStableQuery(() => ({...})))` replaces a hand-rolled `useMemo` with an easy-to-get-wrong deps array. Never throws: a `buildQuery` that itself throws degrades to a stable `undefined`; a result that can't be diffed (e.g. circular) degrades to "always a new reference". |
44
- | **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options?)` | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
44
+ | **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options)` (optional: options) | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
45
45
  | **DATASTORE** | `useDatastoreRecord(table, id)` | `{ data, loading, error, refetch }` | `records(table).get` — `datastore.read:<table>` |
46
46
  | **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
47
47
  | **DATASTORE** | `useBoundColumns(tableId, shape, props)` | `{ columns, resolved, missing, loading, error }` | `schema(tableId)` (built on `useDatastoreSchema`) — `datastore.read:<table>`. Resolves author-bound column NAMES from `props` by exact name → case-insensitive name → first unclaimed column matching `shape[key].dataType`, so a column an author renamed after install still resolves instead of `record[props.titleField]` reading `undefined`. `columns` holds the resolved NAME (`record[columns.titleField]`); `resolved` holds the full `Column`; `missing` lists non-`optional` keys that never resolved. Falsy `tableId` collapses to `{ columns: {}, resolved: {}, missing: Object.keys(shape), loading: false, error: null }`. |
48
48
  | **DATASTORE** | `useInterpretDraft(tableId)` | `{ interpret, interpreting, error, result, available }` | `interpret(tableId, body)` — `datastore.read:<table>`. Turns ONE sentence a user typed ("walk at 11 am tomorrow") into DRAFT column values so a form can prefill itself. IMPERATIVE: call `interpret(text, { fields?, timeZone? })` from an event handler, never on mount. It DRAFTS and writes nothing — show the values for review, then submit through `useDatastoreMutation().create`. Resolves to `{ values, unresolved }`; `values` is keyed by column NAME (the shape `create()` takes) and `unresolved` names the fields the sentence did not state. Only text / number / boolean / date / datetime / array columns are drafted — `FILE`, `RELATION`, `USER` and `USER_GROUP` carry ids and are never guessed. Fails closed to an empty draft. **Every call spends the workspace's AI credits** and is rate-limited per actor, so call it once per user action (never on mount or in a render loop); once the workspace runs out the call is refused with a generic 429 — an app user is deliberately **not** told the workspace's billing state, since they have never heard of an AI credit and cannot buy one. Never surface a raw error to the person filling the form: say drafting is unavailable and keep every field editable by hand. `available` is false where the host brokers no interpreter. |
49
49
  | **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*` |
50
- | **DATASTORE** | `useDatastoreSubscription(table, handlers, options?)` | `{ status }` — `"connecting" \| "live" \| "reconnecting" \| "fallback"` | `records(table).subscribe` — `datastore.read:<table>`. Live `onCreated` / `onUpdated` / `onDeleted` off the REQ-RT-07 socket; never throws, resolving to `{ status: "fallback" }` so the widget polls instead. A whole-table subscribe is gated on read-EVERY-row, because one envelope reaches every subscriber of the table — so for a table governed by per-record grants pass `options.scope`: `{ kind: "record", record_id }` for one row, or `{ kind: "parent", relation_column, record_id }` for the rows whose RELATION column points at that parent (the column must carry `inheritAcl`, else the subscribe reports `"fallback"`). Re-subscribes on the scope's VALUES, so a fresh object literal each render is fine. |
50
+ | **DATASTORE** | `useDatastoreSubscription(table, handlers, options)` (optional: options) | `{ status }` — `"connecting" \| "live" \| "reconnecting" \| "fallback"` | `records(table).subscribe` — `datastore.read:<table>`. Live `onCreated` / `onUpdated` / `onDeleted` off the REQ-RT-07 socket; never throws, resolving to `{ status: "fallback" }` so the widget polls instead. A whole-table subscribe is gated on read-EVERY-row, because one envelope reaches every subscriber of the table — so for a table governed by per-record grants pass `options.scope`: `{ kind: "record", record_id }` for one row, or `{ kind: "parent", relation_column, record_id }` for the rows whose RELATION column points at that parent (the column must carry `inheritAcl`, else the subscribe reports `"fallback"`). Re-subscribes on the scope's VALUES, so a fresh object literal each render is fine. |
51
51
  | **DATASTORE** | `useRecordPermissions(tableId, recordId)` | `{ permissions, loading, error, grant, revoke, update, refetch }` | `records(table).permissions(record).{ list, grant, update, revoke }` — `acl.write:records` (+ `can_grant` on the record) |
52
- | **DATASTORE** | `useCanWrite(tableId, options?)` | `{ canWrite, loading, error, refetch }` | `myPermissions(tableId, { recordId? })` — scope `datastore.read:<table>`. A FLOOR, not a full replacement for domain-specific write rules: answers "may this caller write", reading the same table-ACL answer the write endpoint enforces — so a table granting Create to Everyone answers `true` for a logged-out visitor, and this hook alone is the right gate for a widget meant to work without signing in. Pass `{ recordId }` for a per-row check. A widget whose own rule is MORE SPECIFIC than the table ACL (e.g. "only the assigned user may edit this row") must still hand-check that in addition. Pair with `useUser()` to also tell "not signed in" apart from "signed in but forbidden" — both resolve `canWrite: false` here. Falsy `tableId`, or a host that hasn't injected `myPermissions` (an older host), collapses to `{ canWrite: false, loading: false, error: null, refetch: async () => undefined }` rather than throwing. |
52
+ | **DATASTORE** | `useCanWrite(tableId, options)` (optional: options) | `{ canWrite, loading, error, refetch }` | `myPermissions(tableId, { recordId? })` — scope `datastore.read:<table>`. A FLOOR, not a full replacement for domain-specific write rules: answers "may this caller write", reading the same table-ACL answer the write endpoint enforces — so a table granting Create to Everyone answers `true` for a logged-out visitor, and this hook alone is the right gate for a widget meant to work without signing in. Pass `{ recordId }` for a per-row check. A widget whose own rule is MORE SPECIFIC than the table ACL (e.g. "only the assigned user may edit this row") must still hand-check that in addition. Pair with `useUser()` to also tell "not signed in" apart from "signed in but forbidden" — both resolve `canWrite: false` here. Falsy `tableId`, or a host that hasn't injected `myPermissions` (an older host), collapses to `{ canWrite: false, loading: false, error: null, refetch: async () => undefined }` rather than throwing. |
53
53
  | **FILES** (`ctx.assets`) | `useAsset(id)` | `{ url, file, loading, error, refetch }` | `ctx.assets.get` — no scope |
54
- | **FILES** | `useAssetsByTag(tag, { type? })` | `{ assets, loading, error, refetch }` | `ctx.assets.list` (unwraps `{ data, meta }` to `assets`) — no scope. `type` defaults to `"image"`; pass `"all"` / `"audio"` / `"video"` / `"document"` to widen. Falsy `tag` collapses to `assets: []` without a round-trip. |
55
- | **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
56
- | **DIRECTORY** | `useUsers(query?)` | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove, sendPasswordReset }` | `directory.users.*` — `users.read:*` (edits, incl. `sendPasswordReset()`, also `users.write:*`; `remove()` also `users.delete:*`) |
57
- | **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
58
- | **DIRECTORY** | `useInvites(query?)` | `{ invites, loading, error, refetch, resend, revoke }` | `directory.invites.*` — `users.write:*` + the SystemAcl `users.write` capability (the whole invite surface, list included). `query` is `{ status?, limit?, offset? }` with `status` ∈ `pending \| accepted \| revoked \| expired \| all` (endpoint default `all`). |
54
+ | **FILES** | `useAssetsByTag(tag, { type })` (optional: the whole argument, and `type` within it) | `{ assets, loading, error, refetch }` | `ctx.assets.list` (unwraps `{ data, meta }` to `assets`) — no scope. `type` defaults to `"image"`; pass `"all"` / `"audio"` / `"video"` / `"document"` to widen. Falsy `tag` collapses to `assets: []` without a round-trip. |
55
+ | **DIRECTORY** (`ctx.directory`) | `useDirectory(query)` (optional: query) | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
56
+ | **DIRECTORY** | `useUsers(query)` (optional: query) | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove, sendPasswordReset }` | `directory.users.*` — `users.read:*` (edits, incl. `sendPasswordReset()`, also `users.write:*`; `remove()` also `users.delete:*`) |
57
+ | **DIRECTORY** | `useGroups(query)` (optional: query) | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
58
+ | **DIRECTORY** | `useInvites(query)` (optional: query) | `{ invites, loading, error, refetch, resend, revoke }` | `directory.invites.*` — `users.write:*` + the SystemAcl `users.write` capability (the whole invite surface, list included). `query` is `{ status?, limit?, offset? }` with `status` ∈ `pending \| accepted \| revoked \| expired \| all` (endpoint default `all`). |
59
59
  | **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
60
- | **FILESTORE** (`ctx.filestore`) | `usePdfExport({ spaceType, folderId? })` | `{ exportToPdf, exporting, error, lastExported }` | `ctx.filestore.files.exportPdf` — `files.write:*`. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to a PDF server-side and saves it as a file (`application/pdf`); same server-side renderer on web + native. |
60
+ | **FILESTORE** (`ctx.filestore`) | `usePdfExport({ spaceType, folderId })` (optional: folderId) | `{ exportToPdf, exporting, error, lastExported }` | `ctx.filestore.files.exportPdf` — `files.write:*`. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to a PDF server-side and saves it as a file (`application/pdf`); same server-side renderer on web + native. |
61
61
  | **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser`. Rejects with `PaymentError { code, message, retryable }`; when `retryable` is `false` show `message` and drop the retry. Charges are accepted ONLY in the currency the workspace sells in — omit `currency` and the platform applies it (a disagreeing literal is a publish-blocking `payment-currency` finding). |
62
62
  | **NOTIFICATIONS** (`ctx.notifications`) | `useSendNotification()` | `{ send, sending, error }` | `ctx.notifications.send` — `notifications.send:appUser`. `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace; call from an event handler (never render); rejects with `NotificationError`. |
63
- | **IDENTIFICATION** (`ctx.identification`) | `useIdentification({ provider?, purpose?, pollIntervalMs? })` | `{ available, status, qr, autoStartToken, message, identity, identificationId, start, refresh, cancel, reset, … }` | `ctx.identification.*` — no scope (the visitor is deliberately NOT signed in). Gate the UI on `available`; `start()` opens the order and the hook polls to completion. `identity` carries `personal_number_masked` + a stable `subject_hash` — never a raw personal number. |
63
+ | **IDENTIFICATION** (`ctx.identification`) | `useIdentification({ provider, purpose, pollIntervalMs })` (optional: every key, and the whole argument) | `{ available, status, qr, autoStartToken, message, identity, identificationId, start, refresh, cancel, reset, … }` | `ctx.identification.*` — no scope (the visitor is deliberately NOT signed in). Gate the UI on `available`; `start()` opens the order and the hook polls to completion. `identity` carries `personal_number_masked` + a stable `subject_hash` — never a raw personal number. |
64
64
 
65
65
  All list calls return the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you. There is no `useWorkspace()` or `useLogger()` hook — read the theme via `useTheme()` and the locale via `useI18n()`; the host logger lives on `ctx.logger` (`{ debug, info, warn, error }`).
66
66
 
@@ -70,7 +70,29 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
70
70
 
71
71
  ## Status
72
72
 
73
- `v0.133.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
73
+ `v0.134.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
74
+
75
+ ### What's new in 0.134.0 (contract 1.103.0)
76
+
77
+ **Formatted content can carry links and tables (sc-7349).** `<MarkdownInput>` and `<RichText>` covered emphasis, headings, lists and images, but a link was not in the grammar at all — so a post that said "see the Booking page" had no way to get the reader there, and a comparison table could only be faked with spaces.
78
+
79
+ ```jsx
80
+ <MarkdownInput value={draft} onChange={setDraft} pages={pages} />
81
+ <RichText value={post.body} />
82
+ ```
83
+
84
+ - **Links: `[label](target)`.** The target is stored **verbatim** and followed through the host's `navigation.openLink`, which already decides page / external / refuse. An absolute URL leaves the app; a bare page id or slug navigates inside it; anything unfollowable (`javascript:`, a control character) is refused by that one resolver. Never pre-filter a target yourself, and never route one through `Linking.openURL` — it performs anything. A link label keeps its own emphasis, so `[**Book now**](booking)` reads bold.
85
+ - **Tables: a header row, a `| --- | --- |` divider, then body rows.** `:--` / `--:` / `:-:` in a divider cell sets that column's alignment, and `\|` puts a literal pipe in a cell. Cells are equal-width columns whose text wraps, so a wide table stays inside the widget on a phone instead of scrolling sideways.
86
+ - **Two new toolbar buttons.** Table inserts a skeleton with the first header cell selected. Link opens a form for the link text plus its target — and when you pass the new optional `pages` prop (`[{ id, name }]`) it offers those pages by name, the way `renderImage` is supplied for filestore images. Without `pages` the form offers only an external address, because a widget cannot know the app's pages on its own.
87
+ - **`<RichText>` gains `followLinks`** (default `true`). The editor's own preview passes `false`, so links render styled but inert and a tap while writing cannot navigate away from the draft.
88
+
89
+ Additive — one shared view module per primitive, bound per host, so both hosts gain this together. Every string already stored keeps parsing identically: a line needs a divider row to become a table, and `![alt](src)` is still read as an image, never a link. `CONTRACT.version` → `1.103.0`.
90
+
91
+ ### What's new in 0.133.1 (contract 1.102.1)
92
+
93
+ **Hook signatures no longer carry doc-style optional markers (sc-6946).** `CONTRACT.hooks[].signature` used to write optionality inline — `useFilestoreFiles({ spaceType, folderId?, q?, type? })` — but a `?` inside a destructuring pattern or object literal is a parse error, and the AI widget agent renders those signatures into its prompt verbatim. Every signature is now a call form that parses; the omittable parts moved to a new `optionalArgs` array the prompt prints beside it. The hook table above was rewritten to match (the version history below is left as each release wrote it).
94
+
95
+ - **Documentation only.** No hook gained, lost, or changed an argument — `useFilestoreFiles({ spaceType })` and `useDirectory()` behave exactly as before. Only the way the contract *writes down* which arguments are optional has changed.
74
96
 
75
97
  ### What's new in 0.133.0 (contract 1.102.0)
76
98
 
package/dist/contract.cjs CHANGED
@@ -793,7 +793,8 @@ const HOOKS = [
793
793
  },
794
794
  {
795
795
  name: "useAssetsByTag",
796
- signature: "useAssetsByTag(tag, { type? } = {})",
796
+ signature: "useAssetsByTag(tag, { type })",
797
+ optionalArgs: ["type"],
797
798
  description:
798
799
  "List every tenant asset carrying a given tag. Backs the Gallery " +
799
800
  "widget's tag-source mode but is a general SDK primitive — any widget " +
@@ -815,7 +816,8 @@ const HOOKS = [
815
816
  },
816
817
  {
817
818
  name: "useFilestoreFiles",
818
- signature: "useFilestoreFiles({ spaceType, folderId?, q?, type? })",
819
+ signature: "useFilestoreFiles({ spaceType, folderId, q, type })",
820
+ optionalArgs: ["folderId", "q", "type"],
819
821
  description:
820
822
  "Browse the end-user's Filestore files in a project, personal, or public " +
821
823
  "space. The hook resolves owner_id from the host context (tenant for " +
@@ -876,7 +878,8 @@ const HOOKS = [
876
878
  },
877
879
  {
878
880
  name: "useFilestoreUpload",
879
- signature: "useFilestoreUpload({ spaceType, folderId?, compress? })",
881
+ signature: "useFilestoreUpload({ spaceType, folderId, compress })",
882
+ optionalArgs: ["folderId", "compress"],
880
883
  description:
881
884
  "Upload a file into the end-user's Filestore space. The widget passes " +
882
885
  "the SPACE (`{ spaceType, folderId? }`); the hook resolves owner_id " +
@@ -914,7 +917,8 @@ const HOOKS = [
914
917
  },
915
918
  {
916
919
  name: "usePdfExport",
917
- signature: "usePdfExport({ spaceType, folderId? })",
920
+ signature: "usePdfExport({ spaceType, folderId })",
921
+ optionalArgs: ["folderId"],
918
922
  description:
919
923
  "Render an HTML string to a PDF server-side and SAVE it as a file in " +
920
924
  "the end-user's Filestore space. The widget passes the SPACE " +
@@ -939,7 +943,8 @@ const HOOKS = [
939
943
  },
940
944
  {
941
945
  name: "useFilestoreFolders",
942
- signature: "useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })",
946
+ signature: "useFilestoreFolders({ spaceType, parentFolderId, q, enabled })",
947
+ optionalArgs: ["parentFolderId", "q", "enabled"],
943
948
  description:
944
949
  "Browse the end-user's Filestore folders in a project, personal, or public " +
945
950
  "space, mirroring useFilestoreFiles for subfolder navigation. The hook " +
@@ -956,7 +961,8 @@ const HOOKS = [
956
961
  },
957
962
  {
958
963
  name: "useFileSignature",
959
- signature: "useFileSignature(fileId, existingSignatureId?)",
964
+ signature: "useFileSignature(fileId, existingSignatureId)",
965
+ optionalArgs: ["existingSignatureId"],
960
966
  description:
961
967
  "Drive a BankID signing flow for one Filestore file. initiate() opens a " +
962
968
  "sign order (the backend hashes the bytes + binds the digest), refresh() " +
@@ -1003,7 +1009,8 @@ const HOOKS = [
1003
1009
  },
1004
1010
  {
1005
1011
  name: "useFileRoster",
1006
- signature: "useFileRoster(fileId, { limit?, offset?, enabled? }?)",
1012
+ signature: "useFileRoster(fileId, { limit, offset, enabled })",
1013
+ optionalArgs: ["limit", "offset", "enabled"],
1007
1014
  description:
1008
1015
  "MANAGE-gated signer roster for one Filestore file: the people expected " +
1009
1016
  "to be able to sign it (the file's folder audience) each annotated " +
@@ -1025,7 +1032,8 @@ const HOOKS = [
1025
1032
  },
1026
1033
  {
1027
1034
  name: "useFolderPermissions",
1028
- signature: "useFolderPermissions(folderId, { enabled? }?)",
1035
+ signature: "useFolderPermissions(folderId, { enabled })",
1036
+ optionalArgs: ["enabled"],
1029
1037
  description:
1030
1038
  "Manage a folder's permissions (the Filestore ACL — folder-scoped). Lists " +
1031
1039
  "the folder's grants and exposes grant(subjectType, subjectId, permission) " +
@@ -1048,7 +1056,8 @@ const HOOKS = [
1048
1056
  },
1049
1057
  {
1050
1058
  name: "useDatastoreQuery",
1051
- signature: "useDatastoreQuery(tableId, options?)",
1059
+ signature: "useDatastoreQuery(tableId, options)",
1060
+ optionalArgs: ["options"],
1052
1061
  returnShape: {
1053
1062
  data: "Record[]",
1054
1063
  loading: "boolean",
@@ -1155,7 +1164,8 @@ const HOOKS = [
1155
1164
  },
1156
1165
  {
1157
1166
  name: "useDirectory",
1158
- signature: "useDirectory(query?)",
1167
+ signature: "useDirectory(query)",
1168
+ optionalArgs: ["query"],
1159
1169
  returnShape: {
1160
1170
  users: "Array<{ id, name, role }> // snake_case rows; unwrapped from { data, meta }",
1161
1171
  loading: "boolean",
@@ -1250,7 +1260,8 @@ const HOOKS = [
1250
1260
  // that only call read methods need only `users.read:*`.
1251
1261
  {
1252
1262
  name: "useUsers",
1253
- signature: "useUsers(query?)",
1263
+ signature: "useUsers(query)",
1264
+ optionalArgs: ["query"],
1254
1265
  description:
1255
1266
  "AppUser administration via the injected directory-client at " +
1256
1267
  "ctx.directory.users.{list,get,invite,deactivate,reactivate,sendPasswordReset}. " +
@@ -1292,7 +1303,8 @@ const HOOKS = [
1292
1303
  // affects the user's effective access.
1293
1304
  {
1294
1305
  name: "useGroups",
1295
- signature: "useGroups(query?)",
1306
+ signature: "useGroups(query)",
1307
+ optionalArgs: ["query"],
1296
1308
  description:
1297
1309
  "AppUserGroup administration via the injected directory-client at " +
1298
1310
  "ctx.directory.groups.{list,create,remove,addMember,removeMember,listMine}. " +
@@ -1322,7 +1334,8 @@ const HOOKS = [
1322
1334
  // caller's `users.write` SystemAcl capability.
1323
1335
  {
1324
1336
  name: "useInvites",
1325
- signature: "useInvites(query?)",
1337
+ signature: "useInvites(query)",
1338
+ optionalArgs: ["query"],
1326
1339
  description:
1327
1340
  "Pending AppUser invite administration via the injected " +
1328
1341
  "directory-client at ctx.directory.invites.{list,resend,revoke}. " +
@@ -1394,7 +1407,8 @@ const HOOKS = [
1394
1407
  // result. Anonymous by design (no widget scope, no session). Mirror of contract.js.
1395
1408
  {
1396
1409
  name: "useIdentification",
1397
- signature: "useIdentification(options?)",
1410
+ signature: "useIdentification(options)",
1411
+ optionalArgs: ["options"],
1398
1412
  description:
1399
1413
  "Identify a visitor who is NOT signed in via the injected " +
1400
1414
  "identification-client at ctx.identification.{available,start,get,cancel}. " +
@@ -1478,7 +1492,8 @@ const HOOKS = [
1478
1492
  // ctx.datastore.myPermissions.
1479
1493
  {
1480
1494
  name: "useCanWrite",
1481
- signature: "useCanWrite(tableId, options?)",
1495
+ signature: "useCanWrite(tableId, options)",
1496
+ optionalArgs: ["options"],
1482
1497
  description:
1483
1498
  "sc-5206 — is the signed-in caller permitted to write to tableId (or, with options.recordId, to that one row)? Returns { canWrite, loading, " +
1484
1499
  "error, refetch }, reading the injected ctx.datastore.myPermissions(tableId, { recordId }). This is a FLOOR, not a full replacement for " +
@@ -1499,7 +1514,8 @@ const HOOKS = [
1499
1514
  // REQ-RT-07 — realtime table subscription.
1500
1515
  {
1501
1516
  name: "useDatastoreSubscription",
1502
- signature: "useDatastoreSubscription(tableId, handlers, options?)",
1517
+ signature: "useDatastoreSubscription(tableId, handlers, options)",
1518
+ optionalArgs: ["options"],
1503
1519
  description:
1504
1520
  "Subscribe to a table's realtime change stream via the injected " +
1505
1521
  "datastore-client at ctx.datastore.records(tableId).subscribe(...). " +
@@ -1571,7 +1587,8 @@ const HOOKS = [
1571
1587
  // degrades to an UNSUPPORTED error rather than throwing at render.
1572
1588
  {
1573
1589
  name: "useGeolocation",
1574
- signature: "useGeolocation(options?)",
1590
+ signature: "useGeolocation(options)",
1591
+ optionalArgs: ["options"],
1575
1592
  description:
1576
1593
  "Read the device's current position. Returns { latitude, longitude, accuracy, loading, error, getCurrentPosition }. " +
1577
1594
  "Capture is IMPERATIVE — call getCurrentPosition() from a user gesture (a tap); browsers and the mobile OS gate the " +
@@ -1612,7 +1629,8 @@ const HOOKS = [
1612
1629
  // reports supported:false rather than throwing at render.
1613
1630
  {
1614
1631
  name: "useSpeechToText",
1615
- signature: "useSpeechToText(options?)",
1632
+ signature: "useSpeechToText(options)",
1633
+ optionalArgs: ["options"],
1616
1634
  description:
1617
1635
  "Dictate into text with the device's ON-DEVICE speech recogniser. Returns { transcript, partial, listening, supported, " +
1618
1636
  "error, start, stop, abort, reset }. Capture is IMPERATIVE — call start() from a user gesture (a tap on a mic button); " +
@@ -1641,7 +1659,8 @@ const HOOKS = [
1641
1659
  // the hook reports supported:false rather than throwing at render.
1642
1660
  {
1643
1661
  name: "useCamera",
1644
- signature: "useCamera(options?)",
1662
+ signature: "useCamera(options)",
1663
+ optionalArgs: ["options"],
1645
1664
  description:
1646
1665
  "Take a photo or choose one from the device library. Returns { asset, loading, error, supported, capture, pick, reset }. " +
1647
1666
  "Capture is IMPERATIVE — call capture() or pick() from a user gesture (a tap); the browser and the mobile OS gate the " +
@@ -1674,7 +1693,8 @@ const HOOKS = [
1674
1693
  // reports supported:false rather than throwing at render.
1675
1694
  {
1676
1695
  name: "useBarcodeScanner",
1677
- signature: "useBarcodeScanner(options?)",
1696
+ signature: "useBarcodeScanner(options)",
1697
+ optionalArgs: ["options"],
1678
1698
  description:
1679
1699
  "Read a barcode or QR code with the device camera. Returns { result, scanning, error, supported, scan, reset }. " +
1680
1700
  "Scanning is IMPERATIVE — call scan() from a user gesture (a tap); the browser and the mobile OS gate the camera " +
@@ -1905,7 +1925,7 @@ const PRIMITIVES = [
1905
1925
  {
1906
1926
  name: "RichText",
1907
1927
  description:
1908
- 'Formatted-content renderer. `<RichText value={record.body} />`. THE way to display text an app user wrote with formatting — bold, italic, inline code, `#`/`##`/`###` headings, `-` bullets, `1.` numbered items, and one-line `![alt](src "large")` images. `value` is markdown text, NOT HTML: widgets render through React Native primitives, which have no `dangerouslySetInnerHTML` on either host, so HTML inside a content string shows up as literal tag soup — never assemble content out of `<strong>`/`<br>` tags. Stored text that still holds legacy HTML is stripped to plain text rather than shown as markup. Props: `value` (markdown string), `renderImage` (optional `({ src, alt, size }) => node` — supply it when images are filestore ids, because resolving those needs the scopes your own widget holds; without it only absolute http(s) URLs render), `style`, `testID`. Colour, type scale and leading all come from the workspace theme — never re-style them.',
1928
+ 'Formatted-content renderer. `<RichText value={record.body} />`. THE way to display text an app user wrote with formatting — bold, italic, inline code, `#`/`##`/`###` headings, `-` bullets, `1.` numbered items, `[label](target)` links, `| a | b |` pipe tables (a header row, a `| --- | --- |` divider, then body rows; `:--` / `--:` / `:-:` in a divider cell sets that column\'s alignment), and one-line `![alt](src "large")` images. `value` is markdown text, NOT HTML: widgets render through React Native primitives, which have no `dangerouslySetInnerHTML` on either host, so HTML inside a content string shows up as literal tag soup — never assemble content out of `<strong>`/`<br>`/`<table>` tags. Stored text that still holds legacy HTML is stripped to plain text rather than shown as markup. A link is pressed through the host\'s `navigation.openLink`, which is what decides page / external / refuse — so a link target is just a string here: an absolute URL leaves the app, a bare page id or slug navigates inside it, and anything unfollowable (`javascript:`, a control character) is refused by that one resolver. Never pre-filter a target yourself and never route one through `Linking.openURL`, which performs anything. Props: `value` (markdown string), `renderImage` (optional `({ src, alt, size }) => node` — supply it when images are filestore ids, because resolving those needs the scopes your own widget holds; without it only absolute http(s) URLs render), `followLinks` (default true; pass false to render links styled but inert, which is what an editing preview wants so a tap cannot discard the draft), `style`, `testID`. Colour, type scale and leading all come from the workspace theme — never re-style them. Table cells are equal-width columns whose text wraps, so a wide table stays inside the widget on a phone instead of scrolling sideways.',
1909
1929
  rnComponent: null,
1910
1930
  docsUrl: null,
1911
1931
  },
@@ -1915,7 +1935,7 @@ const PRIMITIVES = [
1915
1935
  {
1916
1936
  name: "MarkdownInput",
1917
1937
  description:
1918
- 'Formatted-content editor. `<MarkdownInput value={draft} onChange={setDraft} />`. THE way to let an app USER write formatted text: a multi-line field, a toolbar whose buttons wrap the current SELECTION in markdown markers (bold, italic, code, H2, H3, bullets, numbered list), and a live `<RichText>` preview of the result underneath. Use it instead of a bare `TextInput` plus hand-rolled formatting buttons — buttons that splice `<strong>`/`<br>` tags into the string leave the author reading tag soup with no preview of the real output. The stored value is markdown text. Props: `value`, `onChange(next)`, `placeholder`, `previewLabel` (default "Preview"), `showPreview` (default true), `renderImage` (passed through to the preview), `minHeight` (default 150) and `maxHeight` (default 280) so the field scrolls internally instead of growing without bound, `accessibilityLabel`, `style`, `testID`.',
1938
+ 'Formatted-content editor. `<MarkdownInput value={draft} onChange={setDraft} />`. THE way to let an app USER write formatted text: a multi-line field, a toolbar whose buttons wrap the current SELECTION in markdown markers (bold, italic, code, H2, H3, bullets, numbered list), a Table button that inserts a pipe-table skeleton with the first header cell selected, a Link button that opens a form for the link text plus its target, and a live `<RichText>` preview of the result underneath. Use it instead of a bare `TextInput` plus hand-rolled formatting buttons — buttons that splice `<strong>`/`<br>`/`<table>` tags into the string leave the author reading tag soup with no preview of the real output. The stored value is markdown text. Props: `value`, `onChange(next)`, `placeholder`, `previewLabel` (default "Preview"), `showPreview` (default true), `renderImage` (passed through to the preview), `pages` (optional `[{ id, name }]` — supply it and the Link form offers those pages to link to by name, the way `renderImage` is supplied for filestore images; without it the form offers only an external address, because a widget cannot know the app\'s pages on its own), `minHeight` (default 150) and `maxHeight` (default 280) so the field scrolls internally instead of growing without bound, `accessibilityLabel`, `style`, `testID`. Links in the preview are deliberately inert, so a tap while editing cannot navigate away from the draft.',
1919
1939
  rnComponent: null,
1920
1940
  docsUrl: null,
1921
1941
  },
@@ -3851,7 +3871,32 @@ const CONTRACT = deepFreeze({
3851
3871
  // §7: a breaking change to a shipped package needs a reason, and there is
3852
3872
  // none here). Only the MANDATE to emit it is gone, from the AI widget
3853
3873
  // agent prompt and the designer skill. Minor bump: nothing removed.
3854
- version: "1.102.0",
3874
+ // 1.102.1: fix (sc-6946) — hook signatures carried doc-style OPTIONAL
3875
+ // markers (`useFilestoreFiles({ spaceType, folderId?, q?, type? })`).
3876
+ // A signature invites a literal copy, and a `?` inside a destructuring
3877
+ // pattern or object literal is a parse error — so the AI widget agent
3878
+ // prompt, which renders these verbatim, was itself teaching source that
3879
+ // fails the publish gate. Every signature is now a valid call and the
3880
+ // omittable parts moved to a new `optionalArgs` array the prompt prints
3881
+ // beside it. Documentation only; no hook changed shape or behaviour.
3882
+ // 1.103.0: additive (sc-7349) — the markdown subset behind `<RichText>` /
3883
+ // `<MarkdownInput>` gains inline `[label](target)` links and `| a | b |`
3884
+ // pipe tables, plus the two toolbar buttons that author them (Table
3885
+ // inserts a skeleton; Link opens a form for the text and its target, and
3886
+ // offers the app's pages by name when the host passes the new optional
3887
+ // `pages` prop). Formatted content could not carry a link AT ALL, so a
3888
+ // post saying "see the Booking page" had no way to get the reader there.
3889
+ // A target is stored VERBATIM and followed through the host's existing
3890
+ // `navigation.openLink`, which already decides page / external / refuse —
3891
+ // no second resolver, and no way for a stored `javascript:` to be
3892
+ // performed. `<RichText>` also gains `followLinks` (default true) so the
3893
+ // editor's own preview renders links styled but inert. Both primitives
3894
+ // stay ONE shared view module bound per host, so this is full parity, not
3895
+ // a §8 native-only case. Every string already stored keeps parsing
3896
+ // identically: a line needs a `| --- |` divider to become a table, and an
3897
+ // `![alt](src)` image is still read as an image, never a link. Minor bump
3898
+ // on the pre-1.0 channel.
3899
+ version: "1.103.0",
3855
3900
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3856
3901
  hooks: HOOKS,
3857
3902
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -793,7 +793,8 @@ const HOOKS = [
793
793
  },
794
794
  {
795
795
  name: "useAssetsByTag",
796
- signature: "useAssetsByTag(tag, { type? } = {})",
796
+ signature: "useAssetsByTag(tag, { type })",
797
+ optionalArgs: ["type"],
797
798
  description:
798
799
  "List every tenant asset carrying a given tag. Backs the Gallery " +
799
800
  "widget's tag-source mode but is a general SDK primitive — any widget " +
@@ -815,7 +816,8 @@ const HOOKS = [
815
816
  },
816
817
  {
817
818
  name: "useFilestoreFiles",
818
- signature: "useFilestoreFiles({ spaceType, folderId?, q?, type? })",
819
+ signature: "useFilestoreFiles({ spaceType, folderId, q, type })",
820
+ optionalArgs: ["folderId", "q", "type"],
819
821
  description:
820
822
  "Browse the end-user's Filestore files in a project, personal, or public " +
821
823
  "space. The hook resolves owner_id from the host context (tenant for " +
@@ -876,7 +878,8 @@ const HOOKS = [
876
878
  },
877
879
  {
878
880
  name: "useFilestoreUpload",
879
- signature: "useFilestoreUpload({ spaceType, folderId?, compress? })",
881
+ signature: "useFilestoreUpload({ spaceType, folderId, compress })",
882
+ optionalArgs: ["folderId", "compress"],
880
883
  description:
881
884
  "Upload a file into the end-user's Filestore space. The widget passes " +
882
885
  "the SPACE (`{ spaceType, folderId? }`); the hook resolves owner_id " +
@@ -914,7 +917,8 @@ const HOOKS = [
914
917
  },
915
918
  {
916
919
  name: "usePdfExport",
917
- signature: "usePdfExport({ spaceType, folderId? })",
920
+ signature: "usePdfExport({ spaceType, folderId })",
921
+ optionalArgs: ["folderId"],
918
922
  description:
919
923
  "Render an HTML string to a PDF server-side and SAVE it as a file in " +
920
924
  "the end-user's Filestore space. The widget passes the SPACE " +
@@ -939,7 +943,8 @@ const HOOKS = [
939
943
  },
940
944
  {
941
945
  name: "useFilestoreFolders",
942
- signature: "useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })",
946
+ signature: "useFilestoreFolders({ spaceType, parentFolderId, q, enabled })",
947
+ optionalArgs: ["parentFolderId", "q", "enabled"],
943
948
  description:
944
949
  "Browse the end-user's Filestore folders in a project, personal, or public " +
945
950
  "space, mirroring useFilestoreFiles for subfolder navigation. The hook " +
@@ -956,7 +961,8 @@ const HOOKS = [
956
961
  },
957
962
  {
958
963
  name: "useFileSignature",
959
- signature: "useFileSignature(fileId, existingSignatureId?)",
964
+ signature: "useFileSignature(fileId, existingSignatureId)",
965
+ optionalArgs: ["existingSignatureId"],
960
966
  description:
961
967
  "Drive a BankID signing flow for one Filestore file. initiate() opens a " +
962
968
  "sign order (the backend hashes the bytes + binds the digest), refresh() " +
@@ -1003,7 +1009,8 @@ const HOOKS = [
1003
1009
  },
1004
1010
  {
1005
1011
  name: "useFileRoster",
1006
- signature: "useFileRoster(fileId, { limit?, offset?, enabled? }?)",
1012
+ signature: "useFileRoster(fileId, { limit, offset, enabled })",
1013
+ optionalArgs: ["limit", "offset", "enabled"],
1007
1014
  description:
1008
1015
  "MANAGE-gated signer roster for one Filestore file: the people expected " +
1009
1016
  "to be able to sign it (the file's folder audience) each annotated " +
@@ -1025,7 +1032,8 @@ const HOOKS = [
1025
1032
  },
1026
1033
  {
1027
1034
  name: "useFolderPermissions",
1028
- signature: "useFolderPermissions(folderId, { enabled? }?)",
1035
+ signature: "useFolderPermissions(folderId, { enabled })",
1036
+ optionalArgs: ["enabled"],
1029
1037
  description:
1030
1038
  "Manage a folder's permissions (the Filestore ACL — folder-scoped). Lists " +
1031
1039
  "the folder's grants and exposes grant(subjectType, subjectId, permission) " +
@@ -1048,7 +1056,8 @@ const HOOKS = [
1048
1056
  },
1049
1057
  {
1050
1058
  name: "useDatastoreQuery",
1051
- signature: "useDatastoreQuery(tableId, options?)",
1059
+ signature: "useDatastoreQuery(tableId, options)",
1060
+ optionalArgs: ["options"],
1052
1061
  returnShape: {
1053
1062
  data: "Record[]",
1054
1063
  loading: "boolean",
@@ -1155,7 +1164,8 @@ const HOOKS = [
1155
1164
  },
1156
1165
  {
1157
1166
  name: "useDirectory",
1158
- signature: "useDirectory(query?)",
1167
+ signature: "useDirectory(query)",
1168
+ optionalArgs: ["query"],
1159
1169
  returnShape: {
1160
1170
  users: "Array<{ id, name, role }> // snake_case rows; unwrapped from { data, meta }",
1161
1171
  loading: "boolean",
@@ -1250,7 +1260,8 @@ const HOOKS = [
1250
1260
  // that only call read methods need only `users.read:*`.
1251
1261
  {
1252
1262
  name: "useUsers",
1253
- signature: "useUsers(query?)",
1263
+ signature: "useUsers(query)",
1264
+ optionalArgs: ["query"],
1254
1265
  description:
1255
1266
  "AppUser administration via the injected directory-client at " +
1256
1267
  "ctx.directory.users.{list,get,invite,deactivate,reactivate,sendPasswordReset}. " +
@@ -1292,7 +1303,8 @@ const HOOKS = [
1292
1303
  // affects the user's effective access.
1293
1304
  {
1294
1305
  name: "useGroups",
1295
- signature: "useGroups(query?)",
1306
+ signature: "useGroups(query)",
1307
+ optionalArgs: ["query"],
1296
1308
  description:
1297
1309
  "AppUserGroup administration via the injected directory-client at " +
1298
1310
  "ctx.directory.groups.{list,create,remove,addMember,removeMember,listMine}. " +
@@ -1322,7 +1334,8 @@ const HOOKS = [
1322
1334
  // caller's `users.write` SystemAcl capability.
1323
1335
  {
1324
1336
  name: "useInvites",
1325
- signature: "useInvites(query?)",
1337
+ signature: "useInvites(query)",
1338
+ optionalArgs: ["query"],
1326
1339
  description:
1327
1340
  "Pending AppUser invite administration via the injected " +
1328
1341
  "directory-client at ctx.directory.invites.{list,resend,revoke}. " +
@@ -1394,7 +1407,8 @@ const HOOKS = [
1394
1407
  // result. Anonymous by design (no widget scope, no session). Mirror of contract.cjs.
1395
1408
  {
1396
1409
  name: "useIdentification",
1397
- signature: "useIdentification(options?)",
1410
+ signature: "useIdentification(options)",
1411
+ optionalArgs: ["options"],
1398
1412
  description:
1399
1413
  "Identify a visitor who is NOT signed in via the injected " +
1400
1414
  "identification-client at ctx.identification.{available,start,get,cancel}. " +
@@ -1478,7 +1492,8 @@ const HOOKS = [
1478
1492
  // ctx.datastore.myPermissions.
1479
1493
  {
1480
1494
  name: "useCanWrite",
1481
- signature: "useCanWrite(tableId, options?)",
1495
+ signature: "useCanWrite(tableId, options)",
1496
+ optionalArgs: ["options"],
1482
1497
  description:
1483
1498
  "sc-5206 — is the signed-in caller permitted to write to tableId (or, with options.recordId, to that one row)? Returns { canWrite, loading, " +
1484
1499
  "error, refetch }, reading the injected ctx.datastore.myPermissions(tableId, { recordId }). This is a FLOOR, not a full replacement for " +
@@ -1499,7 +1514,8 @@ const HOOKS = [
1499
1514
  // REQ-RT-07 — realtime table subscription.
1500
1515
  {
1501
1516
  name: "useDatastoreSubscription",
1502
- signature: "useDatastoreSubscription(tableId, handlers, options?)",
1517
+ signature: "useDatastoreSubscription(tableId, handlers, options)",
1518
+ optionalArgs: ["options"],
1503
1519
  description:
1504
1520
  "Subscribe to a table's realtime change stream via the injected " +
1505
1521
  "datastore-client at ctx.datastore.records(tableId).subscribe(...). " +
@@ -1571,7 +1587,8 @@ const HOOKS = [
1571
1587
  // degrades to an UNSUPPORTED error rather than throwing at render.
1572
1588
  {
1573
1589
  name: "useGeolocation",
1574
- signature: "useGeolocation(options?)",
1590
+ signature: "useGeolocation(options)",
1591
+ optionalArgs: ["options"],
1575
1592
  description:
1576
1593
  "Read the device's current position. Returns { latitude, longitude, accuracy, loading, error, getCurrentPosition }. " +
1577
1594
  "Capture is IMPERATIVE — call getCurrentPosition() from a user gesture (a tap); browsers and the mobile OS gate the " +
@@ -1612,7 +1629,8 @@ const HOOKS = [
1612
1629
  // reports supported:false rather than throwing at render.
1613
1630
  {
1614
1631
  name: "useSpeechToText",
1615
- signature: "useSpeechToText(options?)",
1632
+ signature: "useSpeechToText(options)",
1633
+ optionalArgs: ["options"],
1616
1634
  description:
1617
1635
  "Dictate into text with the device's ON-DEVICE speech recogniser. Returns { transcript, partial, listening, supported, " +
1618
1636
  "error, start, stop, abort, reset }. Capture is IMPERATIVE — call start() from a user gesture (a tap on a mic button); " +
@@ -1641,7 +1659,8 @@ const HOOKS = [
1641
1659
  // the hook reports supported:false rather than throwing at render.
1642
1660
  {
1643
1661
  name: "useCamera",
1644
- signature: "useCamera(options?)",
1662
+ signature: "useCamera(options)",
1663
+ optionalArgs: ["options"],
1645
1664
  description:
1646
1665
  "Take a photo or choose one from the device library. Returns { asset, loading, error, supported, capture, pick, reset }. " +
1647
1666
  "Capture is IMPERATIVE — call capture() or pick() from a user gesture (a tap); the browser and the mobile OS gate the " +
@@ -1674,7 +1693,8 @@ const HOOKS = [
1674
1693
  // reports supported:false rather than throwing at render.
1675
1694
  {
1676
1695
  name: "useBarcodeScanner",
1677
- signature: "useBarcodeScanner(options?)",
1696
+ signature: "useBarcodeScanner(options)",
1697
+ optionalArgs: ["options"],
1678
1698
  description:
1679
1699
  "Read a barcode or QR code with the device camera. Returns { result, scanning, error, supported, scan, reset }. " +
1680
1700
  "Scanning is IMPERATIVE — call scan() from a user gesture (a tap); the browser and the mobile OS gate the camera " +
@@ -1905,7 +1925,7 @@ const PRIMITIVES = [
1905
1925
  {
1906
1926
  name: "RichText",
1907
1927
  description:
1908
- 'Formatted-content renderer. `<RichText value={record.body} />`. THE way to display text an app user wrote with formatting — bold, italic, inline code, `#`/`##`/`###` headings, `-` bullets, `1.` numbered items, and one-line `![alt](src "large")` images. `value` is markdown text, NOT HTML: widgets render through React Native primitives, which have no `dangerouslySetInnerHTML` on either host, so HTML inside a content string shows up as literal tag soup — never assemble content out of `<strong>`/`<br>` tags. Stored text that still holds legacy HTML is stripped to plain text rather than shown as markup. Props: `value` (markdown string), `renderImage` (optional `({ src, alt, size }) => node` — supply it when images are filestore ids, because resolving those needs the scopes your own widget holds; without it only absolute http(s) URLs render), `style`, `testID`. Colour, type scale and leading all come from the workspace theme — never re-style them.',
1928
+ 'Formatted-content renderer. `<RichText value={record.body} />`. THE way to display text an app user wrote with formatting — bold, italic, inline code, `#`/`##`/`###` headings, `-` bullets, `1.` numbered items, `[label](target)` links, `| a | b |` pipe tables (a header row, a `| --- | --- |` divider, then body rows; `:--` / `--:` / `:-:` in a divider cell sets that column\'s alignment), and one-line `![alt](src "large")` images. `value` is markdown text, NOT HTML: widgets render through React Native primitives, which have no `dangerouslySetInnerHTML` on either host, so HTML inside a content string shows up as literal tag soup — never assemble content out of `<strong>`/`<br>`/`<table>` tags. Stored text that still holds legacy HTML is stripped to plain text rather than shown as markup. A link is pressed through the host\'s `navigation.openLink`, which is what decides page / external / refuse — so a link target is just a string here: an absolute URL leaves the app, a bare page id or slug navigates inside it, and anything unfollowable (`javascript:`, a control character) is refused by that one resolver. Never pre-filter a target yourself and never route one through `Linking.openURL`, which performs anything. Props: `value` (markdown string), `renderImage` (optional `({ src, alt, size }) => node` — supply it when images are filestore ids, because resolving those needs the scopes your own widget holds; without it only absolute http(s) URLs render), `followLinks` (default true; pass false to render links styled but inert, which is what an editing preview wants so a tap cannot discard the draft), `style`, `testID`. Colour, type scale and leading all come from the workspace theme — never re-style them. Table cells are equal-width columns whose text wraps, so a wide table stays inside the widget on a phone instead of scrolling sideways.',
1909
1929
  rnComponent: null,
1910
1930
  docsUrl: null,
1911
1931
  },
@@ -1915,7 +1935,7 @@ const PRIMITIVES = [
1915
1935
  {
1916
1936
  name: "MarkdownInput",
1917
1937
  description:
1918
- 'Formatted-content editor. `<MarkdownInput value={draft} onChange={setDraft} />`. THE way to let an app USER write formatted text: a multi-line field, a toolbar whose buttons wrap the current SELECTION in markdown markers (bold, italic, code, H2, H3, bullets, numbered list), and a live `<RichText>` preview of the result underneath. Use it instead of a bare `TextInput` plus hand-rolled formatting buttons — buttons that splice `<strong>`/`<br>` tags into the string leave the author reading tag soup with no preview of the real output. The stored value is markdown text. Props: `value`, `onChange(next)`, `placeholder`, `previewLabel` (default "Preview"), `showPreview` (default true), `renderImage` (passed through to the preview), `minHeight` (default 150) and `maxHeight` (default 280) so the field scrolls internally instead of growing without bound, `accessibilityLabel`, `style`, `testID`.',
1938
+ 'Formatted-content editor. `<MarkdownInput value={draft} onChange={setDraft} />`. THE way to let an app USER write formatted text: a multi-line field, a toolbar whose buttons wrap the current SELECTION in markdown markers (bold, italic, code, H2, H3, bullets, numbered list), a Table button that inserts a pipe-table skeleton with the first header cell selected, a Link button that opens a form for the link text plus its target, and a live `<RichText>` preview of the result underneath. Use it instead of a bare `TextInput` plus hand-rolled formatting buttons — buttons that splice `<strong>`/`<br>`/`<table>` tags into the string leave the author reading tag soup with no preview of the real output. The stored value is markdown text. Props: `value`, `onChange(next)`, `placeholder`, `previewLabel` (default "Preview"), `showPreview` (default true), `renderImage` (passed through to the preview), `pages` (optional `[{ id, name }]` — supply it and the Link form offers those pages to link to by name, the way `renderImage` is supplied for filestore images; without it the form offers only an external address, because a widget cannot know the app\'s pages on its own), `minHeight` (default 150) and `maxHeight` (default 280) so the field scrolls internally instead of growing without bound, `accessibilityLabel`, `style`, `testID`. Links in the preview are deliberately inert, so a tap while editing cannot navigate away from the draft.',
1919
1939
  rnComponent: null,
1920
1940
  docsUrl: null,
1921
1941
  },
@@ -3851,7 +3871,32 @@ const CONTRACT = deepFreeze({
3851
3871
  // §7: a breaking change to a shipped package needs a reason, and there is
3852
3872
  // none here). Only the MANDATE to emit it is gone, from the AI widget
3853
3873
  // agent prompt and the designer skill. Minor bump: nothing removed.
3854
- version: "1.102.0",
3874
+ // 1.102.1: fix (sc-6946) — hook signatures carried doc-style OPTIONAL
3875
+ // markers (`useFilestoreFiles({ spaceType, folderId?, q?, type? })`).
3876
+ // A signature invites a literal copy, and a `?` inside a destructuring
3877
+ // pattern or object literal is a parse error — so the AI widget agent
3878
+ // prompt, which renders these verbatim, was itself teaching source that
3879
+ // fails the publish gate. Every signature is now a valid call and the
3880
+ // omittable parts moved to a new `optionalArgs` array the prompt prints
3881
+ // beside it. Documentation only; no hook changed shape or behaviour.
3882
+ // 1.103.0: additive (sc-7349) — the markdown subset behind `<RichText>` /
3883
+ // `<MarkdownInput>` gains inline `[label](target)` links and `| a | b |`
3884
+ // pipe tables, plus the two toolbar buttons that author them (Table
3885
+ // inserts a skeleton; Link opens a form for the text and its target, and
3886
+ // offers the app's pages by name when the host passes the new optional
3887
+ // `pages` prop). Formatted content could not carry a link AT ALL, so a
3888
+ // post saying "see the Booking page" had no way to get the reader there.
3889
+ // A target is stored VERBATIM and followed through the host's existing
3890
+ // `navigation.openLink`, which already decides page / external / refuse —
3891
+ // no second resolver, and no way for a stored `javascript:` to be
3892
+ // performed. `<RichText>` also gains `followLinks` (default true) so the
3893
+ // editor's own preview renders links styled but inert. Both primitives
3894
+ // stay ONE shared view module bound per host, so this is full parity, not
3895
+ // a §8 native-only case. Every string already stored keeps parsing
3896
+ // identically: a line needs a `| --- |` divider to become a table, and an
3897
+ // `![alt](src)` image is still read as an image, never a link. Minor bump
3898
+ // on the pre-1.0 channel.
3899
+ version: "1.103.0",
3855
3900
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3856
3901
  hooks: HOOKS,
3857
3902
  primitives: PRIMITIVES,