@colixsystems/widget-sdk 0.150.0 → 0.150.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
@@ -72,9 +72,17 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
72
72
 
73
73
  ## Status
74
74
 
75
- `v0.149.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**.
75
+ `v0.150.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**.
76
76
 
77
- ### What's new in 0.149.0 (contract 1.117.0)
77
+ ### What's new in 0.150.1 (contract 1.118.0 — unchanged)
78
+
79
+ **A capability's fallback belongs to the web build, not to native (sc-8381).** The barcode guidance here, in the in-Studio hooks reference and in both AI agents' prompts told you to gate the scan button on `supported` **and** keep a manual-entry path — unconditionally, while the reason given was a browser-only gap. `supported` is always `true` on the Expo export, so that read shipped a redundant "or type the code" field on every phone: the exact typing the scanner exists to remove. The rule is now scoped to the host that actually fails the gate.
80
+
81
+ - **Gate the substitute on the same flag that gates the button.** Where `supported` is `false` — the web Player in Safari and Firefox, which ship no `BarcodeDetector` — render the typed field instead of the scan button. Where it is `true`, the capability is the whole input. Nothing about the API changed; only the guidance did.
82
+ - **A native `catch` is an error state, not a web substitute.** The `react-native-nfc-manager` notes said to render "the same fallback from the `catch` that `widget.web.jsx` uses". A workspace that has not switched the NFC device capability on in Publish settings is a **misconfiguration** the widget should name, not a reason to park a permanent manual-entry field beside the tap target.
83
+ - **The web half is still required.** This narrows where a *substitute* belongs; it does not relax the parity rule. A native-only capability still ships a real way in on `widget.web.jsx` — a typed code, a QR scan, a non-sensor path — and still degrades visibly rather than rendering blank.
84
+
85
+ ### What's new in 0.150.0 (contract 1.118.0)
78
86
 
79
87
  **New primitive `<EmojiPicker>` — pick an emoji without leaving for the OS keyboard (sc-8104).** Typed emoji always worked: a plain `<TextInput>` stores and renders them on both hosts, and nothing about that changes. What was missing was an in-app way to *choose* one — for a message composer, or to react to something.
80
88
 
@@ -99,6 +107,10 @@ It presents inside `<Overlay>`, so it escapes the widget's clipping box on both
99
107
 
100
108
  **Also new: a shared reaction model.** `groupReactions(rows, { typeField, userField, userId })` turns reaction rows into per-emoji pill groups — one row per (target, user, emoji), de-duplicated per user — and `readReactionEmoji(value)` maps a stored value to its emoji, ignoring anything it does not recognise. Widgets storing reactions should group through these rather than rolling their own, so pills behave identically wherever they appear.
101
109
 
110
+ ### What's new in 0.149.0 (contract 1.117.0)
111
+
112
+ **Six text roles on the theme, so every widget speaks one typographic voice (sc-8058).** See the theme section above for the roles and how to read them.
113
+
102
114
  ### What's new in 0.148.0 (contract 1.116.0)
103
115
 
104
116
  **New `useAgent(agentRef)` hook — a widget can talk to the WORKSPACE's own AI agent (sc-7991).** A new `agents` host client (`@colixsystems/agents-client`), a new `agentRef` propertySchema type and a new `ai.invoke:*` scope. Everything is additive; nothing existing changes shape.
@@ -299,7 +311,7 @@ Also tightened in this release: the `publicGrant` flags are now type-checked. Th
299
311
  **NFC tags are vetted, native-only (`react-native-nfc-manager`).** A widget can now read and write NFC tags on the Expo export — NDEF over `NfcManager.requestTechnology(NfcTech.Ndef)`, plus the raw technologies a badge, asset tag or transit card uses: `NfcTech.IsoDep` on both hosts, `NfcTech.MifareClassic` on Android, `NfcTech.MifareIOS` / `FelicaIOS` / `Iso15693IOS` on iOS. (There is no `NfcTech.FeliCa` — check `index.d.ts` before naming one.) This is the last common "tap a physical thing" input the allowlist was missing; barcode and QR already had `useBarcodeScanner()`.
