@colixsystems/widget-sdk 0.133.0 → 0.133.1

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,13 @@ 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.133.1` — 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.133.1 (contract 1.102.1)
76
+
77
+ **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).
78
+
79
+ - **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
80
 
75
81
  ### What's new in 0.133.0 (contract 1.102.0)
76
82
 
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 " +
@@ -3851,7 +3871,15 @@ 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
+ version: "1.102.1",
3855
3883
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3856
3884
  hooks: HOOKS,
3857
3885
  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 " +
@@ -3851,7 +3871,15 @@ 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
+ version: "1.102.1",
3855
3883
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3856
3884
  hooks: HOOKS,
3857
3885
  primitives: PRIMITIVES,
package/dist/index.d.ts CHANGED
@@ -2600,6 +2600,8 @@ export function lintSource(
2600
2600
  export interface ContractHookEntry {
2601
2601
  name: string;
2602
2602
  signature: string;
2603
+ /** Arguments and option keys in `signature` that may be omitted (sc-6946). */
2604
+ optionalArgs?: string[];
2603
2605
  returnShape: Record<string, string>;
2604
2606
  requiredContextSlice: string[];
2605
2607
  scopes: string[] | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.133.0",
3
+ "version": "0.133.1",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",