@colixsystems/widget-sdk 0.128.0 → 0.130.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 +32 -1
- package/dist/contract.cjs +78 -5
- package/dist/contract.js +78 -5
- package/dist/hooks.js +157 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +3 -0
- package/dist/index.native.js +3 -0
- package/dist/style-group.js +33 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -70,7 +70,38 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
70
70
|
|
|
71
71
|
## Status
|
|
72
72
|
|
|
73
|
-
`v0.
|
|
73
|
+
`v0.130.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.130.0 (contract 1.101.0)
|
|
76
|
+
|
|
77
|
+
**New primitive `styleGroup(name)` — mark which element a style group paints (sc-7282).** A widget's `styleSchema` already names the element each field belongs to via `ui.group` ("Card", "Title", "Value"), and the Studio renders those as labelled fieldsets. But a group name says which *fieldset* a control sits in, not which *element on screen* it moves — so clicking a button in the Widget Builder preview could only ever open the whole schema.
|
|
78
|
+
|
|
79
|
+
- **Spread it on the element each non-Basics group paints:** `<View {...styleGroup("Card")}>`. Pass the EXACT `ui.group` string; a mismatch marks an element no group owns. Mark each group ONCE, on the outermost element an author would point at — never the `"Basics"` group (it paints the whole widget) and never a child of an already-marked element. On a list that repeats a marked element per row, mark every row.
|
|
80
|
+
- **One module, both hosts.** It rides the existing `dataSet` prop: `react-native-web` maps it to a `data-*` attribute, and real react-native drops it, so the marker is inert on the device rather than a second implementation. It is a marker, not a style — it changes nothing about how a widget renders.
|
|
81
|
+
|
|
82
|
+
### What's new in 0.129.0 (contract 1.100.0)
|
|
83
|
+
|
|
84
|
+
**Widgets can READ a barcode now — `useBarcodeScanner()` (sc-7228).** A widget could already *generate* a QR code (`react-native-qrcode-svg`) but never read one, so the whole class of apps that starts by pointing a phone at a label — inventory counts, asset check-in/out, warehouse picking, ticket scanning, scan-on-delivery — had no way in.
|
|
85
|
+
|
|
86
|
+
```jsx
|
|
87
|
+
import { useBarcodeScanner, BarcodeError } from "@colixsystems/widget-sdk";
|
|
88
|
+
|
|
89
|
+
const { result, scanning, supported, scan, reset } = useBarcodeScanner();
|
|
90
|
+
const hit = await scan(); // { value, format } | null
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`scan()` is **imperative** — call it from a tap, because both hosts gate the camera permission prompt on a gesture. It resolves the first code decoded, resolves `null` when the user dismisses (a dismissal is NOT an error), and rejects a `BarcodeError` with `.code` of `PERMISSION_DENIED | UNSUPPORTED | INTERNAL`. `format` is a lowercase symbology name (`qr_code`, `code_128`, `ean_13`, …) and is a **hint**: the two hosts detect different sets, so never branch on it for correctness.
|
|
94
|
+
|
|
95
|
+
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.
|
|
96
|
+
|
|
97
|
+
**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:
|
|
98
|
+
|
|
99
|
+
```jsx
|
|
100
|
+
<TextInput value={code} onChangeText={setCode} />
|
|
101
|
+
{supported ? <Pressable onPress={scan}><Text>Scan</Text></Pressable> : null}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**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.
|
|
74
105
|
|
|
75
106
|
### What's new in 0.128.0 (contract 1.99.0 — unchanged)
|
|
76
107
|
|
package/dist/contract.cjs
CHANGED
|
@@ -1670,6 +1670,38 @@ const HOOKS = [
|
|
|
1670
1670
|
requiredContextSlice: [],
|
|
1671
1671
|
scopes: null,
|
|
1672
1672
|
},
|
|
1673
|
+
// sc-7228 — host-brokered barcode / QR scanning. Optional slice; the hook
|
|
1674
|
+
// reports supported:false rather than throwing at render.
|
|
1675
|
+
{
|
|
1676
|
+
name: "useBarcodeScanner",
|
|
1677
|
+
signature: "useBarcodeScanner(options?)",
|
|
1678
|
+
description:
|
|
1679
|
+
"Read a barcode or QR code with the device camera. Returns { result, scanning, error, supported, scan, reset }. " +
|
|
1680
|
+
"Scanning is IMPERATIVE — call scan() from a user gesture (a tap); the browser and the mobile OS gate the camera " +
|
|
1681
|
+
"permission prompt on a gesture, so it NEVER opens on mount. It resolves { value, format } for the FIRST code " +
|
|
1682
|
+
"decoded, or NULL when the user dismisses the scanner — dismissal is the common case and is deliberately NOT an " +
|
|
1683
|
+
"error, so no try/catch is needed on the happy path. It rejects with a BarcodeError whose .code is one of " +
|
|
1684
|
+
"PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `value` is the decoded text; `format` is a lowercase symbology name " +
|
|
1685
|
+
"(qr_code, code_128, ean_13, …) — treat it as a hint, not a promise, because the two hosts detect different sets. " +
|
|
1686
|
+
"It is deliberately ONE-SHOT rather than a start/stop subscription: to scan several codes, call scan() again. " +
|
|
1687
|
+
"reset() clears the last result and error. ALWAYS check `supported` before rendering a scan button and give the " +
|
|
1688
|
+
"widget a manual-entry path — the web Player reports false wherever the browser has no BarcodeDetector (Safari and " +
|
|
1689
|
+
"Firefox today), which is a real browser gap, not a missing feature. options: { formats } is a HINT narrowing the " +
|
|
1690
|
+
"symbologies to look for. The Expo export scans via expo-camera; the web Player via BarcodeDetector over the same " +
|
|
1691
|
+
"getUserMedia preview useCamera() uses. The camera is released on every exit path, including a dismissal.",
|
|
1692
|
+
returnShape: {
|
|
1693
|
+
result: "{ value, format } | null",
|
|
1694
|
+
scanning: "boolean",
|
|
1695
|
+
error: "BarcodeError | null",
|
|
1696
|
+
supported:
|
|
1697
|
+
"boolean // GATE THE BUTTON ON THIS: false where the browser has no BarcodeDetector",
|
|
1698
|
+
scan:
|
|
1699
|
+
"() => Promise<{ value, format } | null> // null if dismissed; rejects with BarcodeError",
|
|
1700
|
+
reset: "() => void // clear the last result + error",
|
|
1701
|
+
},
|
|
1702
|
+
requiredContextSlice: [],
|
|
1703
|
+
scopes: null,
|
|
1704
|
+
},
|
|
1673
1705
|
// sc-7193 — host-brokered image editing. Optional slice; the hook reports
|
|
1674
1706
|
// supported:false rather than throwing at render.
|
|
1675
1707
|
{
|
|
@@ -1846,6 +1878,17 @@ const PRIMITIVES = [
|
|
|
1846
1878
|
rnComponent: null,
|
|
1847
1879
|
docsUrl: null,
|
|
1848
1880
|
},
|
|
1881
|
+
// sc-7282 — the element marker. A FUNCTION primitive like pressableLift: it
|
|
1882
|
+
// returns props to spread, so ONE declaration marks the element on both
|
|
1883
|
+
// hosts. Inert on native (react-native drops `dataSet`), so it costs the
|
|
1884
|
+
// device nothing.
|
|
1885
|
+
{
|
|
1886
|
+
name: "styleGroup",
|
|
1887
|
+
description:
|
|
1888
|
+
"Marks WHICH rendered element a styleSchema `ui.group` paints. Spread it on the element: `<View {...styleGroup(\"Card\")}>`. Pass the EXACT group name from your styleSchema `ui.group` — character for character, or the click resolves to nothing. The Widget Builder preview reads it to map a clicked element back to its group and show only that group's style fields. Mark the OUTERMOST element of each non-Basics group ONCE; never mark the \"Basics\" group (it paints the whole widget) and never mark a child of an already-marked element. On a list that repeats a marked element per row, mark EVERY row. It is a marker, not a style: it changes nothing about how the widget renders.",
|
|
1889
|
+
rnComponent: null,
|
|
1890
|
+
docsUrl: null,
|
|
1891
|
+
},
|
|
1849
1892
|
// sc-6607 — the SCREEN-level overlay. Widgets could previously only paint an
|
|
1850
1893
|
// overlay inside their own box, so a preview or dialog was clipped by the
|
|
1851
1894
|
// layout container the widget sits in; this is the one primitive that leaves
|
|
@@ -2343,10 +2386,11 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2343
2386
|
"subscribeBackgroundWatchState(cb) -> unsubscribe }, " +
|
|
2344
2387
|
"speech: { isSupported() -> boolean, start(options, { onResult, onError, onEnd }) -> Promise<{ stop(), abort() }> }, " +
|
|
2345
2388
|
"camera: { isSupported() -> boolean, capture(options?) -> Promise<asset | null>, pick(options?) -> Promise<asset | null> }, " +
|
|
2346
|
-
"imageEditor: { isSupported() -> boolean, edit(uri, actions, options?) -> Promise<asset> }
|
|
2347
|
-
"
|
|
2348
|
-
"
|
|
2349
|
-
"
|
|
2389
|
+
"imageEditor: { isSupported() -> boolean, edit(uri, actions, options?) -> Promise<asset> }, " +
|
|
2390
|
+
"barcode: { isSupported() -> boolean, scan(options?) -> Promise<{ value, format } | null> } }. " +
|
|
2391
|
+
"Backs useGeolocation(), useSpeechToText(), useCamera(), useImageEditor() and useBarcodeScanner(). The web Player brokers them via " +
|
|
2392
|
+
"navigator.geolocation, window.SpeechRecognition, a getUserMedia camera preview, a host-side canvas and BarcodeDetector; the Expo export via " +
|
|
2393
|
+
"expo-location, expo-speech-recognition, expo-image-picker, expo-image-manipulator and expo-camera. " +
|
|
2350
2394
|
"imageEditor.edit applies resize / crop / rotate / flip in order and rejects with an ImageEditorError " +
|
|
2351
2395
|
"(.code UNSUPPORTED | INVALID_ACTION | DECODE_FAILED | ENCODE_FAILED | INTERNAL). " +
|
|
2352
2396
|
"getCurrentPosition rejects with a GeolocationError (.code PERMISSION_DENIED | UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL). " +
|
|
@@ -2354,6 +2398,11 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2354
2398
|
"its onError carries the Web Speech error vocabulary (not-allowed | no-speech | language-not-supported | network | aborted). " +
|
|
2355
2399
|
"camera.capture/pick resolve a normalised { uri, name, mimeType, width, height, size, file, release? } or NULL when the " +
|
|
2356
2400
|
"user dismisses the picker, and reject with a CameraError (.code PERMISSION_DENIED | UNSUPPORTED | INTERNAL). " +
|
|
2401
|
+
"barcode.scan resolves { value, format } for the first code decoded or NULL when the user dismisses, and rejects with a " +
|
|
2402
|
+
"BarcodeError (.code PERMISSION_DENIED | UNSUPPORTED | INTERNAL). Its formats hint and its format output BOTH speak " +
|
|
2403
|
+
"BarcodeDetector names (qr_code, code_128, ean_13, ...) on both hosts; the Expo export maps them to expo-camera own " +
|
|
2404
|
+
"vocabulary internally and DROPS a name it cannot map. isSupported() is false wherever the browser ships no " +
|
|
2405
|
+
"BarcodeDetector (Safari, Firefox), so a widget must always offer manual entry beside a scan button. " +
|
|
2357
2406
|
"sc-6450 — the geolocation background-watch members are NATIVE-ONLY and opt-in per app: the web Player and an export that " +
|
|
2358
2407
|
"did not opt in both report isBackgroundSupported() false, and the Expo export backs it with expo-location + " +
|
|
2359
2408
|
"expo-task-manager. subscribeBackgroundPositions and subscribeBackgroundWatchState are plain subscriptions — they start no " +
|
|
@@ -2367,6 +2416,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2367
2416
|
speech: "object",
|
|
2368
2417
|
camera: "object",
|
|
2369
2418
|
imageEditor: "object",
|
|
2419
|
+
barcode: "object",
|
|
2370
2420
|
},
|
|
2371
2421
|
},
|
|
2372
2422
|
};
|
|
@@ -3768,7 +3818,30 @@ const CONTRACT = deepFreeze({
|
|
|
3768
3818
|
// reason date-fns is: an AI-agent widget is transpiled, never bundled, so
|
|
3769
3819
|
// its bare import must resolve at runtime on both hosts. No existing entry
|
|
3770
3820
|
// changed shape — minor bump on the pre-1.0 channel.
|
|
3771
|
-
|
|
3821
|
+
// 1.100.0: additive (sc-7228) — new `useBarcodeScanner()` hook + a `barcode`
|
|
3822
|
+
// member on the optional `device` host slice. Widgets could GENERATE a QR
|
|
3823
|
+
// code (react-native-qrcode-svg) but never READ one, so inventory counts,
|
|
3824
|
+
// asset check-in/out, ticket scanning and scan-on-delivery were
|
|
3825
|
+
// unbuildable. Host-brokered rather than a vetted import, the same call as
|
|
3826
|
+
// `camera` and `speech`: widgets never import the native module, so
|
|
3827
|
+
// `expo-camera` stays out of widget bundles and is pinned in the export
|
|
3828
|
+
// only. One-shot scan() modelled on useCamera().capture() rather than a
|
|
3829
|
+
// start/stop subscription — no second streaming lifecycle to keep in step.
|
|
3830
|
+
// NOT native-only: the web half is BarcodeDetector over the getUserMedia
|
|
3831
|
+
// preview useCamera already owns, so this is full parity wherever the
|
|
3832
|
+
// browser ships the API and a declared `supported:false` where it does not
|
|
3833
|
+
// (Safari, Firefox) — a genuine browser gap per CLAUDE.md §8.
|
|
3834
|
+
// 1.101.0: additive (sc-7282) — a new `styleGroup(name)` primitive marking
|
|
3835
|
+
// WHICH rendered element a styleSchema `ui.group` paints, so the Widget
|
|
3836
|
+
// Builder preview can map a clicked node back to its group and show only
|
|
3837
|
+
// that group's style fields instead of the whole schema. It returns props
|
|
3838
|
+
// to spread (`<View {...styleGroup("Card")}>`) and rides the existing
|
|
3839
|
+
// `dataSet` prop: react-native-web maps it to `data-*`, real react-native
|
|
3840
|
+
// drops it, so ONE module serves both hosts and the marker is inert on the
|
|
3841
|
+
// device — no §8 native-only case and no paired implementation. Purely a
|
|
3842
|
+
// marker: it changes nothing about how a widget renders. Minor bump on the
|
|
3843
|
+
// pre-1.0 channel.
|
|
3844
|
+
version: "1.101.0",
|
|
3772
3845
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3773
3846
|
hooks: HOOKS,
|
|
3774
3847
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1670,6 +1670,38 @@ const HOOKS = [
|
|
|
1670
1670
|
requiredContextSlice: [],
|
|
1671
1671
|
scopes: null,
|
|
1672
1672
|
},
|
|
1673
|
+
// sc-7228 — host-brokered barcode / QR scanning. Optional slice; the hook
|
|
1674
|
+
// reports supported:false rather than throwing at render.
|
|
1675
|
+
{
|
|
1676
|
+
name: "useBarcodeScanner",
|
|
1677
|
+
signature: "useBarcodeScanner(options?)",
|
|
1678
|
+
description:
|
|
1679
|
+
"Read a barcode or QR code with the device camera. Returns { result, scanning, error, supported, scan, reset }. " +
|
|
1680
|
+
"Scanning is IMPERATIVE — call scan() from a user gesture (a tap); the browser and the mobile OS gate the camera " +
|
|
1681
|
+
"permission prompt on a gesture, so it NEVER opens on mount. It resolves { value, format } for the FIRST code " +
|
|
1682
|
+
"decoded, or NULL when the user dismisses the scanner — dismissal is the common case and is deliberately NOT an " +
|
|
1683
|
+
"error, so no try/catch is needed on the happy path. It rejects with a BarcodeError whose .code is one of " +
|
|
1684
|
+
"PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `value` is the decoded text; `format` is a lowercase symbology name " +
|
|
1685
|
+
"(qr_code, code_128, ean_13, …) — treat it as a hint, not a promise, because the two hosts detect different sets. " +
|
|
1686
|
+
"It is deliberately ONE-SHOT rather than a start/stop subscription: to scan several codes, call scan() again. " +
|
|
1687
|
+
"reset() clears the last result and error. ALWAYS check `supported` before rendering a scan button and give the " +
|
|
1688
|
+
"widget a manual-entry path — the web Player reports false wherever the browser has no BarcodeDetector (Safari and " +
|
|
1689
|
+
"Firefox today), which is a real browser gap, not a missing feature. options: { formats } is a HINT narrowing the " +
|
|
1690
|
+
"symbologies to look for. The Expo export scans via expo-camera; the web Player via BarcodeDetector over the same " +
|
|
1691
|
+
"getUserMedia preview useCamera() uses. The camera is released on every exit path, including a dismissal.",
|
|
1692
|
+
returnShape: {
|
|
1693
|
+
result: "{ value, format } | null",
|
|
1694
|
+
scanning: "boolean",
|
|
1695
|
+
error: "BarcodeError | null",
|
|
1696
|
+
supported:
|
|
1697
|
+
"boolean // GATE THE BUTTON ON THIS: false where the browser has no BarcodeDetector",
|
|
1698
|
+
scan:
|
|
1699
|
+
"() => Promise<{ value, format } | null> // null if dismissed; rejects with BarcodeError",
|
|
1700
|
+
reset: "() => void // clear the last result + error",
|
|
1701
|
+
},
|
|
1702
|
+
requiredContextSlice: [],
|
|
1703
|
+
scopes: null,
|
|
1704
|
+
},
|
|
1673
1705
|
// sc-7193 — host-brokered image editing. Optional slice; the hook reports
|
|
1674
1706
|
// supported:false rather than throwing at render.
|
|
1675
1707
|
{
|
|
@@ -1846,6 +1878,17 @@ const PRIMITIVES = [
|
|
|
1846
1878
|
rnComponent: null,
|
|
1847
1879
|
docsUrl: null,
|
|
1848
1880
|
},
|
|
1881
|
+
// sc-7282 — the element marker. A FUNCTION primitive like pressableLift: it
|
|
1882
|
+
// returns props to spread, so ONE declaration marks the element on both
|
|
1883
|
+
// hosts. Inert on native (react-native drops `dataSet`), so it costs the
|
|
1884
|
+
// device nothing.
|
|
1885
|
+
{
|
|
1886
|
+
name: "styleGroup",
|
|
1887
|
+
description:
|
|
1888
|
+
"Marks WHICH rendered element a styleSchema `ui.group` paints. Spread it on the element: `<View {...styleGroup(\"Card\")}>`. Pass the EXACT group name from your styleSchema `ui.group` — character for character, or the click resolves to nothing. The Widget Builder preview reads it to map a clicked element back to its group and show only that group's style fields. Mark the OUTERMOST element of each non-Basics group ONCE; never mark the \"Basics\" group (it paints the whole widget) and never mark a child of an already-marked element. On a list that repeats a marked element per row, mark EVERY row. It is a marker, not a style: it changes nothing about how the widget renders.",
|
|
1889
|
+
rnComponent: null,
|
|
1890
|
+
docsUrl: null,
|
|
1891
|
+
},
|
|
1849
1892
|
// sc-6607 — the SCREEN-level overlay. Widgets could previously only paint an
|
|
1850
1893
|
// overlay inside their own box, so a preview or dialog was clipped by the
|
|
1851
1894
|
// layout container the widget sits in; this is the one primitive that leaves
|
|
@@ -2343,10 +2386,11 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2343
2386
|
"subscribeBackgroundWatchState(cb) -> unsubscribe }, " +
|
|
2344
2387
|
"speech: { isSupported() -> boolean, start(options, { onResult, onError, onEnd }) -> Promise<{ stop(), abort() }> }, " +
|
|
2345
2388
|
"camera: { isSupported() -> boolean, capture(options?) -> Promise<asset | null>, pick(options?) -> Promise<asset | null> }, " +
|
|
2346
|
-
"imageEditor: { isSupported() -> boolean, edit(uri, actions, options?) -> Promise<asset> }
|
|
2347
|
-
"
|
|
2348
|
-
"
|
|
2349
|
-
"
|
|
2389
|
+
"imageEditor: { isSupported() -> boolean, edit(uri, actions, options?) -> Promise<asset> }, " +
|
|
2390
|
+
"barcode: { isSupported() -> boolean, scan(options?) -> Promise<{ value, format } | null> } }. " +
|
|
2391
|
+
"Backs useGeolocation(), useSpeechToText(), useCamera(), useImageEditor() and useBarcodeScanner(). The web Player brokers them via " +
|
|
2392
|
+
"navigator.geolocation, window.SpeechRecognition, a getUserMedia camera preview, a host-side canvas and BarcodeDetector; the Expo export via " +
|
|
2393
|
+
"expo-location, expo-speech-recognition, expo-image-picker, expo-image-manipulator and expo-camera. " +
|
|
2350
2394
|
"imageEditor.edit applies resize / crop / rotate / flip in order and rejects with an ImageEditorError " +
|
|
2351
2395
|
"(.code UNSUPPORTED | INVALID_ACTION | DECODE_FAILED | ENCODE_FAILED | INTERNAL). " +
|
|
2352
2396
|
"getCurrentPosition rejects with a GeolocationError (.code PERMISSION_DENIED | UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL). " +
|
|
@@ -2354,6 +2398,11 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2354
2398
|
"its onError carries the Web Speech error vocabulary (not-allowed | no-speech | language-not-supported | network | aborted). " +
|
|
2355
2399
|
"camera.capture/pick resolve a normalised { uri, name, mimeType, width, height, size, file, release? } or NULL when the " +
|
|
2356
2400
|
"user dismisses the picker, and reject with a CameraError (.code PERMISSION_DENIED | UNSUPPORTED | INTERNAL). " +
|
|
2401
|
+
"barcode.scan resolves { value, format } for the first code decoded or NULL when the user dismisses, and rejects with a " +
|
|
2402
|
+
"BarcodeError (.code PERMISSION_DENIED | UNSUPPORTED | INTERNAL). Its formats hint and its format output BOTH speak " +
|
|
2403
|
+
"BarcodeDetector names (qr_code, code_128, ean_13, ...) on both hosts; the Expo export maps them to expo-camera own " +
|
|
2404
|
+
"vocabulary internally and DROPS a name it cannot map. isSupported() is false wherever the browser ships no " +
|
|
2405
|
+
"BarcodeDetector (Safari, Firefox), so a widget must always offer manual entry beside a scan button. " +
|
|
2357
2406
|
"sc-6450 — the geolocation background-watch members are NATIVE-ONLY and opt-in per app: the web Player and an export that " +
|
|
2358
2407
|
"did not opt in both report isBackgroundSupported() false, and the Expo export backs it with expo-location + " +
|
|
2359
2408
|
"expo-task-manager. subscribeBackgroundPositions and subscribeBackgroundWatchState are plain subscriptions — they start no " +
|
|
@@ -2367,6 +2416,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2367
2416
|
speech: "object",
|
|
2368
2417
|
camera: "object",
|
|
2369
2418
|
imageEditor: "object",
|
|
2419
|
+
barcode: "object",
|
|
2370
2420
|
},
|
|
2371
2421
|
},
|
|
2372
2422
|
};
|
|
@@ -3768,7 +3818,30 @@ const CONTRACT = deepFreeze({
|
|
|
3768
3818
|
// reason date-fns is: an AI-agent widget is transpiled, never bundled, so
|
|
3769
3819
|
// its bare import must resolve at runtime on both hosts. No existing entry
|
|
3770
3820
|
// changed shape — minor bump on the pre-1.0 channel.
|
|
3771
|
-
|
|
3821
|
+
// 1.100.0: additive (sc-7228) — new `useBarcodeScanner()` hook + a `barcode`
|
|
3822
|
+
// member on the optional `device` host slice. Widgets could GENERATE a QR
|
|
3823
|
+
// code (react-native-qrcode-svg) but never READ one, so inventory counts,
|
|
3824
|
+
// asset check-in/out, ticket scanning and scan-on-delivery were
|
|
3825
|
+
// unbuildable. Host-brokered rather than a vetted import, the same call as
|
|
3826
|
+
// `camera` and `speech`: widgets never import the native module, so
|
|
3827
|
+
// `expo-camera` stays out of widget bundles and is pinned in the export
|
|
3828
|
+
// only. One-shot scan() modelled on useCamera().capture() rather than a
|
|
3829
|
+
// start/stop subscription — no second streaming lifecycle to keep in step.
|
|
3830
|
+
// NOT native-only: the web half is BarcodeDetector over the getUserMedia
|
|
3831
|
+
// preview useCamera already owns, so this is full parity wherever the
|
|
3832
|
+
// browser ships the API and a declared `supported:false` where it does not
|
|
3833
|
+
// (Safari, Firefox) — a genuine browser gap per CLAUDE.md §8.
|
|
3834
|
+
// 1.101.0: additive (sc-7282) — a new `styleGroup(name)` primitive marking
|
|
3835
|
+
// WHICH rendered element a styleSchema `ui.group` paints, so the Widget
|
|
3836
|
+
// Builder preview can map a clicked node back to its group and show only
|
|
3837
|
+
// that group's style fields instead of the whole schema. It returns props
|
|
3838
|
+
// to spread (`<View {...styleGroup("Card")}>`) and rides the existing
|
|
3839
|
+
// `dataSet` prop: react-native-web maps it to `data-*`, real react-native
|
|
3840
|
+
// drops it, so ONE module serves both hosts and the marker is inert on the
|
|
3841
|
+
// device — no §8 native-only case and no paired implementation. Purely a
|
|
3842
|
+
// marker: it changes nothing about how a widget renders. Minor bump on the
|
|
3843
|
+
// pre-1.0 channel.
|
|
3844
|
+
version: "1.101.0",
|
|
3772
3845
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3773
3846
|
hooks: HOOKS,
|
|
3774
3847
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -1589,6 +1589,163 @@ export function useCamera(options) {
|
|
|
1589
1589
|
return { asset, loading, error, supported, capture, pick, reset };
|
|
1590
1590
|
}
|
|
1591
1591
|
|
|
1592
|
+
/**
|
|
1593
|
+
* The symbology names the SDK speaks on BOTH hosts — BarcodeDetector's
|
|
1594
|
+
* vocabulary. Normalising here is what stops the hosts diverging on bad
|
|
1595
|
+
* input: the web detector THROWS on a name outside its enum while the native
|
|
1596
|
+
* scanner silently ignores one, so an unknown name would reject on web and
|
|
1597
|
+
* quietly scan everything on native.
|
|
1598
|
+
*/
|
|
1599
|
+
const BARCODE_FORMATS = Object.freeze([
|
|
1600
|
+
"aztec",
|
|
1601
|
+
"codabar",
|
|
1602
|
+
"code_128",
|
|
1603
|
+
"code_39",
|
|
1604
|
+
"code_93",
|
|
1605
|
+
"data_matrix",
|
|
1606
|
+
"ean_13",
|
|
1607
|
+
"ean_8",
|
|
1608
|
+
"itf",
|
|
1609
|
+
"pdf417",
|
|
1610
|
+
"qr_code",
|
|
1611
|
+
"upc_a",
|
|
1612
|
+
"upc_e",
|
|
1613
|
+
]);
|
|
1614
|
+
|
|
1615
|
+
/** Drop anything the hosts cannot both honour, so neither has to guess. */
|
|
1616
|
+
function normaliseBarcodeOptions(options) {
|
|
1617
|
+
const opts = options || {};
|
|
1618
|
+
if (!Array.isArray(opts.formats)) return opts;
|
|
1619
|
+
const formats = opts.formats
|
|
1620
|
+
.map((name) => String(name).toLowerCase())
|
|
1621
|
+
.filter((name) => BARCODE_FORMATS.includes(name));
|
|
1622
|
+
return { ...opts, formats };
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Structured error thrown by `useBarcodeScanner` callbacks.
|
|
1627
|
+
*
|
|
1628
|
+
* `code` is one of:
|
|
1629
|
+
* - "PERMISSION_DENIED" — the user (or OS) refused camera access.
|
|
1630
|
+
* - "UNSUPPORTED" — this host brokers no scanner (no BarcodeDetector).
|
|
1631
|
+
* - "INTERNAL" — anything else.
|
|
1632
|
+
*
|
|
1633
|
+
* A user dismissing the scanner is NOT an error — the promise resolves `null`.
|
|
1634
|
+
*/
|
|
1635
|
+
export class BarcodeError extends Error {
|
|
1636
|
+
constructor(code, message, opts) {
|
|
1637
|
+
super(message);
|
|
1638
|
+
this.name = "BarcodeError";
|
|
1639
|
+
this.code = code;
|
|
1640
|
+
if (opts && opts.cause) this.cause = opts.cause;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/** Coerce a thrown value into a BarcodeError with a stable code. */
|
|
1645
|
+
function toBarcodeError(err) {
|
|
1646
|
+
if (err instanceof BarcodeError) return err;
|
|
1647
|
+
const raw = err && err.code !== undefined ? err.code : null;
|
|
1648
|
+
let code = "INTERNAL";
|
|
1649
|
+
if (raw === "PERMISSION_DENIED") code = "PERMISSION_DENIED";
|
|
1650
|
+
else if (raw === "UNSUPPORTED") code = "UNSUPPORTED";
|
|
1651
|
+
const message =
|
|
1652
|
+
(err && typeof err.message === "string" && err.message) ||
|
|
1653
|
+
"Barcode scan failed";
|
|
1654
|
+
return new BarcodeError(code, message, { cause: err });
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Read a barcode or QR code with the device camera. Returns
|
|
1659
|
+
* `{ result, scanning, error, supported, scan, reset }`.
|
|
1660
|
+
*
|
|
1661
|
+
* Scanning is IMPERATIVE — call `scan()` from a user gesture. The OS and the
|
|
1662
|
+
* browser gate the camera permission prompt on a gesture, so the hook never
|
|
1663
|
+
* opens the camera on mount.
|
|
1664
|
+
*
|
|
1665
|
+
* `scan()` resolves `{ value, format }` for the first code decoded, or `null`
|
|
1666
|
+
* when the user dismisses the scanner — dismissal is the most common outcome
|
|
1667
|
+
* and is deliberately not an error, so widgets need no try/catch on the happy
|
|
1668
|
+
* path. It rejects with a `BarcodeError` for a genuine failure.
|
|
1669
|
+
*
|
|
1670
|
+
* One-shot by design: to read several codes, call `scan()` again. There is no
|
|
1671
|
+
* start/stop subscription to leave running.
|
|
1672
|
+
*
|
|
1673
|
+
* ALWAYS check `supported` before rendering a scan button, and give the widget
|
|
1674
|
+
* a manual-entry path: the web Player reports false wherever the browser ships
|
|
1675
|
+
* no `BarcodeDetector` (Safari and Firefox today). That is a real browser gap,
|
|
1676
|
+
* so a scanner must never be the only way to enter a code.
|
|
1677
|
+
*/
|
|
1678
|
+
export function useBarcodeScanner(options) {
|
|
1679
|
+
const ctx = useWidgetContextOrThrow("useBarcodeScanner");
|
|
1680
|
+
const [result, setResult] = useState(null);
|
|
1681
|
+
const [scanning, setScanning] = useState(false);
|
|
1682
|
+
const [error, setError] = useState(null);
|
|
1683
|
+
// `ctx` is a fresh identity every host render — hold the live client and
|
|
1684
|
+
// options in refs so the returned callbacks stay stable.
|
|
1685
|
+
const clientRef = useRef(ctx.device && ctx.device.barcode);
|
|
1686
|
+
clientRef.current = ctx.device && ctx.device.barcode;
|
|
1687
|
+
const optionsRef = useRef(options);
|
|
1688
|
+
optionsRef.current = options;
|
|
1689
|
+
const runRef = useRef(0);
|
|
1690
|
+
|
|
1691
|
+
const supported = Boolean(
|
|
1692
|
+
clientRef.current &&
|
|
1693
|
+
typeof clientRef.current.scan === "function" &&
|
|
1694
|
+
(typeof clientRef.current.isSupported !== "function" ||
|
|
1695
|
+
clientRef.current.isSupported()),
|
|
1696
|
+
);
|
|
1697
|
+
|
|
1698
|
+
// A scan open at unmount still resolves; bumping the run id discards it
|
|
1699
|
+
// instead of setting state on a widget that is gone.
|
|
1700
|
+
useEffect(
|
|
1701
|
+
() => () => {
|
|
1702
|
+
runRef.current += 1;
|
|
1703
|
+
},
|
|
1704
|
+
[],
|
|
1705
|
+
);
|
|
1706
|
+
|
|
1707
|
+
const reset = useCallback(() => {
|
|
1708
|
+
runRef.current += 1;
|
|
1709
|
+
setResult(null);
|
|
1710
|
+
setError(null);
|
|
1711
|
+
}, []);
|
|
1712
|
+
|
|
1713
|
+
const scan = useCallback(async () => {
|
|
1714
|
+
const client = clientRef.current;
|
|
1715
|
+
if (
|
|
1716
|
+
!client ||
|
|
1717
|
+
typeof client.scan !== "function" ||
|
|
1718
|
+
(typeof client.isSupported === "function" && !client.isSupported())
|
|
1719
|
+
) {
|
|
1720
|
+
const e = new BarcodeError(
|
|
1721
|
+
"UNSUPPORTED",
|
|
1722
|
+
"This host does not provide barcode scanning.",
|
|
1723
|
+
);
|
|
1724
|
+
setError(e);
|
|
1725
|
+
throw e;
|
|
1726
|
+
}
|
|
1727
|
+
const run = (runRef.current += 1);
|
|
1728
|
+
setScanning(true);
|
|
1729
|
+
setError(null);
|
|
1730
|
+
try {
|
|
1731
|
+
const next = await client.scan(normaliseBarcodeOptions(optionsRef.current));
|
|
1732
|
+
// A reset(), a newer scan, or an unmount landed while this one was open.
|
|
1733
|
+
if (run !== runRef.current) return null;
|
|
1734
|
+
if (!next) return null;
|
|
1735
|
+
setResult(next);
|
|
1736
|
+
return next;
|
|
1737
|
+
} catch (err) {
|
|
1738
|
+
const be = toBarcodeError(err);
|
|
1739
|
+
if (run === runRef.current) setError(be);
|
|
1740
|
+
throw be;
|
|
1741
|
+
} finally {
|
|
1742
|
+
if (run === runRef.current) setScanning(false);
|
|
1743
|
+
}
|
|
1744
|
+
}, []);
|
|
1745
|
+
|
|
1746
|
+
return { result, scanning, error, supported, scan, reset };
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1592
1749
|
/**
|
|
1593
1750
|
* Structured error thrown by `useImageEditor` callbacks.
|
|
1594
1751
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1746,6 +1746,68 @@ export class ImageEditorError extends Error {
|
|
|
1746
1746
|
);
|
|
1747
1747
|
}
|
|
1748
1748
|
|
|
1749
|
+
/** A code decoded by `useBarcodeScanner().scan()`. */
|
|
1750
|
+
export interface BarcodeScan {
|
|
1751
|
+
/** The decoded text. */
|
|
1752
|
+
value: string;
|
|
1753
|
+
/**
|
|
1754
|
+
* Lowercase symbology name (`qr_code`, `code_128`, `ean_13`, …). A HINT, not
|
|
1755
|
+
* a promise: the two hosts detect different sets, so never branch on it for
|
|
1756
|
+
* correctness.
|
|
1757
|
+
*/
|
|
1758
|
+
format: string;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
/** Options for `useBarcodeScanner(...)`. */
|
|
1762
|
+
export interface BarcodeScannerOptions {
|
|
1763
|
+
/** Narrow which symbologies to look for. A hint the host honours where it can. */
|
|
1764
|
+
formats?: string[];
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
export interface BarcodeScannerResult {
|
|
1768
|
+
/** The most recent scan, or null before the first call / after reset. */
|
|
1769
|
+
result: BarcodeScan | null;
|
|
1770
|
+
scanning: boolean;
|
|
1771
|
+
error: BarcodeError | null;
|
|
1772
|
+
/**
|
|
1773
|
+
* False where the host brokers no scanner — notably any browser without
|
|
1774
|
+
* `BarcodeDetector` (Safari, Firefox). GATE THE SCAN BUTTON ON THIS.
|
|
1775
|
+
*/
|
|
1776
|
+
supported: boolean;
|
|
1777
|
+
/** Resolves the first code decoded, or null if the user dismisses. */
|
|
1778
|
+
scan(): Promise<BarcodeScan | null>;
|
|
1779
|
+
/** Clear `result` and `error`. */
|
|
1780
|
+
reset(): void;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Read a barcode or QR code with the device camera. Imperative — call `scan()`
|
|
1785
|
+
* from a user gesture; the OS and the browser gate the camera prompt on one, so
|
|
1786
|
+
* it never opens on mount. One-shot by design: to read several codes, call
|
|
1787
|
+
* `scan()` again rather than leaving a subscription running.
|
|
1788
|
+
*
|
|
1789
|
+
* The Expo export scans via `expo-camera`; the web Player via `BarcodeDetector`
|
|
1790
|
+
* over the same `getUserMedia` preview `useCamera()` uses. Where the browser
|
|
1791
|
+
* ships no `BarcodeDetector` this is a genuine platform gap (CLAUDE.md §8), so
|
|
1792
|
+
* `supported` is false and the widget must offer manual entry instead.
|
|
1793
|
+
*/
|
|
1794
|
+
export function useBarcodeScanner(
|
|
1795
|
+
options?: BarcodeScannerOptions,
|
|
1796
|
+
): BarcodeScannerResult;
|
|
1797
|
+
|
|
1798
|
+
/**
|
|
1799
|
+
* Error surfaced by `useBarcodeScanner()` — thrown by `scan()` and stored in
|
|
1800
|
+
* the hook's `error` slot. A dismissal is not an error; `scan()` resolves null.
|
|
1801
|
+
*/
|
|
1802
|
+
export class BarcodeError extends Error {
|
|
1803
|
+
code: "PERMISSION_DENIED" | "UNSUPPORTED" | "INTERNAL";
|
|
1804
|
+
constructor(
|
|
1805
|
+
code: BarcodeError["code"],
|
|
1806
|
+
message: string,
|
|
1807
|
+
opts?: { cause?: unknown },
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1749
1811
|
/**
|
|
1750
1812
|
* Error class thrown by useDatastoreMutation callbacks (and surfaced by
|
|
1751
1813
|
* useDatastoreQuery in its `error` slot). The `code` is a stable
|
|
@@ -2617,3 +2679,15 @@ export function pressableLift(state?: {
|
|
|
2617
2679
|
hovered?: boolean;
|
|
2618
2680
|
pressed?: boolean;
|
|
2619
2681
|
}): Array<Record<string, unknown> | null>;
|
|
2682
|
+
|
|
2683
|
+
/**
|
|
2684
|
+
* sc-7282 — marks WHICH rendered element a styleSchema `ui.group` paints, so
|
|
2685
|
+
* the Widget Builder preview can show only that group's style fields when the
|
|
2686
|
+
* element is clicked. Spread it: `<View {...styleGroup("Card")}>`.
|
|
2687
|
+
*
|
|
2688
|
+
* `name` must be the EXACT `ui.group` string. A missing or blank name returns
|
|
2689
|
+
* `undefined`, so spreading the result is always safe.
|
|
2690
|
+
*/
|
|
2691
|
+
export function styleGroup(
|
|
2692
|
+
name: string,
|
|
2693
|
+
): { dataSet: Record<string, string> } | undefined;
|
package/dist/index.js
CHANGED
|
@@ -84,6 +84,8 @@ export {
|
|
|
84
84
|
CameraError,
|
|
85
85
|
useImageEditor,
|
|
86
86
|
ImageEditorError,
|
|
87
|
+
useBarcodeScanner,
|
|
88
|
+
BarcodeError,
|
|
87
89
|
WidgetTree,
|
|
88
90
|
} from "./hooks.js";
|
|
89
91
|
export { isNarrowWidth, NARROW_WIDTH_PX } from "./container-width.js";
|
|
@@ -153,3 +155,4 @@ export {
|
|
|
153
155
|
} from "./contract.js";
|
|
154
156
|
export { normalizeLucideIconName } from "./lucideIconName.js";
|
|
155
157
|
export { pressableLift } from "./interaction.js";
|
|
158
|
+
export { styleGroup } from "./style-group.js";
|
package/dist/index.native.js
CHANGED
|
@@ -84,6 +84,8 @@ export {
|
|
|
84
84
|
CameraError,
|
|
85
85
|
useImageEditor,
|
|
86
86
|
ImageEditorError,
|
|
87
|
+
useBarcodeScanner,
|
|
88
|
+
BarcodeError,
|
|
87
89
|
WidgetTree,
|
|
88
90
|
} from "./hooks.js";
|
|
89
91
|
export { isNarrowWidth, NARROW_WIDTH_PX } from "./container-width.js";
|
|
@@ -147,3 +149,4 @@ export {
|
|
|
147
149
|
} from "./contract.js";
|
|
148
150
|
export { normalizeLucideIconName } from "./lucideIconName.js";
|
|
149
151
|
export { pressableLift } from "./interaction.native.js";
|
|
152
|
+
export { styleGroup } from "./style-group.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// sc-7282 — marks WHICH rendered element a styleSchema `ui.group` paints, so
|
|
2
|
+
// the Widget Builder preview can map a clicked node back to its group and show
|
|
3
|
+
// only that group's style fields.
|
|
4
|
+
//
|
|
5
|
+
// ONE file, no `.native.js` twin: `react-native-web` maps `dataSet` to `data-*`
|
|
6
|
+
// attributes while real react-native drops the prop, so the marker is inert on
|
|
7
|
+
// the device rather than a second implementation (CLAUDE.md §3). The paired
|
|
8
|
+
// modules in this package (interaction.js, overlay.js) exist because their
|
|
9
|
+
// behaviour genuinely differs per host; this one's does not.
|
|
10
|
+
//
|
|
11
|
+
// Not a primitive wrapper either — primitives.js re-exports react-native-web
|
|
12
|
+
// directly, and its header records that the hand-written paired wrappers were
|
|
13
|
+
// removed on purpose.
|
|
14
|
+
|
|
15
|
+
/** The `dataSet` key, surfacing in the DOM as `data-style-group`. */
|
|
16
|
+
export const STYLE_GROUP_DATA_KEY = "styleGroup";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Props marking the element a style group paints. Spread it:
|
|
20
|
+
* `<View {...styleGroup("Card")}>`.
|
|
21
|
+
*
|
|
22
|
+
* `name` must be the EXACT `ui.group` string from the widget's styleSchema — a
|
|
23
|
+
* mismatch marks an element no group owns, and the click resolves to nothing.
|
|
24
|
+
*
|
|
25
|
+
* Returns `undefined` for a missing or blank name so `{...styleGroup(x)}` stays
|
|
26
|
+
* safe when `x` is absent: spreading `undefined` is a no-op in JSX.
|
|
27
|
+
*/
|
|
28
|
+
export function styleGroup(name) {
|
|
29
|
+
if (typeof name !== "string") return undefined;
|
|
30
|
+
const trimmed = name.trim();
|
|
31
|
+
if (!trimmed) return undefined;
|
|
32
|
+
return { dataSet: { [STYLE_GROUP_DATA_KEY]: trimmed } };
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.130.0",
|
|
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",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "node scripts/build.js",
|
|
52
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/vetted-imports-audit.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-flex-basis-percent.test.js src/__tests__/linter-hardcoded-design.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/flatten-entry.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/overlay-tokens.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js src/__tests__/hooks-camera.test.js src/__tests__/hooks-image-editor.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js src/__tests__/linter-html-in-content.test.js src/__tests__/markdown.test.js src/__tests__/markdown-edit.test.js src/__tests__/richtext-tokens.test.js"
|
|
52
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/vetted-imports-audit.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-flex-basis-percent.test.js src/__tests__/linter-hardcoded-design.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/flatten-entry.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/overlay-tokens.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js src/__tests__/hooks-camera.test.js src/__tests__/hooks-barcode.test.js src/__tests__/hooks-image-editor.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js src/__tests__/linter-html-in-content.test.js src/__tests__/markdown.test.js src/__tests__/markdown-edit.test.js src/__tests__/richtext-tokens.test.js src/__tests__/style-group.test.js"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
55
|
"node": ">=18"
|