300
312
 
301
313
  - **Native-only, and the browser is why.** Web NFC ships in Chrome on Android alone — Safari, Firefox and every desktop browser omit it — so there is no web build to pair with. Put the import in `widget.native.jsx` and give `widget.web.jsx` a real way in: a typed code field, or the SDK's `useBarcodeScanner()` QR path. The linter's `import-platform-mismatch` enforces the split.
302
- - **Opt-in per workspace — and `isSupported()` cannot see that.** The export claims the iOS NFC entitlement and the Android `NFC` permission only when the publisher enables the **NFC tags** device capability in Publish settings. `isSupported()` probes the **device** (`readingAvailable` on iOS, `hasSystemFeature` on Android), not the gate: on a capable phone in an app that never asked, it returns `true` and the read then fails — Android throws a `SecurityException` from `registerTagEvent`, iOS cannot open the reader session. So wrap `start()` / `requestTechnology()` in a `try` and render the **same** fallback from the `catch` that `widget.web.jsx` uses. That `catch` is the degradation path.
314
+ - **Opt-in per workspace — and `isSupported()` cannot see that.** The export claims the iOS NFC entitlement and the Android `NFC` permission only when the publisher enables the **NFC tags** device capability in Publish settings. `isSupported()` probes the **device** (`readingAvailable` on iOS, `hasSystemFeature` on Android), not the gate: on a capable phone in an app that never asked, it returns `true` and the read then fails — Android throws a `SecurityException` from `registerTagEvent`, iOS cannot open the reader session. So wrap `start()` / `requestTechnology()` in a `try` and render a clear **error state** from the `catch` — name what is missing and say the capability has to be switched on in Publish settings. That `catch` is an error path, **not** the web build's typed-code substitute: do not give `widget.native.jsx` a permanent manual-entry field beside the tap target, because in a correctly-configured app the radio works and the field is the typing the tag was meant to replace.
303
315
  - **Clean up the session.** `cancelTechnologyRequest()` in your effect cleanup; an abandoned request leaves the reader sheet up on iOS.
304
316
  - **It is a write capability too.** Read with `getNdefMessage()`; write with `writeNdefMessage(bytes)`, `formatNdef(bytes)` (Android only, for a blank tag) and `transceive(bytes)` on the `IsoDep` / `NfcA` / `NfcV` handlers. One capability covers both — Android expresses read and write through the single `android.permission.NFC` — so the workspace owner who enables it is consenting to every widget in the workspace being able to rewrite the tags their users tap. Raw ISO7816 `transceive` on iOS additionally needs `select-identifiers` on the entitlement, which the export does not declare; NDEF read and write work on both hosts.
305
317
  - **`makeReadOnly()` is irreversible.** It permanently locks the tag — no later write will ever succeed, on any device, by any app. Call it only where the feature genuinely wants a one-time-programmable tag, and never as a cleanup step after writing.
@@ -378,13 +390,19 @@ const hit = await scan(); // { value, format } | null
378
390
 
379
391
  It is deliberately **one-shot** rather than a start/stop subscription — to read several codes, call `scan()` again. There is no loop to leave running and no camera to forget to release.
380
392
 
381
- **Gate the button on `supported`, and always keep a manual-entry path.** `BarcodeDetector` is Chromium-only today, so the web Player reports `supported: false` in Safari and Firefox. That is a genuine browser gap, not a missing feature, so a scanner must never be the only way to enter a code:
393
+ **Gate the button on `supported`, and keep the manual-entry path for the host that fails that gate.** `BarcodeDetector` is Chromium-only today, so the web Player reports `supported: false` in Safari and Firefox — a genuine browser gap, not a missing feature, and there the scanner must not be the only way to enter a code. The Expo export always reports `true`, so do **not** mirror the typed field onto native: on the phone the scanner is the whole input, and the twin field is the typing the scan was meant to remove. Gate the substitute on the same flag that gates the button:
382
394
 
