@colixsystems/widget-sdk 0.114.0 → 0.116.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 +26 -1
- package/dist/contract.cjs +34 -2
- package/dist/contract.js +34 -2
- package/dist/index.d.ts +30 -0
- package/dist/index.js +4 -0
- package/dist/index.native.js +2 -0
- package/dist/linter.cjs +15 -9
- package/dist/linter.js +15 -9
- package/dist/overlay-tokens.js +64 -0
- package/dist/overlay-view.js +109 -0
- package/dist/overlay.js +13 -0
- package/dist/overlay.native.js +11 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
49
49
|
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*` |
|
|
50
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. |
|
|
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 "
|
|
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. |
|
|
53
53
|
| **FILES** (`ctx.assets`) | `useAsset(id)` | `{ url, file, loading, error, refetch }` | `ctx.assets.get` — no scope |
|
|
54
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
55
|
| **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
|
|
@@ -72,6 +72,30 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
72
72
|
|
|
73
73
|
`v0.112.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
74
|
|
|
75
|
+
### What's new in 0.116.0 (contract 1.88.0)
|
|
76
|
+
|
|
77
|
+
**New primitive `<Overlay>` — a widget can finally open something over the SCREEN (sc-6607).** Until now a widget could only paint an overlay inside its own root, where the host's layout containers clip it: a PDF preview, a lightbox, or a confirm dialog opened trapped inside the widget's tile, and no amount of `zIndex` fixed it (`overflow: "hidden"` clips regardless, and `position: "fixed"` does not exist on native).
|
|
78
|
+
|
|
79
|
+
```jsx
|
|
80
|
+
<Overlay visible={!!preview} onRequestClose={() => setPreview(null)} size="full">
|
|
81
|
+
<ScrollView>{renderPreview(preview)}</ScrollView>
|
|
82
|
+
</Overlay>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`<Overlay>` renders its children OUTSIDE the widget's layout box on both hosts — the web Player portals them to the document root, the exported Expo app hands them to the OS modal — while they stay in your own React tree, so their state and hooks are untouched. `onRequestClose` carries the backdrop press, Escape on web, and the Android back button; `size` is `sm` | `md` (default) | `lg` | `full`; the scrim, surface, radius, padding and elevation come from the workspace theme.
|
|
86
|
+
|
|
87
|
+
An anchored dropdown or popover is the one overlay kind that still belongs inside your own root — `<Overlay>` centres on the screen rather than on a trigger.
|
|
88
|
+
|
|
89
|
+
### What's new in 0.115.0 (contract unchanged)
|
|
90
|
+
|
|
91
|
+
**`write-not-gated-on-user` now accepts a `useCanWrite()` gate — a widget may be opened to logged-out visitors (sc-6593).** The rule (added in 0.89.0, below) flagged any `useDatastoreMutation` write that carried no identity guard, and only a `.id` / `groupIds` / `roles` check counted as one. That encoded "a write needs a signed-in app user" as a platform fact, which it is not: a table whose permissions grant **Create** to *Everyone (anonymous + signed-in)* accepts a write from a logged-out visitor, and `useCanWrite(tableId)` answers `true` for them.
|
|
92
|
+
|
|
93
|
+
So a widget gated on `useCanWrite` alone — the correct shape for a public tally, a guest sign-up sheet, or an open feedback form — used to trip the warning that steers the AI widget agent's repair loop back to identity gating, making the sign-in requirement impossible for an author to remove. It is now recognised as a gate, and its finding label names it first.
|
|
94
|
+
|
|
95
|
+
Nothing else changes: a widget with **no** gate at all is still flagged, `useUser().id` read purely as a VALUE still does not satisfy the rule, and the severity is still `warning` (never publish-blocking). Identity gating remains the right default for almost every write — this only stops the linter from arguing against the one case where it isn't.
|
|
96
|
+
|
|
97
|
+
When you take the `useCanWrite`-only route, omit the USER column for a guest (`if (user.id) payload[byField] = user.id;`) — an anonymous row records no author, so per-person limits and "my entries" views cannot work for one.
|
|
98
|
+
|
|
75
99
|
### What's new in 0.113.0 (contract 1.86.0)
|
|
76
100
|
|
|
77
101
|
**New `useCamera()` hook — take a photo or pick one from the device library.** A new CORE hook reading a new `camera` capability on the existing `ctx.device` slice. Returns `{ asset, loading, error, supported, capture, pick, reset }`. Capture is **imperative** — call `capture()` or `pick()` from a user gesture (a `Pressable.onPress`); the browser and the mobile OS gate the permission prompt on a gesture, so it NEVER opens on mount. `options` (`{ allowsEditing, quality }`) pass through to the host. It needs **no manifest scope** and **no `requestedScopes` entry**.
|
|
@@ -1080,6 +1104,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
1080
1104
|
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `useWidgetInput`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useRouteParams`, `usePageContext`, `useWidgetRoute`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError` carrying `code`, the server's user-safe `message`, and `retryable` (`false` = this charge cannot succeed until the workspace, manifest, or amount changes — show the message, not a retry). `useSendNotification()` returns `{ send, sending, error }` and requires the `notifications.send:appUser` scope; `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace (cross-workspace `recipient_user_id` is rejected), must be called from an event handler rather than render, and rejects with a `NotificationError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (camelCase — the host-built context object, not a wire payload; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute, openLink }` for internal page navigation; `openLink(link)` follows an author- or data-supplied link of unknown shape through the host's shared resolver (in-app page → internal route, off-app http(s) → opened outside, unsafe → refused) and is the safe choice for any value your code did not construct, while `Linking.openURL(url)` is for an external URL you built yourself. `useRouteParams()` returns the current route's params object (`currentRoute.params`) — the flat master→detail accessor; read a param off it (e.g. `recordId`), never call it. `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useAsset(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case). `useWidgetInput(inputName)` returns the latest payload a sibling widget published on the event the page author wired to this widget's declared `inputs` entry (`undefined` when unwired or not yet published).
|
|
1081
1105
|
- `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
|
|
1082
1106
|
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`; `DateTimePicker` wraps `@react-native-community/datetimepicker` on native and renders `<input type="date|time|datetime-local">` directly on web because the RN library has no react-native-web mapping). The web build aliases `react-native` to `react-native-web` so the RN-re-exported primitives render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
|
|
1107
|
+
- `Overlay` — the screen-level overlay (sc-6607). `<Overlay visible={!!preview} onRequestClose={() => setPreview(null)} size="full">…</Overlay>` renders its children OUTSIDE the widget's layout box on both hosts (web portals it to the document root, native uses the OS modal), so no clipping card, `ScrollView` or neighbouring widget can cut off a document/media preview, lightbox or confirm dialog — which absolute positioning inside the widget cannot achieve on either platform. Children stay in your own React tree, so state and hooks work normally. `onRequestClose` carries the backdrop press, Escape on web, and the Android back button; `size` is `sm` | `md` (default) | `lg` | `full`; the scrim, surface, radius, padding and elevation come from the workspace theme. An anchored dropdown still belongs inside your own root.
|
|
1083
1108
|
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
|
1084
1109
|
|
|
1085
1110
|
## Design & visual polish
|
package/dist/contract.cjs
CHANGED
|
@@ -272,6 +272,16 @@ const LINK_ACTION_FIELDS = Object.freeze({
|
|
|
272
272
|
radius: "buttonRadius",
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
+
// sc-6602: Image frames media with the flat `radius`/`borderColor`/`background`
|
|
276
|
+
// trio rather than the `card*` names, so the card scope reaches it through this
|
|
277
|
+
// map. Bound by id, so `shadow` is safe to carry here.
|
|
278
|
+
const MEDIA_FRAME_FIELDS = Object.freeze({
|
|
279
|
+
background: "background",
|
|
280
|
+
borderColor: "borderColor",
|
|
281
|
+
radius: "radius",
|
|
282
|
+
shadow: "shadow",
|
|
283
|
+
});
|
|
284
|
+
|
|
275
285
|
// REQ-THEME-WIDGET: the card fields whose NAMES are unambiguous, so they bind to
|
|
276
286
|
// ANY widget that reads them -- including a Mason-generated one, whose id can
|
|
277
287
|
// never appear in a hand-maintained allowlist. That allowlist is why "make the
|
|
@@ -280,7 +290,8 @@ const LINK_ACTION_FIELDS = Object.freeze({
|
|
|
280
290
|
// `shadow` is deliberately ABSENT: its name is bare and shared with the button
|
|
281
291
|
// scope, so binding it by name would cross the scopes. The bare names stay on
|
|
282
292
|
// the allowlist for exactly that reason -- `appstudio.image` also reads a
|
|
283
|
-
// `background` field, and the button scope must not leak into it.
|
|
293
|
+
// `background` field, and the button scope must not leak into it. The media
|
|
294
|
+
// surfaces get themed elevation by ID instead (sc-6602); keep it out of here.
|
|
284
295
|
const CARD_UNIVERSAL_FIELDS = Object.freeze({
|
|
285
296
|
background: "cardBackground",
|
|
286
297
|
borderColor: "cardBorderColor",
|
|
@@ -367,6 +378,12 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
367
378
|
"appstudio.form-builder": CARD_SURFACE_FIELDS,
|
|
368
379
|
"appstudio.user-management": CARD_SURFACE_FIELDS,
|
|
369
380
|
"appstudio.link": CARD_SURFACE_FIELDS,
|
|
381
|
+
// sc-6602: the media surfaces. They paint a card exactly as the widgets
|
|
382
|
+
// above do, so a themed elevation must reach them — a reference design's
|
|
383
|
+
// lifted hero image was unthemeable while these three were absent.
|
|
384
|
+
"appstudio.sound": CARD_SURFACE_FIELDS,
|
|
385
|
+
"appstudio.video": CARD_SURFACE_FIELDS,
|
|
386
|
+
"appstudio.image": MEDIA_FRAME_FIELDS,
|
|
370
387
|
}),
|
|
371
388
|
}),
|
|
372
389
|
text: Object.freeze({
|
|
@@ -1770,6 +1787,17 @@ const PRIMITIVES = [
|
|
|
1770
1787
|
rnComponent: null,
|
|
1771
1788
|
docsUrl: null,
|
|
1772
1789
|
},
|
|
1790
|
+
// sc-6607 — the SCREEN-level overlay. Widgets could previously only paint an
|
|
1791
|
+
// overlay inside their own box, so a preview or dialog was clipped by the
|
|
1792
|
+
// layout container the widget sits in; this is the one primitive that leaves
|
|
1793
|
+
// that box on both hosts.
|
|
1794
|
+
{
|
|
1795
|
+
name: "Overlay",
|
|
1796
|
+
description:
|
|
1797
|
+
'Screen-level overlay. `<Overlay visible={open} onRequestClose={() => setOpen(false)} size="md">…panel…</Overlay>`. THE way to open anything that must cover the SCREEN rather than the widget — a preview (PDF, image, video), a lightbox, a confirm dialog, a full detail panel. It renders OUTSIDE the widget layout box on both hosts (react-native-web portals it to the document root; native uses the OS modal), so no `overflow: hidden` card, scroll container or sibling widget can clip or cover it — which absolute positioning inside the widget cannot achieve on either host. Children stay in your own React tree, so state and hooks work normally. Props: `visible` (boolean, required to show), `onRequestClose` (fired by the backdrop press, the Escape key on web, and the Android back button — always wire it or the overlay cannot be closed), `size` ("sm" | "md" | "lg" | "full", default "md"; `full` runs edge to edge for media), `dismissOnBackdropPress` (default true), `accessibilityLabel`, and `style` for extra panel styles. The scrim, surface, radius, padding and elevation come from the workspace theme — never re-style them. Keep it for screen-level surfaces: a dropdown anchored to its trigger still belongs inside the widget.',
|
|
1798
|
+
rnComponent: null,
|
|
1799
|
+
docsUrl: null,
|
|
1800
|
+
},
|
|
1773
1801
|
];
|
|
1774
1802
|
|
|
1775
1803
|
const CATEGORIES = [
|
|
@@ -3468,7 +3496,11 @@ const CONTRACT = deepFreeze({
|
|
|
3468
3496
|
// plus the `pressableLift` primitive that resolves it per input — hover on
|
|
3469
3497
|
// web, press on native. Tappable layout containers and widget surfaces lift
|
|
3470
3498
|
// from this ONE table.
|
|
3471
|
-
|
|
3499
|
+
// 1.88.0: additive (sc-6607) — the `Overlay` primitive: the first surface a
|
|
3500
|
+
// widget can open at SCREEN level. Everything before it was clipped by the
|
|
3501
|
+
// layout container the widget sits in, so a preview or dialog could not leave
|
|
3502
|
+
// the widget's own tile on either host.
|
|
3503
|
+
version: "1.88.0",
|
|
3472
3504
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3473
3505
|
hooks: HOOKS,
|
|
3474
3506
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -272,6 +272,16 @@ const LINK_ACTION_FIELDS = Object.freeze({
|
|
|
272
272
|
radius: "buttonRadius",
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
+
// sc-6602: Image frames media with the flat `radius`/`borderColor`/`background`
|
|
276
|
+
// trio rather than the `card*` names, so the card scope reaches it through this
|
|
277
|
+
// map. Bound by id, so `shadow` is safe to carry here.
|
|
278
|
+
const MEDIA_FRAME_FIELDS = Object.freeze({
|
|
279
|
+
background: "background",
|
|
280
|
+
borderColor: "borderColor",
|
|
281
|
+
radius: "radius",
|
|
282
|
+
shadow: "shadow",
|
|
283
|
+
});
|
|
284
|
+
|
|
275
285
|
// REQ-THEME-WIDGET: the card fields whose NAMES are unambiguous, so they bind to
|
|
276
286
|
// ANY widget that reads them -- including a Mason-generated one, whose id can
|
|
277
287
|
// never appear in a hand-maintained allowlist. That allowlist is why "make the
|
|
@@ -280,7 +290,8 @@ const LINK_ACTION_FIELDS = Object.freeze({
|
|
|
280
290
|
// `shadow` is deliberately ABSENT: its name is bare and shared with the button
|
|
281
291
|
// scope, so binding it by name would cross the scopes. The bare names stay on
|
|
282
292
|
// the allowlist for exactly that reason -- `appstudio.image` also reads a
|
|
283
|
-
// `background` field, and the button scope must not leak into it.
|
|
293
|
+
// `background` field, and the button scope must not leak into it. The media
|
|
294
|
+
// surfaces get themed elevation by ID instead (sc-6602); keep it out of here.
|
|
284
295
|
const CARD_UNIVERSAL_FIELDS = Object.freeze({
|
|
285
296
|
background: "cardBackground",
|
|
286
297
|
borderColor: "cardBorderColor",
|
|
@@ -367,6 +378,12 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
367
378
|
"appstudio.form-builder": CARD_SURFACE_FIELDS,
|
|
368
379
|
"appstudio.user-management": CARD_SURFACE_FIELDS,
|
|
369
380
|
"appstudio.link": CARD_SURFACE_FIELDS,
|
|
381
|
+
// sc-6602: the media surfaces. They paint a card exactly as the widgets
|
|
382
|
+
// above do, so a themed elevation must reach them — a reference design's
|
|
383
|
+
// lifted hero image was unthemeable while these three were absent.
|
|
384
|
+
"appstudio.sound": CARD_SURFACE_FIELDS,
|
|
385
|
+
"appstudio.video": CARD_SURFACE_FIELDS,
|
|
386
|
+
"appstudio.image": MEDIA_FRAME_FIELDS,
|
|
370
387
|
}),
|
|
371
388
|
}),
|
|
372
389
|
text: Object.freeze({
|
|
@@ -1770,6 +1787,17 @@ const PRIMITIVES = [
|
|
|
1770
1787
|
rnComponent: null,
|
|
1771
1788
|
docsUrl: null,
|
|
1772
1789
|
},
|
|
1790
|
+
// sc-6607 — the SCREEN-level overlay. Widgets could previously only paint an
|
|
1791
|
+
// overlay inside their own box, so a preview or dialog was clipped by the
|
|
1792
|
+
// layout container the widget sits in; this is the one primitive that leaves
|
|
1793
|
+
// that box on both hosts.
|
|
1794
|
+
{
|
|
1795
|
+
name: "Overlay",
|
|
1796
|
+
description:
|
|
1797
|
+
'Screen-level overlay. `<Overlay visible={open} onRequestClose={() => setOpen(false)} size="md">…panel…</Overlay>`. THE way to open anything that must cover the SCREEN rather than the widget — a preview (PDF, image, video), a lightbox, a confirm dialog, a full detail panel. It renders OUTSIDE the widget layout box on both hosts (react-native-web portals it to the document root; native uses the OS modal), so no `overflow: hidden` card, scroll container or sibling widget can clip or cover it — which absolute positioning inside the widget cannot achieve on either host. Children stay in your own React tree, so state and hooks work normally. Props: `visible` (boolean, required to show), `onRequestClose` (fired by the backdrop press, the Escape key on web, and the Android back button — always wire it or the overlay cannot be closed), `size` ("sm" | "md" | "lg" | "full", default "md"; `full` runs edge to edge for media), `dismissOnBackdropPress` (default true), `accessibilityLabel`, and `style` for extra panel styles. The scrim, surface, radius, padding and elevation come from the workspace theme — never re-style them. Keep it for screen-level surfaces: a dropdown anchored to its trigger still belongs inside the widget.',
|
|
1798
|
+
rnComponent: null,
|
|
1799
|
+
docsUrl: null,
|
|
1800
|
+
},
|
|
1773
1801
|
];
|
|
1774
1802
|
|
|
1775
1803
|
const CATEGORIES = [
|
|
@@ -3468,7 +3496,11 @@ const CONTRACT = deepFreeze({
|
|
|
3468
3496
|
// plus the `pressableLift` primitive that resolves it per input — hover on
|
|
3469
3497
|
// web, press on native. Tappable layout containers and widget surfaces lift
|
|
3470
3498
|
// from this ONE table.
|
|
3471
|
-
|
|
3499
|
+
// 1.88.0: additive (sc-6607) — the `Overlay` primitive: the first surface a
|
|
3500
|
+
// widget can open at SCREEN level. Everything before it was clipped by the
|
|
3501
|
+
// layout container the widget sits in, so a preview or dialog could not leave
|
|
3502
|
+
// the widget's own tile on either host.
|
|
3503
|
+
version: "1.88.0",
|
|
3472
3504
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3473
3505
|
hooks: HOOKS,
|
|
3474
3506
|
primitives: PRIMITIVES,
|
package/dist/index.d.ts
CHANGED
|
@@ -2151,6 +2151,36 @@ export const Gradient: (props: {
|
|
|
2151
2151
|
children?: ReactNode;
|
|
2152
2152
|
}) => any;
|
|
2153
2153
|
|
|
2154
|
+
/**
|
|
2155
|
+
* sc-6607 — screen-level overlay. Renders OUTSIDE the widget's layout box on
|
|
2156
|
+
* both hosts (web portals it to the document root, native uses the OS modal),
|
|
2157
|
+
* so no clipping card, scroll container or neighbouring widget can cut it off.
|
|
2158
|
+
* Use it for anything that takes over the screen — a document/media preview, a
|
|
2159
|
+
* lightbox, a confirm dialog. An anchored dropdown still belongs inside the
|
|
2160
|
+
* widget's own root.
|
|
2161
|
+
*
|
|
2162
|
+
* @example
|
|
2163
|
+
* <Overlay visible={!!preview} onRequestClose={() => setPreview(null)} size="full">
|
|
2164
|
+
* <ScrollView>{renderPreview(preview)}</ScrollView>
|
|
2165
|
+
* </Overlay>
|
|
2166
|
+
*/
|
|
2167
|
+
export const Overlay: (props: {
|
|
2168
|
+
/** Nothing renders (and nothing is mounted) while this is false. */
|
|
2169
|
+
visible?: boolean;
|
|
2170
|
+
/** Backdrop press, Escape (web) and the Android back button all fire this. */
|
|
2171
|
+
onRequestClose?: () => void;
|
|
2172
|
+
/** Panel width tier. `full` runs edge to edge. Defaults to `md`. */
|
|
2173
|
+
size?: "sm" | "md" | "lg" | "full";
|
|
2174
|
+
/** Set false when only an explicit control may close the overlay. */
|
|
2175
|
+
dismissOnBackdropPress?: boolean;
|
|
2176
|
+
accessibilityLabel?: string;
|
|
2177
|
+
/** Label for the backdrop's dismiss target. Defaults to "Close". */
|
|
2178
|
+
closeAccessibilityLabel?: string;
|
|
2179
|
+
/** Extra styles merged onto the themed panel. */
|
|
2180
|
+
style?: any;
|
|
2181
|
+
children?: ReactNode;
|
|
2182
|
+
}) => any;
|
|
2183
|
+
|
|
2154
2184
|
// ------------------------------------------------------- theme derivation
|
|
2155
2185
|
// sc-3696: the colour maths both hosts resolve `useTheme()` with. Exported so
|
|
2156
2186
|
// the Player (frontend/src/services/widgetTheme.js) and the exported app's
|
package/dist/index.js
CHANGED
|
@@ -90,6 +90,10 @@ export { isNarrowWidth, NARROW_WIDTH_PX } from "./container-width.js";
|
|
|
90
90
|
// the web variant and index.native.js picks the native variant.
|
|
91
91
|
export { useClipboard, ClipboardError } from "./clipboard.js";
|
|
92
92
|
export { useToast } from "./toast.js";
|
|
93
|
+
// sc-6607 — `<Overlay>` is one shared component (./overlay-view.js) bound to
|
|
94
|
+
// each platform's primitives, so a widget's modal escapes its container box
|
|
95
|
+
// identically on the Player and the Expo export.
|
|
96
|
+
export { Overlay } from "./overlay.js";
|
|
93
97
|
export {
|
|
94
98
|
Text,
|
|
95
99
|
View,
|
package/dist/index.native.js
CHANGED
|
@@ -88,6 +88,8 @@ export { isNarrowWidth, NARROW_WIDTH_PX } from "./container-width.js";
|
|
|
88
88
|
// REQ-WSDK-PLATFORM §6 — Tier A hooks (native variants).
|
|
89
89
|
export { useClipboard, ClipboardError } from "./clipboard.native.js";
|
|
90
90
|
export { useToast } from "./toast.native.js";
|
|
91
|
+
// sc-6607 — see the note on the web mirror in ./index.js.
|
|
92
|
+
export { Overlay } from "./overlay.native.js";
|
|
91
93
|
export {
|
|
92
94
|
Text,
|
|
93
95
|
View,
|
package/dist/linter.cjs
CHANGED
|
@@ -928,12 +928,17 @@ const CURRENCY_LABEL_RES = [
|
|
|
928
928
|
];
|
|
929
929
|
|
|
930
930
|
// sc-4985 — soft warning: a widget that writes must decide what a signed-OUT
|
|
931
|
-
// visitor sees. A
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
// a
|
|
935
|
-
//
|
|
936
|
-
//
|
|
931
|
+
// visitor sees. A visitor handed a live "Save" / "Book" / "Delete" button the
|
|
932
|
+
// table will refuse can only tap it and fail — the failure the gate exists to
|
|
933
|
+
// spare them. Satisfied by any identity guard: a negated or compared `.id`, or
|
|
934
|
+
// a `groupIds` / `roles` check. Reading `useUser().id` purely as a VALUE (the
|
|
935
|
+
// USER-column write pattern) is NOT a guard, which is why an operator follows.
|
|
936
|
+
//
|
|
937
|
+
// sc-6593 — `useCanWrite(tableId)` satisfies it too, and answers better: it
|
|
938
|
+
// reads the ACL the write endpoint enforces, so it goes live for whoever may
|
|
939
|
+
// write. Since a table granting Create to EVERYONE accepts an anonymous write
|
|
940
|
+
// (sc-5229), demanding identity here would flag the only correct way to build
|
|
941
|
+
// the logged-out-friendly widget an author explicitly asked for.
|
|
937
942
|
//
|
|
938
943
|
// Conservative on purpose: an unrelated `.id` comparison elsewhere in the
|
|
939
944
|
// source silences the rule. A warning that occasionally stays quiet is far
|
|
@@ -952,6 +957,7 @@ function _writeGatedOnUserRules(source) {
|
|
|
952
957
|
const code = _stripNonCode(source);
|
|
953
958
|
const call = /\buseDatastoreMutation\s*\(/.exec(code);
|
|
954
959
|
if (!call) return [];
|
|
960
|
+
if (/\buseCanWrite\s*\(/.test(code)) return [];
|
|
955
961
|
if (_IDENTITY_GUARD_RES.some((re) => re.test(code))) return [];
|
|
956
962
|
const line = code.slice(0, call.index).split(/\r?\n/).length;
|
|
957
963
|
return [
|
|
@@ -961,9 +967,9 @@ function _writeGatedOnUserRules(source) {
|
|
|
961
967
|
// Kept under ~210 chars: a finding is truncated at 300 downstream, and
|
|
962
968
|
// the fix instruction is the half worth keeping.
|
|
963
969
|
label:
|
|
964
|
-
`writes with useDatastoreMutation() but never checks who
|
|
965
|
-
|
|
966
|
-
`
|
|
970
|
+
`writes with useDatastoreMutation() but never checks who may write ` +
|
|
971
|
+
`- gate on useCanWrite(tableId), or useUser() when the action needs ` +
|
|
972
|
+
`a signed-in user, and render the control inactive, never live-but-doomed.`,
|
|
967
973
|
line,
|
|
968
974
|
snippet: (source.split(/\r?\n/)[line - 1] || "").trim().slice(0, 200),
|
|
969
975
|
},
|
package/dist/linter.js
CHANGED
|
@@ -1077,12 +1077,17 @@ const CURRENCY_LABEL_RES = [
|
|
|
1077
1077
|
];
|
|
1078
1078
|
|
|
1079
1079
|
// sc-4985 — soft warning: a widget that writes must decide what a signed-OUT
|
|
1080
|
-
// visitor sees. A
|
|
1081
|
-
//
|
|
1082
|
-
//
|
|
1083
|
-
// a
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1080
|
+
// visitor sees. A visitor handed a live "Save" / "Book" / "Delete" button the
|
|
1081
|
+
// table will refuse can only tap it and fail — the failure the gate exists to
|
|
1082
|
+
// spare them. Satisfied by any identity guard: a negated or compared `.id`, or
|
|
1083
|
+
// a `groupIds` / `roles` check. Reading `useUser().id` purely as a VALUE (the
|
|
1084
|
+
// USER-column write pattern) is NOT a guard, which is why an operator follows.
|
|
1085
|
+
//
|
|
1086
|
+
// sc-6593 — `useCanWrite(tableId)` satisfies it too, and answers better: it
|
|
1087
|
+
// reads the ACL the write endpoint enforces, so it goes live for whoever may
|
|
1088
|
+
// write. Since a table granting Create to EVERYONE accepts an anonymous write
|
|
1089
|
+
// (sc-5229), demanding identity here would flag the only correct way to build
|
|
1090
|
+
// the logged-out-friendly widget an author explicitly asked for.
|
|
1086
1091
|
//
|
|
1087
1092
|
// Conservative on purpose: an unrelated `.id` comparison elsewhere in the
|
|
1088
1093
|
// source silences the rule. A warning that occasionally stays quiet is far
|
|
@@ -1101,6 +1106,7 @@ function _writeGatedOnUserRules(source) {
|
|
|
1101
1106
|
const code = _stripNonCode(source);
|
|
1102
1107
|
const call = /\buseDatastoreMutation\s*\(/.exec(code);
|
|
1103
1108
|
if (!call) return [];
|
|
1109
|
+
if (/\buseCanWrite\s*\(/.test(code)) return [];
|
|
1104
1110
|
if (_IDENTITY_GUARD_RES.some((re) => re.test(code))) return [];
|
|
1105
1111
|
const line = code.slice(0, call.index).split(/\r?\n/).length;
|
|
1106
1112
|
return [
|
|
@@ -1110,9 +1116,9 @@ function _writeGatedOnUserRules(source) {
|
|
|
1110
1116
|
// Kept under ~210 chars: a finding is truncated at 300 downstream, and
|
|
1111
1117
|
// the fix instruction is the half worth keeping.
|
|
1112
1118
|
label:
|
|
1113
|
-
`writes with useDatastoreMutation() but never checks who
|
|
1114
|
-
|
|
1115
|
-
`
|
|
1119
|
+
`writes with useDatastoreMutation() but never checks who may write ` +
|
|
1120
|
+
`- gate on useCanWrite(tableId), or useUser() when the action needs ` +
|
|
1121
|
+
`a signed-in user, and render the control inactive, never live-but-doomed.`,
|
|
1116
1122
|
line,
|
|
1117
1123
|
snippet: (source.split(/\r?\n/)[line - 1] || "").trim().slice(0, 200),
|
|
1118
1124
|
},
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// sc-6607 — the values `<Overlay>` paints one screen-level overlay with.
|
|
2
|
+
//
|
|
3
|
+
// Presentation-free and platform-free, the same way `toast-host.js` is: both
|
|
4
|
+
// hosts render the overlay from the SAME component (`overlay-view.js`), and
|
|
5
|
+
// this module is where the theme becomes numbers. Keeping the maths here makes
|
|
6
|
+
// it unit-testable without a renderer, and makes the scrim/panel contract a
|
|
7
|
+
// thing tests can pin rather than a literal buried in JSX.
|
|
8
|
+
|
|
9
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
10
|
+
|
|
11
|
+
export const OVERLAY_SIZES = Object.freeze(["sm", "md", "lg", "full"]);
|
|
12
|
+
|
|
13
|
+
export const OVERLAY_DEFAULTS = Object.freeze({
|
|
14
|
+
size: "md",
|
|
15
|
+
// Slate-black at 55%: dark enough to mute a busy page behind the panel,
|
|
16
|
+
// light enough that the page still reads as "still there, just behind".
|
|
17
|
+
scrim: "rgba(15, 23, 42, 0.55)",
|
|
18
|
+
// Breathing room between the panel and the screen edge on a phone.
|
|
19
|
+
screenPadding: 16,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// `full` deliberately has no ceiling — a media/PDF preview wants the screen.
|
|
23
|
+
const SIZE_MAX_WIDTH = Object.freeze({
|
|
24
|
+
sm: 360,
|
|
25
|
+
md: 520,
|
|
26
|
+
lg: 760,
|
|
27
|
+
full: null,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export function normaliseOverlaySize(size) {
|
|
31
|
+
return OVERLAY_SIZES.indexOf(size) === -1 ? OVERLAY_DEFAULTS.size : size;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the values one overlay is painted with, from the workspace theme.
|
|
36
|
+
*
|
|
37
|
+
* Returns primitives only, so the panel a widget opens carries the workspace's
|
|
38
|
+
* own surface, radius, spacing and elevation instead of a hard-coded card.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} theme — the resolved workspace theme (`useTheme()` shape).
|
|
41
|
+
* @param {string} size — `sm` | `md` | `lg` | `full`.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveOverlayTokens(theme, size) {
|
|
44
|
+
const safeSize = normaliseOverlaySize(size);
|
|
45
|
+
const base = theme && typeof theme === "object" ? theme : DEFAULT_THEME_TOKENS;
|
|
46
|
+
const fallback = DEFAULT_THEME_TOKENS;
|
|
47
|
+
const colors = base.colors || fallback.colors;
|
|
48
|
+
const radii = base.radii || fallback.radii;
|
|
49
|
+
const spacing = base.spacing || fallback.spacing;
|
|
50
|
+
const elevation = (base.elevation || fallback.elevation).lg || {};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
size: safeSize,
|
|
54
|
+
scrim: OVERLAY_DEFAULTS.scrim,
|
|
55
|
+
surface: colors.surface || fallback.colors.surface,
|
|
56
|
+
radius: safeSize === "full" ? 0 : radii.lg || fallback.radii.lg,
|
|
57
|
+
padding: spacing.lg || fallback.spacing.lg,
|
|
58
|
+
// A `full` overlay runs edge to edge; every other size keeps a margin so
|
|
59
|
+
// the scrim stays visible and the panel reads as a layer, not a screen.
|
|
60
|
+
screenPadding: safeSize === "full" ? 0 : OVERLAY_DEFAULTS.screenPadding,
|
|
61
|
+
maxWidth: SIZE_MAX_WIDTH[safeSize],
|
|
62
|
+
elevation,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// sc-6607 — the ONE implementation behind the `<Overlay>` SDK primitive.
|
|
2
|
+
//
|
|
3
|
+
// A widget that opens a preview, a lightbox, or a confirm dialog used to have
|
|
4
|
+
// no way to paint over the SCREEN: it rendered the panel as an absolutely
|
|
5
|
+
// positioned child of its own root, where the Player's layout containers
|
|
6
|
+
// (`overflow: hidden` cards, scroll containers) clip it and where React Native
|
|
7
|
+
// has no `position: "fixed"` at all. `<Overlay>` fixes that for both hosts at
|
|
8
|
+
// once by rendering into React Native's `Modal`, which leaves the parent's
|
|
9
|
+
// layout on BOTH platforms — react-native-web portals it to `document.body`,
|
|
10
|
+
// native hands it to the OS — while keeping the children in the widget's own
|
|
11
|
+
// React tree, so their state, hooks and WidgetContext all survive.
|
|
12
|
+
//
|
|
13
|
+
// The two platform bindings (`overlay.js` / `overlay.native.js`) differ ONLY in
|
|
14
|
+
// where the primitives come from; the component itself is defined once here so
|
|
15
|
+
// the hosts cannot drift (CLAUDE.md §3, §8).
|
|
16
|
+
|
|
17
|
+
import React from "react";
|
|
18
|
+
import { useHostTheme } from "./hooks.js";
|
|
19
|
+
import { resolveOverlayTokens } from "./overlay-tokens.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bind the shared overlay component to one platform's primitives.
|
|
23
|
+
*
|
|
24
|
+
* @param {{Modal: any, View: any, Pressable: any, StyleSheet: any}} rn
|
|
25
|
+
*/
|
|
26
|
+
export function makeOverlay(rn) {
|
|
27
|
+
const { Modal, View, Pressable, StyleSheet } = rn;
|
|
28
|
+
|
|
29
|
+
function Overlay({
|
|
30
|
+
visible = false,
|
|
31
|
+
onRequestClose,
|
|
32
|
+
size = "md",
|
|
33
|
+
dismissOnBackdropPress = true,
|
|
34
|
+
accessibilityLabel,
|
|
35
|
+
closeAccessibilityLabel = "Close",
|
|
36
|
+
style,
|
|
37
|
+
children,
|
|
38
|
+
}) {
|
|
39
|
+
const theme = useHostTheme();
|
|
40
|
+
const tokens = React.useMemo(
|
|
41
|
+
() => resolveOverlayTokens(theme, size),
|
|
42
|
+
[theme, size],
|
|
43
|
+
);
|
|
44
|
+
const requestClose = React.useCallback(() => {
|
|
45
|
+
if (typeof onRequestClose === "function") onRequestClose();
|
|
46
|
+
}, [onRequestClose]);
|
|
47
|
+
|
|
48
|
+
// Unmounted while closed: `Modal` appends a host container the moment it
|
|
49
|
+
// renders, so every widget on the page would otherwise leave one behind.
|
|
50
|
+
if (!visible) return null;
|
|
51
|
+
|
|
52
|
+
const backdrop = {
|
|
53
|
+
flex: 1,
|
|
54
|
+
alignItems: "center",
|
|
55
|
+
justifyContent: "center",
|
|
56
|
+
padding: tokens.screenPadding,
|
|
57
|
+
backgroundColor: tokens.scrim,
|
|
58
|
+
};
|
|
59
|
+
const panel = {
|
|
60
|
+
width: "100%",
|
|
61
|
+
// Capped to the padded screen box so a tall panel scrolls inside its own
|
|
62
|
+
// ScrollView instead of overflowing past the viewport unreachably.
|
|
63
|
+
maxHeight: "100%",
|
|
64
|
+
flexShrink: 1,
|
|
65
|
+
...(tokens.maxWidth ? { maxWidth: tokens.maxWidth } : { flex: 1 }),
|
|
66
|
+
backgroundColor: tokens.surface,
|
|
67
|
+
borderRadius: tokens.radius,
|
|
68
|
+
padding: tokens.padding,
|
|
69
|
+
...tokens.elevation,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return React.createElement(
|
|
73
|
+
Modal,
|
|
74
|
+
{
|
|
75
|
+
visible: true,
|
|
76
|
+
transparent: true,
|
|
77
|
+
// Not animated on purpose: the web host arms the overlay's dialog role
|
|
78
|
+
// and its Escape handler when the entrance animation ENDS, so a fade
|
|
79
|
+
// leaves a window where the keyboard cannot dismiss it — and a dropped
|
|
80
|
+
// animation event would leave it un-dismissable for good.
|
|
81
|
+
animationType: "none",
|
|
82
|
+
onRequestClose: requestClose,
|
|
83
|
+
},
|
|
84
|
+
React.createElement(
|
|
85
|
+
View,
|
|
86
|
+
{ style: backdrop },
|
|
87
|
+
dismissOnBackdropPress
|
|
88
|
+
? React.createElement(Pressable, {
|
|
89
|
+
style: StyleSheet.absoluteFill,
|
|
90
|
+
onPress: requestClose,
|
|
91
|
+
accessibilityRole: "button",
|
|
92
|
+
accessibilityLabel: closeAccessibilityLabel,
|
|
93
|
+
})
|
|
94
|
+
: null,
|
|
95
|
+
React.createElement(
|
|
96
|
+
View,
|
|
97
|
+
{
|
|
98
|
+
style: [panel, style],
|
|
99
|
+
accessibilityLabel,
|
|
100
|
+
},
|
|
101
|
+
children,
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
Overlay.displayName = "Overlay";
|
|
108
|
+
return Overlay;
|
|
109
|
+
}
|
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// sc-6607 — `<Overlay>` (web). Binds the shared implementation in
|
|
2
|
+
// ./overlay-view.js to react-native-web's primitives.
|
|
3
|
+
//
|
|
4
|
+
// The import is `react-native-web` rather than the bare `react-native`
|
|
5
|
+
// specifier for the rolldown optional-peer-dep reason spelled out at the top of
|
|
6
|
+
// ./primitives.js. react-native-web's `Modal` portals its subtree into a
|
|
7
|
+
// `document.body` container at `position: fixed`, which is exactly what makes
|
|
8
|
+
// the overlay escape the widget's clipping ancestors.
|
|
9
|
+
|
|
10
|
+
import * as ReactNative from "react-native-web";
|
|
11
|
+
import { makeOverlay } from "./overlay-view.js";
|
|
12
|
+
|
|
13
|
+
export const Overlay = makeOverlay(ReactNative);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// sc-6607 — `<Overlay>` (native). Binds the shared implementation in
|
|
2
|
+
// ./overlay-view.js to React Native's own primitives.
|
|
3
|
+
//
|
|
4
|
+
// RN's `Modal` renders outside the parent's layout and routes the Android
|
|
5
|
+
// hardware back button to `onRequestClose`, matching what react-native-web's
|
|
6
|
+
// portal does on the web (./overlay.js). Only the import differs.
|
|
7
|
+
|
|
8
|
+
import { Modal, View, Pressable, StyleSheet } from "react-native";
|
|
9
|
+
import { makeOverlay } from "./overlay-view.js";
|
|
10
|
+
|
|
11
|
+
export const Overlay = makeOverlay({ Modal, View, Pressable, StyleSheet });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.116.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node scripts/build.js",
|
|
51
|
-
"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-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__/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__/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-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js"
|
|
51
|
+
"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-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__/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-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|