@colixsystems/widget-sdk 0.23.0 → 0.25.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 +22 -1
- package/dist/contract.cjs +74 -1
- package/dist/contract.js +156 -36
- package/dist/hooks.js +107 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
18
18
|
| Group | Hook (signature) | Returns | Reads / scope |
|
|
19
19
|
| ----- | ---------------- | ------- | ------------- |
|
|
20
20
|
| **CORE** | `useTheme()` | `{ colors, spacing, radii, typography }` | `ctx.workspace.theme` — no scope |
|
|
21
|
+
| **CORE** | `useWidgetStyle()` | `{ [styleField]: value }` | `ctx.props.style` — no scope. The author-set per-widget style values declared in `manifest.styleSchema`; apply each onto whatever element you choose. |
|
|
21
22
|
| **CORE** | `useUser()` | `{ id, email, display_name, roles, group_ids }` | `ctx.user` (snake_case verbatim; `id` null when anonymous) — no scope |
|
|
22
23
|
| **CORE** | `useNavigation()` | `{ goTo, goBack, push, replace, back, currentRoute }` | `ctx.navigation` — no scope (external URLs use the `Linking` primitive) |
|
|
23
24
|
| **CORE** | `useWidgetEvent(name)` | `(payload?) => void` | `ctx.events.emit` — no scope |
|
|
@@ -45,7 +46,27 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
45
46
|
|
|
46
47
|
## Status
|
|
47
48
|
|
|
48
|
-
`v0.
|
|
49
|
+
`v0.25.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**.
|
|
50
|
+
|
|
51
|
+
### What's new in 0.25.0
|
|
52
|
+
|
|
53
|
+
**Per-widget styling via `styleSchema` (REQ-THEME-13).**
|
|
54
|
+
|
|
55
|
+
- **New optional `manifest.styleSchema` field.** Same shape and types as `propertySchema` (`color`, `number`, `select`, `boolean`, …) — it declares the styling options the widget exposes. Example:
|
|
56
|
+
```js
|
|
57
|
+
styleSchema: {
|
|
58
|
+
cardBackground: { type: "color", label: "Card background" },
|
|
59
|
+
cardRadius: { type: "number", label: "Corner radius", validation: { min: 0, max: 48 } },
|
|
60
|
+
valueColor: { type: "color", label: "Value color" },
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
- **The Studio renders a "Style" section** from `styleSchema` using the same `SchemaForm` engine as `propertySchema`. The author's resolved values are persisted under the node's `props.style`.
|
|
64
|
+
- **New `useWidgetStyle()` hook** returns `props.style` (an object keyed by your style-field names). Read `style.<field>` and apply each onto whatever element you choose — the host never auto-applies style, so the widget controls placement:
|
|
65
|
+
```jsx
|
|
66
|
+
const style = useWidgetStyle();
|
|
67
|
+
<View style={[styles.card, style.cardBackground && { backgroundColor: style.cardBackground }]}>
|
|
68
|
+
```
|
|
69
|
+
- **`CONTRACT.version` → `1.15.0`** (additive: one optional manifest field + one hook). No existing field, hook, primitive, or token changed shape.
|
|
49
70
|
|
|
50
71
|
### What's new in 0.23.0
|
|
51
72
|
|
package/dist/contract.cjs
CHANGED
|
@@ -47,6 +47,23 @@ const HOOKS = [
|
|
|
47
47
|
requiredContextSlice: ["workspace.theme"],
|
|
48
48
|
scopes: null,
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
name: "useWidgetStyle",
|
|
52
|
+
signature: "useWidgetStyle()",
|
|
53
|
+
description:
|
|
54
|
+
"Returns the author-set per-widget style values (REQ-THEME-13) — the " +
|
|
55
|
+
"resolved object the host delivers under props.style, keyed by the " +
|
|
56
|
+
"style-field names the widget declares in manifest.styleSchema. Read " +
|
|
57
|
+
"props.style.<field> and apply each onto whatever element you choose " +
|
|
58
|
+
"(spread AFTER your intrinsic styles so author choices win). Returns an " +
|
|
59
|
+
"empty object when nothing is set, so reads are always safe. Equivalent " +
|
|
60
|
+
"to reading the `style` prop directly.",
|
|
61
|
+
returnShape: {
|
|
62
|
+
"<styleFieldName>": "the author-set value (string/number/… per the styleSchema def)",
|
|
63
|
+
},
|
|
64
|
+
requiredContextSlice: ["props"],
|
|
65
|
+
scopes: null,
|
|
66
|
+
},
|
|
50
67
|
{
|
|
51
68
|
name: "useI18n",
|
|
52
69
|
signature: "useI18n()",
|
|
@@ -314,6 +331,31 @@ const HOOKS = [
|
|
|
314
331
|
requiredContextSlice: ["datastore.records"],
|
|
315
332
|
scopes: ["acl.write:records"],
|
|
316
333
|
},
|
|
334
|
+
// REQ-RT-07 — realtime table subscription.
|
|
335
|
+
{
|
|
336
|
+
name: "useDatastoreSubscription",
|
|
337
|
+
signature: "useDatastoreSubscription(tableId, handlers, options?)",
|
|
338
|
+
description:
|
|
339
|
+
"Subscribe to a table's realtime change stream via the injected " +
|
|
340
|
+
"datastore-client at ctx.datastore.records(tableId).subscribe(...). " +
|
|
341
|
+
"handlers is { onCreated?, onUpdated?, onDeleted? }; each receives the " +
|
|
342
|
+
"snake_case record verbatim (same shape REST returns). The socket opens " +
|
|
343
|
+
"on mount, re-opens when tableId changes, and is torn down on unmount. " +
|
|
344
|
+
"Returns { status } where status is 'connecting' | 'live' | " +
|
|
345
|
+
"'reconnecting' | 'fallback'. The server gates each subscribe by the " +
|
|
346
|
+
"same read ACL REST honours; on an ACL reject, a missing WebSocket, or a " +
|
|
347
|
+
"host whose client predates realtime, the hook resolves to " +
|
|
348
|
+
"{ status: 'fallback' } WITHOUT throwing so the widget can poll instead. " +
|
|
349
|
+
"Reads the same datastore.read scope as useDatastoreQuery — declare " +
|
|
350
|
+
"datastore.read for the table you subscribe to. Pair with " +
|
|
351
|
+
"useDatastoreQuery for the initial load and merge the streamed envelopes.",
|
|
352
|
+
returnShape: {
|
|
353
|
+
status:
|
|
354
|
+
"'connecting' | 'live' | 'reconnecting' | 'fallback' // transport state; 'fallback' → poll",
|
|
355
|
+
},
|
|
356
|
+
requiredContextSlice: ["datastore.records"],
|
|
357
|
+
scopes: ["datastore.read:<table>"],
|
|
358
|
+
},
|
|
317
359
|
// REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
|
|
318
360
|
{
|
|
319
361
|
name: "useClipboard",
|
|
@@ -576,6 +618,20 @@ const MANIFEST_SCHEMA = {
|
|
|
576
618
|
description: "Map of prop name to property definition.",
|
|
577
619
|
default: {},
|
|
578
620
|
},
|
|
621
|
+
styleSchema: {
|
|
622
|
+
type: "object",
|
|
623
|
+
required: false,
|
|
624
|
+
description:
|
|
625
|
+
"Optional (REQ-THEME-13). Map of style-field name to definition — the " +
|
|
626
|
+
"SAME shape and types as propertySchema (color, number, select, " +
|
|
627
|
+
"boolean, …). Declares the per-instance styling options the widget " +
|
|
628
|
+
"exposes; the Studio Properties Panel renders a \"Style\" section from " +
|
|
629
|
+
"it. The author's resolved values are delivered to the widget under " +
|
|
630
|
+
"props.style (one object keyed by style-field name); the widget reads " +
|
|
631
|
+
"props.style.<field> and applies each wherever it chooses. The host " +
|
|
632
|
+
"never auto-applies style — the widget owns placement.",
|
|
633
|
+
default: {},
|
|
634
|
+
},
|
|
579
635
|
events: {
|
|
580
636
|
type: "object[]",
|
|
581
637
|
required: true,
|
|
@@ -1136,7 +1192,24 @@ const CONTRACT = deepFreeze({
|
|
|
1136
1192
|
// is additive — minor bump on the pre-1.0 channel. The compiler pins the
|
|
1137
1193
|
// native-module members in the exported Expo app's package.json and now
|
|
1138
1194
|
// always emits the react-native-reanimated/plugin (CLAUDE.md §3 parity).
|
|
1139
|
-
|
|
1195
|
+
//
|
|
1196
|
+
// 1.14.0: additive (REQ-RT-07) — new `useDatastoreSubscription(tableId,
|
|
1197
|
+
// handlers, options?)` hook reading ctx.datastore.records(t).subscribe.
|
|
1198
|
+
// Backed by the datastore-client 0.6.0 `subscribe` method (WebSocket to
|
|
1199
|
+
// `/datastore/ws`). Safe-by-default: resolves to { status: 'fallback' }
|
|
1200
|
+
// when the host's client predates realtime or no WebSocket is available,
|
|
1201
|
+
// so widgets degrade to polling on both platforms. No existing hook,
|
|
1202
|
+
// primitive, or contract field changed — minor bump on the pre-1.0 channel.
|
|
1203
|
+
//
|
|
1204
|
+
// 1.15.0: additive (REQ-THEME-13) — per-widget styling via `styleSchema`. New
|
|
1205
|
+
// optional manifest field `styleSchema`: the SAME shape as propertySchema
|
|
1206
|
+
// (color/number/select/… fields), declaring the per-instance styling
|
|
1207
|
+
// options a widget exposes. The Studio renders a "Style" section from it;
|
|
1208
|
+
// the resolved values are delivered to the widget under props.style and
|
|
1209
|
+
// the widget applies them itself (the host never auto-styles). New
|
|
1210
|
+
// `useWidgetStyle()` hook returns props.style. No existing field, hook,
|
|
1211
|
+
// primitive, or token changed shape — minor bump on the pre-1.0 channel.
|
|
1212
|
+
version: "1.15.0",
|
|
1140
1213
|
hooks: HOOKS,
|
|
1141
1214
|
primitives: PRIMITIVES,
|
|
1142
1215
|
manifestSchema: MANIFEST_SCHEMA,
|
package/dist/contract.js
CHANGED
|
@@ -47,6 +47,23 @@ const HOOKS = [
|
|
|
47
47
|
requiredContextSlice: ["workspace.theme"],
|
|
48
48
|
scopes: null,
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
name: "useWidgetStyle",
|
|
52
|
+
signature: "useWidgetStyle()",
|
|
53
|
+
description:
|
|
54
|
+
"Returns the author-set per-widget style values (REQ-THEME-13) — the " +
|
|
55
|
+
"resolved object the host delivers under props.style, keyed by the " +
|
|
56
|
+
"style-field names the widget declares in manifest.styleSchema. Read " +
|
|
57
|
+
"props.style.<field> and apply each onto whatever element you choose " +
|
|
58
|
+
"(spread AFTER your intrinsic styles so author choices win). Returns an " +
|
|
59
|
+
"empty object when nothing is set, so reads are always safe. Equivalent " +
|
|
60
|
+
"to reading the `style` prop directly.",
|
|
61
|
+
returnShape: {
|
|
62
|
+
"<styleFieldName>": "the author-set value (string/number/… per the styleSchema def)",
|
|
63
|
+
},
|
|
64
|
+
requiredContextSlice: ["props"],
|
|
65
|
+
scopes: null,
|
|
66
|
+
},
|
|
50
67
|
{
|
|
51
68
|
name: "useI18n",
|
|
52
69
|
signature: "useI18n()",
|
|
@@ -177,8 +194,7 @@ const HOOKS = [
|
|
|
177
194
|
signature: "useDatastoreMutation(tableId)",
|
|
178
195
|
returnShape: {
|
|
179
196
|
create: "(record) => Promise<Record> // rejects with DatastoreError",
|
|
180
|
-
update:
|
|
181
|
-
"(id, partial) => Promise<Record> // rejects with DatastoreError",
|
|
197
|
+
update: "(id, partial) => Promise<Record> // rejects with DatastoreError",
|
|
182
198
|
delete: "(id) => Promise<void> // rejects with DatastoreError",
|
|
183
199
|
},
|
|
184
200
|
requiredContextSlice: ["datastore.records"],
|
|
@@ -281,13 +297,7 @@ const HOOKS = [
|
|
|
281
297
|
scopes: ["groups.read:*"],
|
|
282
298
|
},
|
|
283
299
|
// REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
|
|
284
|
-
// management for a single record.
|
|
285
|
-
// ctx.datastore.records(tableId).permissions(recordId) (the recordPermissions
|
|
286
|
-
// facade was folded into the datastore-client). The backend gates the call
|
|
287
|
-
// on `can_grant` for the target record (Studio owners short-circuit;
|
|
288
|
-
// APP_USER actors must hold `can_grant` via REQ-ACL-05 / REQ-ACL-06). A
|
|
289
|
-
// widget that declares the scope but whose caller lacks the grant receives
|
|
290
|
-
// `PermissionError { code: 'FORBIDDEN' }`.
|
|
300
|
+
// management for a single record. Mirror of contract.js.
|
|
291
301
|
{
|
|
292
302
|
name: "useRecordPermissions",
|
|
293
303
|
signature: "useRecordPermissions(tableId, recordId)",
|
|
@@ -321,7 +331,32 @@ const HOOKS = [
|
|
|
321
331
|
requiredContextSlice: ["datastore.records"],
|
|
322
332
|
scopes: ["acl.write:records"],
|
|
323
333
|
},
|
|
324
|
-
// REQ-
|
|
334
|
+
// REQ-RT-07 — realtime table subscription.
|
|
335
|
+
{
|
|
336
|
+
name: "useDatastoreSubscription",
|
|
337
|
+
signature: "useDatastoreSubscription(tableId, handlers, options?)",
|
|
338
|
+
description:
|
|
339
|
+
"Subscribe to a table's realtime change stream via the injected " +
|
|
340
|
+
"datastore-client at ctx.datastore.records(tableId).subscribe(...). " +
|
|
341
|
+
"handlers is { onCreated?, onUpdated?, onDeleted? }; each receives the " +
|
|
342
|
+
"snake_case record verbatim (same shape REST returns). The socket opens " +
|
|
343
|
+
"on mount, re-opens when tableId changes, and is torn down on unmount. " +
|
|
344
|
+
"Returns { status } where status is 'connecting' | 'live' | " +
|
|
345
|
+
"'reconnecting' | 'fallback'. The server gates each subscribe by the " +
|
|
346
|
+
"same read ACL REST honours; on an ACL reject, a missing WebSocket, or a " +
|
|
347
|
+
"host whose client predates realtime, the hook resolves to " +
|
|
348
|
+
"{ status: 'fallback' } WITHOUT throwing so the widget can poll instead. " +
|
|
349
|
+
"Reads the same datastore.read scope as useDatastoreQuery — declare " +
|
|
350
|
+
"datastore.read for the table you subscribe to. Pair with " +
|
|
351
|
+
"useDatastoreQuery for the initial load and merge the streamed envelopes.",
|
|
352
|
+
returnShape: {
|
|
353
|
+
status:
|
|
354
|
+
"'connecting' | 'live' | 'reconnecting' | 'fallback' // transport state; 'fallback' → poll",
|
|
355
|
+
},
|
|
356
|
+
requiredContextSlice: ["datastore.records"],
|
|
357
|
+
scopes: ["datastore.read:<table>"],
|
|
358
|
+
},
|
|
359
|
+
// REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
|
|
325
360
|
{
|
|
326
361
|
name: "useClipboard",
|
|
327
362
|
signature: "useClipboard()",
|
|
@@ -359,7 +394,13 @@ const HOOKS = [
|
|
|
359
394
|
},
|
|
360
395
|
];
|
|
361
396
|
|
|
362
|
-
// REQ-WSDK-RN-WEB:
|
|
397
|
+
// REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
|
|
398
|
+
// (backed by react-native-web on the browser, by react-native on the
|
|
399
|
+
// exported Expo app). The contract describes the *cross-platform
|
|
400
|
+
// subset* the SDK re-exports; the per-prop reference for each
|
|
401
|
+
// component is whatever React Native documents. Adding a new primitive
|
|
402
|
+
// = add the name to `./primitives.js` + `./primitives.native.js` (both
|
|
403
|
+
// one-line re-exports) and append it to this list.
|
|
363
404
|
const PRIMITIVES = [
|
|
364
405
|
{
|
|
365
406
|
name: "Text",
|
|
@@ -577,6 +618,20 @@ const MANIFEST_SCHEMA = {
|
|
|
577
618
|
description: "Map of prop name to property definition.",
|
|
578
619
|
default: {},
|
|
579
620
|
},
|
|
621
|
+
styleSchema: {
|
|
622
|
+
type: "object",
|
|
623
|
+
required: false,
|
|
624
|
+
description:
|
|
625
|
+
"Optional (REQ-THEME-13). Map of style-field name to definition — the " +
|
|
626
|
+
"SAME shape and types as propertySchema (color, number, select, " +
|
|
627
|
+
"boolean, …). Declares the per-instance styling options the widget " +
|
|
628
|
+
"exposes; the Studio Properties Panel renders a \"Style\" section from " +
|
|
629
|
+
"it. The author's resolved values are delivered to the widget under " +
|
|
630
|
+
"props.style (one object keyed by style-field name); the widget reads " +
|
|
631
|
+
"props.style.<field> and applies each wherever it chooses. The host " +
|
|
632
|
+
"never auto-applies style — the widget owns placement.",
|
|
633
|
+
default: {},
|
|
634
|
+
},
|
|
580
635
|
events: {
|
|
581
636
|
type: "object[]",
|
|
582
637
|
required: true,
|
|
@@ -611,7 +666,8 @@ const MANIFEST_SCHEMA = {
|
|
|
611
666
|
|
|
612
667
|
const WIDGET_CONTEXT_SHAPE = {
|
|
613
668
|
props: {
|
|
614
|
-
description:
|
|
669
|
+
description:
|
|
670
|
+
"Resolved property values, shape per manifest.propertySchema.",
|
|
615
671
|
required: true,
|
|
616
672
|
fields: {},
|
|
617
673
|
},
|
|
@@ -725,10 +781,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
725
781
|
error: "function",
|
|
726
782
|
},
|
|
727
783
|
},
|
|
728
|
-
// REQ-WSDK-PLATFORM §6 — backs useToast().
|
|
729
|
-
// workspace-themed toast renderer here; if omitted, the SDK's useToast
|
|
730
|
-
// hook falls back to a CustomEvent on web / console.log on native so
|
|
731
|
-
// widget code still runs without a host integration.
|
|
784
|
+
// REQ-WSDK-PLATFORM §6 — backs useToast(). Mirror of contract.js.
|
|
732
785
|
toast: {
|
|
733
786
|
description:
|
|
734
787
|
"Optional host toast slot. { showToast({ kind, message }): void }. " +
|
|
@@ -762,8 +815,14 @@ const BUNDLE_EXPORT_CONTRACT = [
|
|
|
762
815
|
];
|
|
763
816
|
|
|
764
817
|
// REQ-WSDK-PLATFORM (docs/design/req-widget-sdk-cross-platform-primitives.md
|
|
765
|
-
// §3.5, §8): `fetch` and `XMLHttpRequest` are NOT banned.
|
|
766
|
-
//
|
|
818
|
+
// §3.5, §8): `fetch` and `XMLHttpRequest` are NOT banned. Widgets may call
|
|
819
|
+
// third-party APIs directly. Same-origin requests to the host's own
|
|
820
|
+
// `/api/*` surface are rejected at runtime by the WidgetContextProvider's
|
|
821
|
+
// network gate (`no host-api access from widgets`) — the JWT token is
|
|
822
|
+
// never shared with widget code, so the call would 401 anyway; the runtime
|
|
823
|
+
// gate makes the failure mode "blocked" instead of "401 noise". A soft
|
|
824
|
+
// linter warning (`no-host-api-url`) flags obvious host-URL substrings at
|
|
825
|
+
// submission so authors learn the rule statically.
|
|
767
826
|
const BANNED_APIS = [
|
|
768
827
|
{ identifier: "eval", reason: "Arbitrary code evaluation." },
|
|
769
828
|
{
|
|
@@ -798,8 +857,22 @@ const BANNED_APIS = [
|
|
|
798
857
|
{ identifier: "globalThis", reason: "Host environment escape." },
|
|
799
858
|
];
|
|
800
859
|
|
|
801
|
-
// REQ-WSDK-PLATFORM §3.4, §5: vetted package allowlist.
|
|
802
|
-
//
|
|
860
|
+
// REQ-WSDK-PLATFORM §3.4, §5: vetted package allowlist. Each entry is a
|
|
861
|
+
// specifier the widget bundle may import as a bare module specifier. The
|
|
862
|
+
// linter validates every `import ... from "<spec>"` line against this
|
|
863
|
+
// list; specifiers not on the list fail the lint. The compiler reads it
|
|
864
|
+
// to know which packages to add to the exported Expo app's package.json.
|
|
865
|
+
//
|
|
866
|
+
// `platforms` is one or both of "web" / "native". A widget that imports a
|
|
867
|
+
// native-only package implicitly drops "web" from the package's effective
|
|
868
|
+
// `supportedPlatforms` (and vice versa) — the marketplace upload pipeline
|
|
869
|
+
// surfaces the derived set so the listing shows honest badges.
|
|
870
|
+
//
|
|
871
|
+
// Adding a package is a CONTRACT change. Review burden: confirm the
|
|
872
|
+
// package does what it claims, has a credible maintainer, and the
|
|
873
|
+
// import shape (named exports, default export) is stable. After that,
|
|
874
|
+
// every widget using the package inherits the review — the marketplace
|
|
875
|
+
// reviewer only spot-checks usage, not the package source.
|
|
803
876
|
const VETTED_IMPORTS = [
|
|
804
877
|
{
|
|
805
878
|
specifier: "react",
|
|
@@ -981,8 +1054,19 @@ const VETTED_IMPORTS = [
|
|
|
981
1054
|
},
|
|
982
1055
|
];
|
|
983
1056
|
|
|
1057
|
+
// Back-compat shape — every existing consumer (widgetLoader, the static
|
|
1058
|
+
// analyzer's DEPENDENCY_ALLOWLIST, the AI prompt's "Allowed imports" line)
|
|
1059
|
+
// reads a plain `string[]` from `CONTRACT.allowedBareImports`. Derive it
|
|
1060
|
+
// from the rich list so a single edit in VETTED_IMPORTS propagates to
|
|
1061
|
+
// every surface.
|
|
984
1062
|
const ALLOWED_BARE_IMPORTS = VETTED_IMPORTS.map((v) => v.specifier);
|
|
985
1063
|
|
|
1064
|
+
// REQ-WSDK-PLATFORM §3.5: host-API URL patterns the linter scans for as a
|
|
1065
|
+
// soft warning. None of these block the lint by themselves — they prompt
|
|
1066
|
+
// the marketplace reviewer to spot-check, and they help authors learn the
|
|
1067
|
+
// rule statically. Strings appear as literal substring matches (the URL
|
|
1068
|
+
// is reachable in widget code by composing it at runtime, so this is
|
|
1069
|
+
// best-effort).
|
|
986
1070
|
const HOST_API_URL_PATTERNS = [
|
|
987
1071
|
"/api/v1",
|
|
988
1072
|
"/uploads/",
|
|
@@ -1011,24 +1095,43 @@ function widgetTranslationKey(id, key) {
|
|
|
1011
1095
|
}
|
|
1012
1096
|
|
|
1013
1097
|
const CONTRACT = deepFreeze({
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1098
|
+
// REQ-WSDK-PLATFORM bump:
|
|
1099
|
+
// - `vettedImports` is a new field (rich allowlist with platforms +
|
|
1100
|
+
// category + description).
|
|
1101
|
+
// - `hostApiUrlPatterns` is a new field (soft-warning substrings).
|
|
1102
|
+
// - `bannedApis` no longer lists `fetch` / `XMLHttpRequest`.
|
|
1103
|
+
// - `allowedBareImports` is now derived from `vettedImports` (same
|
|
1104
|
+
// shape; same contents grow with each vetted addition).
|
|
1105
|
+
// Permissive-direction change: minor bump on the contract's own
|
|
1106
|
+
// versioning (per CLAUDE.md §4, pre-1.0 minor is the breaking channel —
|
|
1107
|
+
// the package.json version bumps accordingly).
|
|
1108
|
+
//
|
|
1109
|
+
// 1.6.0: additive — `themeTokens.colors` gains `secondary` + `onSecondary`
|
|
1110
|
+
// so the tenant's Theme Settings "Secondary Color" flows through
|
|
1111
|
+
// `useTheme().colors.secondary` (Button secondary variant + any widget
|
|
1112
|
+
// that wants the brand's second accent).
|
|
1113
|
+
//
|
|
1114
|
+
// 1.7.0: additive — new `useDatastoreSchema(tableId)` hook + the
|
|
1115
|
+
// `datastore.schema` host-context slice it reads. Lets a widget resolve a
|
|
1116
|
+
// stored columnId to its column name / dataType / relation target at
|
|
1117
|
+
// runtime (Form Builder renders inputs by column type). Reads the existing
|
|
1118
|
+
// ACL-gated `GET /tables/:id` — structure only, no row data.
|
|
1017
1119
|
//
|
|
1018
1120
|
// 1.8.0: two additive features.
|
|
1019
|
-
// - REQ-WIDGET-ACTION — manifests may declare an optional `actions` array
|
|
1020
|
-
// server-side cron
|
|
1021
|
-
// tenant
|
|
1022
|
-
// rendered app
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
//
|
|
1026
|
-
//
|
|
1121
|
+
// - REQ-WIDGET-ACTION — manifests may declare an optional `actions` array.
|
|
1122
|
+
// Each entry is a server-side action (cron- or record-triggered JS) the
|
|
1123
|
+
// operator enables per tenant; it runs in the existing isolated-vm action
|
|
1124
|
+
// runner, never in the rendered app. New contract fields
|
|
1125
|
+
// `actionTriggerTypes`, `actionScriptGlobals`, and `actionScriptMaxBytes`
|
|
1126
|
+
// let the docs + agent prompt + validator derive the grammar from one
|
|
1127
|
+
// source. New `ADMINISTRATION` manifest category (REQ-USERMGMT-06).
|
|
1128
|
+
// - REQ-ACL-06 — new `useRecordPermissions(tableId, recordId)` hook + the
|
|
1129
|
+
// `recordPermissions` host-context slice it reads + the `acl.write:records`
|
|
1027
1130
|
// scope it gates on. Hits the existing REQ-ACL-06
|
|
1028
|
-
// /api/v1/tables/:tableId/records/:recordId/permissions REST surface;
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1131
|
+
// /api/v1/tables/:tableId/records/:recordId/permissions REST surface; the
|
|
1132
|
+
// host normalises the wire shape into a single principalType+principalId
|
|
1133
|
+
// pair widgets branch on. Caller still needs canGrant on the target
|
|
1134
|
+
// record (REQ-ACL-RELINHERIT-05).
|
|
1032
1135
|
//
|
|
1033
1136
|
// 1.9.0: host-contract change (REQ-WSDK-DOMAIN-CLIENTS) — the host now
|
|
1034
1137
|
// injects domain-client INSTANCES on the WidgetContext rather than
|
|
@@ -1089,7 +1192,24 @@ const CONTRACT = deepFreeze({
|
|
|
1089
1192
|
// is additive — minor bump on the pre-1.0 channel. The compiler pins the
|
|
1090
1193
|
// native-module members in the exported Expo app's package.json and now
|
|
1091
1194
|
// always emits the react-native-reanimated/plugin (CLAUDE.md §3 parity).
|
|
1092
|
-
|
|
1195
|
+
//
|
|
1196
|
+
// 1.14.0: additive (REQ-RT-07) — new `useDatastoreSubscription(tableId,
|
|
1197
|
+
// handlers, options?)` hook reading ctx.datastore.records(t).subscribe.
|
|
1198
|
+
// Backed by the datastore-client 0.6.0 `subscribe` method (WebSocket to
|
|
1199
|
+
// `/datastore/ws`). Safe-by-default: resolves to { status: 'fallback' }
|
|
1200
|
+
// when the host's client predates realtime or no WebSocket is available,
|
|
1201
|
+
// so widgets degrade to polling on both platforms. No existing hook,
|
|
1202
|
+
// primitive, or contract field changed — minor bump on the pre-1.0 channel.
|
|
1203
|
+
//
|
|
1204
|
+
// 1.15.0: additive (REQ-THEME-13) — per-widget styling via `styleSchema`. New
|
|
1205
|
+
// optional manifest field `styleSchema`: the SAME shape as propertySchema
|
|
1206
|
+
// (color/number/select/… fields), declaring the per-instance styling
|
|
1207
|
+
// options a widget exposes. The Studio renders a "Style" section from it;
|
|
1208
|
+
// the resolved values are delivered to the widget under props.style and
|
|
1209
|
+
// the widget applies them itself (the host never auto-styles). New
|
|
1210
|
+
// `useWidgetStyle()` hook returns props.style. No existing field, hook,
|
|
1211
|
+
// primitive, or token changed shape — minor bump on the pre-1.0 channel.
|
|
1212
|
+
version: "1.15.0",
|
|
1093
1213
|
hooks: HOOKS,
|
|
1094
1214
|
primitives: PRIMITIVES,
|
|
1095
1215
|
manifestSchema: MANIFEST_SCHEMA,
|
package/dist/hooks.js
CHANGED
|
@@ -83,6 +83,26 @@ export function useTheme() {
|
|
|
83
83
|
return ctx.workspace.theme;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* REQ-THEME-13 — returns the author-set per-widget style values: the object the
|
|
88
|
+
* host delivers under `props.style`, keyed by the style-field names the widget
|
|
89
|
+
* declares in its `manifest.styleSchema`. Read `style.<field>` and apply each
|
|
90
|
+
* onto whatever element you choose (spread AFTER your intrinsic styles so the
|
|
91
|
+
* author's choice wins):
|
|
92
|
+
*
|
|
93
|
+
* const style = useWidgetStyle();
|
|
94
|
+
* <View style={[styles.card, style.cardBackground && { backgroundColor: style.cardBackground }]}>
|
|
95
|
+
*
|
|
96
|
+
* Returns an empty object when nothing is set, so reads are always safe. This
|
|
97
|
+
* is equivalent to reading the `style` prop directly — the hook is a
|
|
98
|
+
* convenience so a widget needn't thread `style` through its prop signature.
|
|
99
|
+
*/
|
|
100
|
+
export function useWidgetStyle() {
|
|
101
|
+
const ctx = useWidgetContextOrThrow("useWidgetStyle");
|
|
102
|
+
const style = ctx.props ? ctx.props.style : undefined;
|
|
103
|
+
return style && typeof style === "object" ? style : {};
|
|
104
|
+
}
|
|
105
|
+
|
|
86
106
|
/**
|
|
87
107
|
* Returns the active end-user identity VERBATIM from the host, e.g.
|
|
88
108
|
* `{ id, email, display_name, roles, group_ids }` (snake_case fields).
|
|
@@ -921,6 +941,93 @@ export function useRecordPermissions(tableId, recordId) {
|
|
|
921
941
|
return { permissions, loading, error, grant, revoke, update, refetch };
|
|
922
942
|
}
|
|
923
943
|
|
|
944
|
+
/**
|
|
945
|
+
* REQ-RT-07: subscribe to a table's realtime change stream. Reads
|
|
946
|
+
* `ctx.datastore.records(table).subscribe({ onCreated, onUpdated, onDeleted,
|
|
947
|
+
* onStatus })` and wires it to a React lifecycle: the socket is opened on
|
|
948
|
+
* mount (and re-opened when `table` changes) and torn down on unmount via the
|
|
949
|
+
* unsubscribe function the client returns. The author's `handlers` callbacks
|
|
950
|
+
* are held in a ref so re-renders with fresh callback identities never tear
|
|
951
|
+
* the socket down — only a `table` change does.
|
|
952
|
+
*
|
|
953
|
+
* Returns `{ status }` where status is "connecting" | "live" | "reconnecting"
|
|
954
|
+
* | "fallback". A widget typically pairs this with `useDatastoreQuery` for the
|
|
955
|
+
* initial load and merges the streamed create/update/delete envelopes into its
|
|
956
|
+
* local list; when `status === "fallback"` (no socket support on the host, the
|
|
957
|
+
* subscribe was ACL-rejected, or the connect timed out) the widget should run
|
|
958
|
+
* its own REST polling instead.
|
|
959
|
+
*
|
|
960
|
+
* Cross-platform + safe-by-default: if the host's datastore client doesn't
|
|
961
|
+
* expose `subscribe` (an older host, or a runtime with no WebSocket), the hook
|
|
962
|
+
* resolves to `{ status: "fallback" }` WITHOUT throwing, so a widget that
|
|
963
|
+
* subscribes degrades to polling on both the web Player and the native export
|
|
964
|
+
* rather than crashing at render (CLAUDE.md §11).
|
|
965
|
+
*
|
|
966
|
+
* @param {string} table Bound table id (falsy → no subscription, status "fallback").
|
|
967
|
+
* @param {{ onCreated?, onUpdated?, onDeleted? }} [handlers] Per-event callbacks; each receives the snake_case record.
|
|
968
|
+
* @param {{ fallbackAfterMs?: number }} [options]
|
|
969
|
+
* @returns {{ status: "connecting" | "live" | "reconnecting" | "fallback" }}
|
|
970
|
+
*/
|
|
971
|
+
export function useDatastoreSubscription(table, handlers, options) {
|
|
972
|
+
const ctx = useWidgetContextOrThrow("useDatastoreSubscription");
|
|
973
|
+
const [status, setStatus] = useState("connecting");
|
|
974
|
+
|
|
975
|
+
// Author callbacks live in a ref so a new closure each render does not
|
|
976
|
+
// re-run the effect (which would tear down + re-open the socket).
|
|
977
|
+
const handlersRef = useRef(handlers);
|
|
978
|
+
handlersRef.current = handlers;
|
|
979
|
+
// `ctx` is a fresh object identity per host render; hold the records
|
|
980
|
+
// factory in a ref so the effect depends only on `table`.
|
|
981
|
+
const recordsRef = useRef(
|
|
982
|
+
ctx.datastore && typeof ctx.datastore.records === "function"
|
|
983
|
+
? ctx.datastore.records
|
|
984
|
+
: null,
|
|
985
|
+
);
|
|
986
|
+
recordsRef.current =
|
|
987
|
+
ctx.datastore && typeof ctx.datastore.records === "function"
|
|
988
|
+
? ctx.datastore.records
|
|
989
|
+
: null;
|
|
990
|
+
|
|
991
|
+
const fallbackAfterMs =
|
|
992
|
+
options && Number.isFinite(options.fallbackAfterMs)
|
|
993
|
+
? options.fallbackAfterMs
|
|
994
|
+
: undefined;
|
|
995
|
+
|
|
996
|
+
useEffect(() => {
|
|
997
|
+
if (!table || typeof recordsRef.current !== "function") {
|
|
998
|
+
setStatus("fallback");
|
|
999
|
+
return undefined;
|
|
1000
|
+
}
|
|
1001
|
+
let ns;
|
|
1002
|
+
try {
|
|
1003
|
+
ns = recordsRef.current(table);
|
|
1004
|
+
} catch (_) {
|
|
1005
|
+
setStatus("fallback");
|
|
1006
|
+
return undefined;
|
|
1007
|
+
}
|
|
1008
|
+
if (!ns || typeof ns.subscribe !== "function") {
|
|
1009
|
+
// Host's datastore client predates realtime — degrade to polling.
|
|
1010
|
+
setStatus("fallback");
|
|
1011
|
+
return undefined;
|
|
1012
|
+
}
|
|
1013
|
+
const stop = ns.subscribe(
|
|
1014
|
+
{
|
|
1015
|
+
onCreated: (r) => handlersRef.current?.onCreated?.(r),
|
|
1016
|
+
onUpdated: (r) => handlersRef.current?.onUpdated?.(r),
|
|
1017
|
+
onDeleted: (r) => handlersRef.current?.onDeleted?.(r),
|
|
1018
|
+
onStatus: (s) => setStatus(s),
|
|
1019
|
+
},
|
|
1020
|
+
fallbackAfterMs != null ? { fallbackAfterMs } : undefined,
|
|
1021
|
+
);
|
|
1022
|
+
return () => {
|
|
1023
|
+
if (typeof stop === "function") stop();
|
|
1024
|
+
};
|
|
1025
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1026
|
+
}, [table, fallbackAfterMs]);
|
|
1027
|
+
|
|
1028
|
+
return { status };
|
|
1029
|
+
}
|
|
1030
|
+
|
|
924
1031
|
/* ============================================================================
|
|
925
1032
|
* FILES CLIENT — ctx.files (@colixsystems/files-client)
|
|
926
1033
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -184,6 +184,14 @@ export interface WidgetManifest {
|
|
|
184
184
|
minAppStudioVersion: string;
|
|
185
185
|
requestedScopes: WidgetScope[];
|
|
186
186
|
propertySchema: WidgetPropertySchema;
|
|
187
|
+
/**
|
|
188
|
+
* REQ-THEME-13 — optional per-widget styling schema. Same shape and types as
|
|
189
|
+
* `propertySchema`; declares the styling options the widget exposes. The
|
|
190
|
+
* Studio renders a "Style" section from it and delivers the author's resolved
|
|
191
|
+
* values to the widget under `props.style` (read them via `useWidgetStyle()`
|
|
192
|
+
* or the `style` prop). The widget applies each value itself.
|
|
193
|
+
*/
|
|
194
|
+
styleSchema?: WidgetPropertySchema;
|
|
187
195
|
events: WidgetEventDescriptor[];
|
|
188
196
|
/**
|
|
189
197
|
* Optional datastore template seeded into the tenant's workspace when
|
|
@@ -556,6 +564,14 @@ export function usePayments(): PaymentsApi;
|
|
|
556
564
|
|
|
557
565
|
export function useTheme(): ThemeTokens;
|
|
558
566
|
|
|
567
|
+
/**
|
|
568
|
+
* REQ-THEME-13 — the author-set per-widget style values (the `props.style`
|
|
569
|
+
* object), keyed by the names declared in `manifest.styleSchema`. Returns an
|
|
570
|
+
* empty object when nothing is set. Apply each value onto whatever element you
|
|
571
|
+
* choose; the host never auto-applies style.
|
|
572
|
+
*/
|
|
573
|
+
export function useWidgetStyle(): Record<string, unknown>;
|
|
574
|
+
|
|
559
575
|
/**
|
|
560
576
|
* Stateful single-record fetch hook. Returns `{ data, loading, error,
|
|
561
577
|
* refetch }`. `data` is one row or `null` (never an array).
|
|
@@ -624,6 +640,37 @@ export function useDatastoreSchema(
|
|
|
624
640
|
tableId: string | null | undefined,
|
|
625
641
|
): SchemaResult;
|
|
626
642
|
|
|
643
|
+
// REQ-RT-07 realtime subscription transport state.
|
|
644
|
+
export type DatastoreSubscriptionStatus =
|
|
645
|
+
| "connecting"
|
|
646
|
+
| "live"
|
|
647
|
+
| "reconnecting"
|
|
648
|
+
| "fallback";
|
|
649
|
+
|
|
650
|
+
export interface DatastoreSubscriptionHandlers {
|
|
651
|
+
onCreated?: (record: Record<string, unknown>) => void;
|
|
652
|
+
onUpdated?: (record: Record<string, unknown>) => void;
|
|
653
|
+
onDeleted?: (record: Record<string, unknown>) => void;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export interface DatastoreSubscriptionOptions {
|
|
657
|
+
fallbackAfterMs?: number;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* REQ-RT-07: subscribe to a table's realtime change stream via
|
|
662
|
+
* `ctx.datastore.records(tableId).subscribe(...)`. Opens on mount, re-opens on
|
|
663
|
+
* `tableId` change, tears down on unmount. Returns `{ status }`; when status
|
|
664
|
+
* is `"fallback"` (no socket support, ACL-rejected, or connect timed out) run
|
|
665
|
+
* REST polling instead. Never throws — degrades to `{ status: "fallback" }` on
|
|
666
|
+
* a host whose datastore client predates realtime.
|
|
667
|
+
*/
|
|
668
|
+
export function useDatastoreSubscription(
|
|
669
|
+
tableId: string | null | undefined,
|
|
670
|
+
handlers?: DatastoreSubscriptionHandlers,
|
|
671
|
+
options?: DatastoreSubscriptionOptions,
|
|
672
|
+
): { status: DatastoreSubscriptionStatus };
|
|
673
|
+
|
|
627
674
|
/**
|
|
628
675
|
* Stateful file-asset resolver hook. Returns `{ url, file, loading, error,
|
|
629
676
|
* refetch }`. The `url` is an absolute URL composed against the host's API
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "node scripts/build.js",
|
|
38
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/linter-users-scope.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js"
|
|
38
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=18"
|