383
395
  ```jsx
384
- <TextInput value={code} onChangeText={setCode} />
385
- {supported ? <Pressable onPress={scan}><Text>Scan</Text></Pressable> : null}
396
+ {supported
397
+ ? <Pressable onPress={scan}><Text>Scan</Text></Pressable>
398
+ : <TextInput value={code} onChangeText={setCode} placeholder="Enter the code" />}
399
+ {/* On the phone there is no typed field to fall back to, so a denied
400
+ camera has to say so rather than leave a button that does nothing. */}
401
+ {error ? <Text>Camera access is off — turn it on in Settings to scan.</Text> : null}
386
402
  ```
387
403
 
404
+ That error line is not optional on native. `supported` is `true` there, so the scan button is the only way in — a `PERMISSION_DENIED` the widget swallows leaves the user tapping a control that silently does nothing.
405
+
388
406
  **Host-brokered, not a vetted import.** Like the camera and speech-to-text, your widget never imports the native module: `expo-camera` is pinned by the Expo export only, so a widget that does not scan gains no native dependency and nothing new enters widget bundles.
389
407
 
390
408
  ### What's new in 0.128.0 (contract 1.99.0 — unchanged)
package/dist/contract.cjs CHANGED
@@ -1860,9 +1860,12 @@ const HOOKS = [
1860
1860
  "PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `value` is the decoded text; `format` is a lowercase symbology name " +
1861
1861
  "(qr_code, code_128, ean_13, …) — treat it as a hint, not a promise, because the two hosts detect different sets. " +
1862
1862
  "It is deliberately ONE-SHOT rather than a start/stop subscription: to scan several codes, call scan() again. " +
1863
- "reset() clears the last result and error. ALWAYS check `supported` before rendering a scan button and give the " +
1864
- "widget a manual-entry path — the web Player reports false wherever the browser has no BarcodeDetector (Safari and " +
1865
- "Firefox today), which is a real browser gap, not a missing feature. options: { formats } is a HINT narrowing the " +
1863
+ "reset() clears the last result and error. ALWAYS check `supported` before rendering a scan button, and give the " +
1864
+ "widget a manual-entry path WHERE THAT GUARD FAILS — which is the WEB Player, wherever the browser ships no " +
1865
+ "BarcodeDetector (Safari and Firefox today), a real browser gap rather than a missing feature. The Expo export " +
1866
+ "ALWAYS reports true, so do NOT pair the scan button with a typed-code twin there: on the phone the scanner is the " +
1867
+ "whole input, and a redundant 'or type it in' field is the manual step the capability exists to remove. " +
1868
+ "options: { formats } is a HINT narrowing the " +
1866
1869
  "symbologies to look for. The Expo export scans via expo-camera; the web Player via BarcodeDetector over the same " +
1867
1870
  "getUserMedia preview useCamera() uses. The camera is released on every exit path, including a dismissal.",
1868
1871
  returnShape: {
@@ -2634,7 +2637,8 @@ const WIDGET_CONTEXT_SHAPE = {
2634
2637
  "BarcodeError (.code PERMISSION_DENIED | UNSUPPORTED | INTERNAL). Its formats hint and its format output BOTH speak " +
2635
2638
  "BarcodeDetector names (qr_code, code_128, ean_13, ...) on both hosts; the Expo export maps them to expo-camera own " +
2636
2639
  "vocabulary internally and DROPS a name it cannot map. isSupported() is false wherever the browser ships no " +
2637
- "BarcodeDetector (Safari, Firefox), so a widget must always offer manual entry beside a scan button. " +
2640
+ "BarcodeDetector (Safari, Firefox), so a WEB build must offer manual entry beside a scan button; the Expo export " +
2641
+ "always reports true and takes the scanner alone — no typed-code twin on native. " +
2638
2642
  "sc-6450 — the geolocation background-watch members are NATIVE-ONLY and opt-in per app: the web Player and an export that " +
2639
2643
  "did not opt in both report isBackgroundSupported() false, and the Expo export backs it with expo-location + " +
2640
2644
  "expo-task-manager. subscribeBackgroundPositions and subscribeBackgroundWatchState are plain subscriptions — they start no " +
@@ -2964,7 +2968,7 @@ const VETTED_IMPORTS = [
2964
2968
  platforms: ["native"],
2965
2969
  category: "system",
2966
2970
  description:
2967
- "Read and write NFC tags on the Expo export: NDEF records via NfcManager.requestTechnology(NfcTech.Ndef), plus the raw technologies a badge, asset tag or transit card uses - NfcTech.IsoDep on both hosts, NfcTech.MifareClassic on Android, NfcTech.MifareIOS / FelicaIOS / Iso15693IOS on iOS (there is no NfcTech.FeliCa; check index.d.ts before naming one). Read with ndefHandler.getNdefMessage(); WRITE with writeNdefMessage(bytes), formatNdef(bytes) (Android only, for a blank tag) and transceive(bytes) on the IsoDep / NfcA / NfcV handlers. makeReadOnly() PERMANENTLY locks a tag - it cannot be undone and no later write will ever succeed, so call it only where the brief actually asks for a one-time-programmable tag, never as a tidy-up step. Raw ISO7816 transceive on iOS additionally needs select-identifiers on the entitlement, which this export does not declare - NDEF read and write work on both hosts. Always cancelTechnologyRequest() in a cleanup - an abandoned session leaves the reader sheet up on iOS. Native-only because no browser hands the Player a reader: Web NFC exists in Chrome on Android alone (Safari, Firefox and every desktop browser omit it), so there is no web build to pair with. Author it in widget.native.jsx and give widget.web.jsx a real way in instead - a typed code field, or the SDK's useBarcodeScanner() QR path. The radio is ALSO opt-in per workspace: the export claims the iOS NFC entitlement and the Android NFC permission only when the publisher enables the 'nfc' device capability in Publish settings. isSupported() does NOT see that gate - it probes the DEVICE (readingAvailable on iOS, hasSystemFeature on Android), so on a capable phone in an app that never asked it returns true and the read then fails: Android throws a SecurityException from registerTagEvent, iOS cannot open the reader session. So wrap start() / requestTechnology() in a try and render the SAME fallback from the catch that widget.web.jsx uses - that catch IS the degradation path, not a nicety. Pinned at 3.17.2, the current stable release (4.x is still beta); a community package, so Expo bundledNativeModules has no version opinion on it.",
2971
+ "Read and write NFC tags on the Expo export: NDEF records via NfcManager.requestTechnology(NfcTech.Ndef), plus the raw technologies a badge, asset tag or transit card uses - NfcTech.IsoDep on both hosts, NfcTech.MifareClassic on Android, NfcTech.MifareIOS / FelicaIOS / Iso15693IOS on iOS (there is no NfcTech.FeliCa; check index.d.ts before naming one). Read with ndefHandler.getNdefMessage(); WRITE with writeNdefMessage(bytes), formatNdef(bytes) (Android only, for a blank tag) and transceive(bytes) on the IsoDep / NfcA / NfcV handlers. makeReadOnly() PERMANENTLY locks a tag - it cannot be undone and no later write will ever succeed, so call it only where the brief actually asks for a one-time-programmable tag, never as a tidy-up step. Raw ISO7816 transceive on iOS additionally needs select-identifiers on the entitlement, which this export does not declare - NDEF read and write work on both hosts. Always cancelTechnologyRequest() in a cleanup - an abandoned session leaves the reader sheet up on iOS. Native-only because no browser hands the Player a reader: Web NFC exists in Chrome on Android alone (Safari, Firefox and every desktop browser omit it), so there is no web build to pair with. Author it in widget.native.jsx and give widget.web.jsx a real way in instead - a typed code field, or the SDK's useBarcodeScanner() QR path. The radio is ALSO opt-in per workspace: the export claims the iOS NFC entitlement and the Android NFC permission only when the publisher enables the 'nfc' device capability in Publish settings. isSupported() does NOT see that gate - it probes the DEVICE (readingAvailable on iOS, hasSystemFeature on Android), so on a capable phone in an app that never asked it returns true and the read then fails: Android throws a SecurityException from registerTagEvent, iOS cannot open the reader session. So wrap start() / requestTechnology() in a try and render a clear ERROR STATE from the catch - name what is missing and say the maker must switch 'nfc' on in Publish settings. That catch is an ERROR path, NOT the web build's typed-code substitute: do not give widget.native.jsx a permanent manual-entry field beside the tap target, because in a correctly-configured app the radio works and the field is the typing the tag was meant to replace. Pinned at 3.17.2, the current stable release (4.x is still beta); a community package, so Expo bundledNativeModules has no version opinion on it.",
2968
2972
  },
2969
2973
  {
2970
2974
  specifier: "decimal.js",
package/dist/contract.js CHANGED
@@ -1860,9 +1860,12 @@ const HOOKS = [
1860
1860
  "PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `value` is the decoded text; `format` is a lowercase symbology name " +
1861
1861
  "(qr_code, code_128, ean_13, …) — treat it as a hint, not a promise, because the two hosts detect different sets. " +
1862
1862
  "It is deliberately ONE-SHOT rather than a start/stop subscription: to scan several codes, call scan() again. " +
1863
- "reset() clears the last result and error. ALWAYS check `supported` before rendering a scan button and give the " +
1864
- "widget a manual-entry path — the web Player reports false wherever the browser has no BarcodeDetector (Safari and " +
1865
- "Firefox today), which is a real browser gap, not a missing feature. options: { formats } is a HINT narrowing the " +
1863
+ "reset() clears the last result and error. ALWAYS check `supported` before rendering a scan button, and give the " +
1864
+ "widget a manual-entry path WHERE THAT GUARD FAILS — which is the WEB Player, wherever the browser ships no " +
1865
+ "BarcodeDetector (Safari and Firefox today), a real browser gap rather than a missing feature. The Expo export " +
1866
+ "ALWAYS reports true, so do NOT pair the scan button with a typed-code twin there: on the phone the scanner is the " +
1867
+ "whole input, and a redundant 'or type it in' field is the manual step the capability exists to remove. " +
1868
+ "options: { formats } is a HINT narrowing the " +
1866
1869
  "symbologies to look for. The Expo export scans via expo-camera; the web Player via BarcodeDetector over the same " +
1867
1870
  "getUserMedia preview useCamera() uses. The camera is released on every exit path, including a dismissal.",
1868
1871
  returnShape: {
@@ -2630,7 +2633,8 @@ const WIDGET_CONTEXT_SHAPE = {
2630
2633
  "BarcodeError (.code PERMISSION_DENIED | UNSUPPORTED | INTERNAL). Its formats hint and its format output BOTH speak " +
2631
2634
  "BarcodeDetector names (qr_code, code_128, ean_13, ...) on both hosts; the Expo export maps them to expo-camera own " +
2632
2635
  "vocabulary internally and DROPS a name it cannot map. isSupported() is false wherever the browser ships no " +
2633
- "BarcodeDetector (Safari, Firefox), so a widget must always offer manual entry beside a scan button. " +
2636
+ "BarcodeDetector (Safari, Firefox), so a WEB build must offer manual entry beside a scan button; the Expo export " +
2637
+ "always reports true and takes the scanner alone — no typed-code twin on native. " +
2634
2638
  "sc-6450 — the geolocation background-watch members are NATIVE-ONLY and opt-in per app: the web Player and an export that " +
2635
2639
  "did not opt in both report isBackgroundSupported() false, and the Expo export backs it with expo-location + " +
2636
2640
  "expo-task-manager. subscribeBackgroundPositions and subscribeBackgroundWatchState are plain subscriptions — they start no " +
@@ -2960,7 +2964,7 @@ const VETTED_IMPORTS = [
2960
2964
  platforms: ["native"],
2961
2965
  category: "system",
2962
2966
  description:
2963
- "Read and write NFC tags on the Expo export: NDEF records via NfcManager.requestTechnology(NfcTech.Ndef), plus the raw technologies a badge, asset tag or transit card uses - NfcTech.IsoDep on both hosts, NfcTech.MifareClassic on Android, NfcTech.MifareIOS / FelicaIOS / Iso15693IOS on iOS (there is no NfcTech.FeliCa; check index.d.ts before naming one). Read with ndefHandler.getNdefMessage(); WRITE with writeNdefMessage(bytes), formatNdef(bytes) (Android only, for a blank tag) and transceive(bytes) on the IsoDep / NfcA / NfcV handlers. makeReadOnly() PERMANENTLY locks a tag - it cannot be undone and no later write will ever succeed, so call it only where the brief actually asks for a one-time-programmable tag, never as a tidy-up step. Raw ISO7816 transceive on iOS additionally needs select-identifiers on the entitlement, which this export does not declare - NDEF read and write work on both hosts. Always cancelTechnologyRequest() in a cleanup - an abandoned session leaves the reader sheet up on iOS. Native-only because no browser hands the Player a reader: Web NFC exists in Chrome on Android alone (Safari, Firefox and every desktop browser omit it), so there is no web build to pair with. Author it in widget.native.jsx and give widget.web.jsx a real way in instead - a typed code field, or the SDK's useBarcodeScanner() QR path. The radio is ALSO opt-in per workspace: the export claims the iOS NFC entitlement and the Android NFC permission only when the publisher enables the 'nfc' device capability in Publish settings. isSupported() does NOT see that gate - it probes the DEVICE (readingAvailable on iOS, hasSystemFeature on Android), so on a capable phone in an app that never asked it returns true and the read then fails: Android throws a SecurityException from registerTagEvent, iOS cannot open the reader session. So wrap start() / requestTechnology() in a try and render the SAME fallback from the catch that widget.web.jsx uses - that catch IS the degradation path, not a nicety. Pinned at 3.17.2, the current stable release (4.x is still beta); a community package, so Expo bundledNativeModules has no version opinion on it.",
2967
+ "Read and write NFC tags on the Expo export: NDEF records via NfcManager.requestTechnology(NfcTech.Ndef), plus the raw technologies a badge, asset tag or transit card uses - NfcTech.IsoDep on both hosts, NfcTech.MifareClassic on Android, NfcTech.MifareIOS / FelicaIOS / Iso15693IOS on iOS (there is no NfcTech.FeliCa; check index.d.ts before naming one). Read with ndefHandler.getNdefMessage(); WRITE with writeNdefMessage(bytes), formatNdef(bytes) (Android only, for a blank tag) and transceive(bytes) on the IsoDep / NfcA / NfcV handlers. makeReadOnly() PERMANENTLY locks a tag - it cannot be undone and no later write will ever succeed, so call it only where the brief actually asks for a one-time-programmable tag, never as a tidy-up step. Raw ISO7816 transceive on iOS additionally needs select-identifiers on the entitlement, which this export does not declare - NDEF read and write work on both hosts. Always cancelTechnologyRequest() in a cleanup - an abandoned session leaves the reader sheet up on iOS. Native-only because no browser hands the Player a reader: Web NFC exists in Chrome on Android alone (Safari, Firefox and every desktop browser omit it), so there is no web build to pair with. Author it in widget.native.jsx and give widget.web.jsx a real way in instead - a typed code field, or the SDK's useBarcodeScanner() QR path. The radio is ALSO opt-in per workspace: the export claims the iOS NFC entitlement and the Android NFC permission only when the publisher enables the 'nfc' device capability in Publish settings. isSupported() does NOT see that gate - it probes the DEVICE (readingAvailable on iOS, hasSystemFeature on Android), so on a capable phone in an app that never asked it returns true and the read then fails: Android throws a SecurityException from registerTagEvent, iOS cannot open the reader session. So wrap start() / requestTechnology() in a try and render a clear ERROR STATE from the catch - name what is missing and say the maker must switch 'nfc' on in Publish settings. That catch is an ERROR path, NOT the web build's typed-code substitute: do not give widget.native.jsx a permanent manual-entry field beside the tap target, because in a correctly-configured app the radio works and the field is the typing the tag was meant to replace. Pinned at 3.17.2, the current stable release (4.x is still beta); a community package, so Expo bundledNativeModules has no version opinion on it.",
2964
2968
  },
2965
2969
  {
2966
2970
  specifier: "decimal.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.150.0",
3
+ "version": "0.150.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",