@colixsystems/widget-sdk 0.40.2 → 0.41.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 +7 -2
- package/dist/contract.cjs +46 -1
- package/dist/contract.js +46 -1
- package/dist/hooks.js +53 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
13
13
|
|
|
14
14
|
**Wire / casing: snake_case end to end.** The clients send and return snake_case **verbatim** (`created_at`, `group_ids`, `can_read`, `amount_cents`, `data_type`, `is_active`, …). There is **no case transform anywhere** — not on the client and not in the backend; the only casing boundary is Prisma `@map` (snake_case field → camelCase column). Author-defined record column values pass through verbatim. Every `list(...)` returns the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you.
|
|
15
15
|
|
|
16
|
-
**Hooks read the injected clients** — they do not hold their own HTTP. This is the **complete** hook surface (
|
|
16
|
+
**Hooks read the injected clients** — they do not hold their own HTTP. This is the **complete** hook surface (19 hooks), grouped by the domain client each one reads. **CORE** hooks read host state directly off `WidgetContext` (no data client); the rest delegate to one of the four injected clients. The grouping mirrors the banner sections in [`src/hooks.js`](src/hooks.js).
|
|
17
17
|
|
|
18
18
|
| Group | Hook (signature) | Returns | Reads / scope |
|
|
19
19
|
| ----- | ---------------- | ------- | ------------- |
|
|
@@ -24,6 +24,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
24
24
|
| **CORE** | `useWidgetEvent(name)` | `(payload?) => void` | `ctx.events.emit` — no scope |
|
|
25
25
|
| **CORE** | `useChildRenderer()` | `{ renderNode(node) }` | `ctx.renderer` — no scope (prefer the `WidgetTree` component) |
|
|
26
26
|
| **CORE** | `useFill()` | `boolean` | `ctx.fill` — no scope. `true` when the host sized this widget to fill its page-grid tile's reserved height (containers + media fill by default; the author can override per tile). Media-style widgets switch to a `flex: 1` / `height: "100%"` layout; others ignore it. Defaults `false`. |
|
|
27
|
+
| **CORE** | `useRefresh(handler)` | `void` | `ctx.refresh.subscribe` — no scope. Subscribes the handler to the page-level refresh tick (pull-to-refresh on mobile). Handler may return a Promise — the host waits for `allSettled` before clearing the spinner. The three datastore hooks auto-subscribe their own `refetch`; widgets only call this directly to re-run non-datastore work. No-op on a host that doesn't implement refresh. |
|
|
27
28
|
| **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
|
|
28
29
|
| **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` (falls back to a CustomEvent / console) — no scope |
|
|
29
30
|
| **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) first, then a **predefined shared key** (`shared.<key>`) when `key` is one of the standard strings (`submit`, `cancel`, `save`, `loading`, …), then the raw key. Use a shared key for an identical default string so it translates once and any per-instance `widget.<id>.<key>` override still wins. |
|
|
@@ -47,7 +48,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
47
48
|
|
|
48
49
|
## Status
|
|
49
50
|
|
|
50
|
-
`v0.
|
|
51
|
+
`v0.41.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**.
|
|
52
|
+
|
|
53
|
+
### What's new in 0.41.0
|
|
54
|
+
|
|
55
|
+
**New `useRefresh(handler)` hook + page-level refresh signal (sc-1179).** Pull-to-refresh on the mobile web Player + the native Expo export's `RefreshControl` now fans a page-level refresh tick out to every widget on the page. The three datastore hooks — `useDatastoreQuery`, `useDatastoreRecord`, `useAsset` — auto-subscribe their own `refetch`, so a widget built on those hooks gets refreshed for free. Widgets that need to re-run other work (a third-party `fetch`, a derived calculation) call `useRefresh(async () => { … })` directly. The handler may return a Promise — the host waits on `Promise.allSettled` of every subscriber before clearing the spinner. The slot (`ctx.refresh.subscribe`) is optional on the WidgetContext: a host that does not implement refresh (the Studio canvas preview) simply omits it and the hook collapses to a no-op. Additive — `CONTRACT.version` bumped to the next minor since the contract grew a new hook + a new (optional) context slice.
|
|
51
56
|
|
|
52
57
|
### What's new in 0.40.2
|
|
53
58
|
|
package/dist/contract.cjs
CHANGED
|
@@ -116,6 +116,26 @@ const HOOKS = [
|
|
|
116
116
|
requiredContextSlice: ["renderer.renderNode"],
|
|
117
117
|
scopes: null,
|
|
118
118
|
},
|
|
119
|
+
{
|
|
120
|
+
name: "useRefresh",
|
|
121
|
+
signature: "useRefresh(handler)",
|
|
122
|
+
description:
|
|
123
|
+
"Subscribe to the page-level refresh tick (pull-to-refresh on " +
|
|
124
|
+
"mobile, manual refresh button, etc.). The handler is called every " +
|
|
125
|
+
"time the host triggers a refresh; it may return a Promise — the " +
|
|
126
|
+
"host waits on all settled handlers before clearing the refresh " +
|
|
127
|
+
"indicator. The three datastore hooks (useDatastoreQuery, " +
|
|
128
|
+
"useDatastoreRecord, useAsset) already subscribe automatically — " +
|
|
129
|
+
"widgets only call useRefresh directly when they need to re-run " +
|
|
130
|
+
"non-datastore work (a third-party fetch, a derived calculation). " +
|
|
131
|
+
"Safe to call on a host that does not implement refresh — it is a " +
|
|
132
|
+
"no-op there.",
|
|
133
|
+
returnShape: {
|
|
134
|
+
"(returns)": "void",
|
|
135
|
+
},
|
|
136
|
+
requiredContextSlice: ["refresh.subscribe"],
|
|
137
|
+
scopes: null,
|
|
138
|
+
},
|
|
119
139
|
{
|
|
120
140
|
name: "useNavigation",
|
|
121
141
|
signature: "useNavigation()",
|
|
@@ -924,6 +944,21 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
924
944
|
required: true,
|
|
925
945
|
fields: { renderNode: "function" },
|
|
926
946
|
},
|
|
947
|
+
// sc-1179 — page-level refresh signal. Host owns the trigger (pull-to-
|
|
948
|
+
// refresh on mobile, refresh button, etc.); widgets opt in via
|
|
949
|
+
// useRefresh(handler). The three datastore hooks auto-subscribe; other
|
|
950
|
+
// widgets subscribe when they need to re-run non-datastore work. Optional
|
|
951
|
+
// so a host that does not implement refresh can simply omit it — the hook
|
|
952
|
+
// collapses to a no-op there.
|
|
953
|
+
refresh: {
|
|
954
|
+
description:
|
|
955
|
+
"Optional page-level refresh slot. { subscribe(handler) -> unsubscribe }. " +
|
|
956
|
+
"Handler is called on every page refresh tick (pull-to-refresh on " +
|
|
957
|
+
"mobile, manual refresh button); may return a Promise the host " +
|
|
958
|
+
"awaits before clearing the refresh indicator. Backs useRefresh().",
|
|
959
|
+
required: false,
|
|
960
|
+
fields: { subscribe: "function" },
|
|
961
|
+
},
|
|
927
962
|
events: {
|
|
928
963
|
description: "{ emit(name, payload) }.",
|
|
929
964
|
required: true,
|
|
@@ -1582,7 +1617,17 @@ const CONTRACT = deepFreeze({
|
|
|
1582
1617
|
// manual key entry, and a `widget.<id>.<key>` override still wins per
|
|
1583
1618
|
// instance. No existing hook, primitive, manifest field, or token changed
|
|
1584
1619
|
// shape — minor bump on the pre-1.0 channel.
|
|
1585
|
-
|
|
1620
|
+
//
|
|
1621
|
+
// 1.29.0: additive (sc-1179) — page-level refresh signal. New hook
|
|
1622
|
+
// `useRefresh(handler)` and new optional WidgetContext slice
|
|
1623
|
+
// `refresh: { subscribe(handler) -> unsubscribe }`. The web Player's
|
|
1624
|
+
// pull-to-refresh gesture and the native Expo export's `RefreshControl`
|
|
1625
|
+
// both call into the same manager so a refresh tick fans out to every
|
|
1626
|
+
// widget on the page; the three datastore hooks (useDatastoreQuery /
|
|
1627
|
+
// useDatastoreRecord / useAsset) auto-subscribe their own `refetch`.
|
|
1628
|
+
// No existing hook, primitive, manifest field, or token changed shape
|
|
1629
|
+
// — minor bump on the pre-1.0 channel.
|
|
1630
|
+
version: "1.29.0",
|
|
1586
1631
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1587
1632
|
hooks: HOOKS,
|
|
1588
1633
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -116,6 +116,26 @@ const HOOKS = [
|
|
|
116
116
|
requiredContextSlice: ["renderer.renderNode"],
|
|
117
117
|
scopes: null,
|
|
118
118
|
},
|
|
119
|
+
{
|
|
120
|
+
name: "useRefresh",
|
|
121
|
+
signature: "useRefresh(handler)",
|
|
122
|
+
description:
|
|
123
|
+
"Subscribe to the page-level refresh tick (pull-to-refresh on " +
|
|
124
|
+
"mobile, manual refresh button, etc.). The handler is called every " +
|
|
125
|
+
"time the host triggers a refresh; it may return a Promise — the " +
|
|
126
|
+
"host waits on all settled handlers before clearing the refresh " +
|
|
127
|
+
"indicator. The three datastore hooks (useDatastoreQuery, " +
|
|
128
|
+
"useDatastoreRecord, useAsset) already subscribe automatically — " +
|
|
129
|
+
"widgets only call useRefresh directly when they need to re-run " +
|
|
130
|
+
"non-datastore work (a third-party fetch, a derived calculation). " +
|
|
131
|
+
"Safe to call on a host that does not implement refresh — it is a " +
|
|
132
|
+
"no-op there.",
|
|
133
|
+
returnShape: {
|
|
134
|
+
"(returns)": "void",
|
|
135
|
+
},
|
|
136
|
+
requiredContextSlice: ["refresh.subscribe"],
|
|
137
|
+
scopes: null,
|
|
138
|
+
},
|
|
119
139
|
{
|
|
120
140
|
name: "useNavigation",
|
|
121
141
|
signature: "useNavigation()",
|
|
@@ -924,6 +944,21 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
924
944
|
required: true,
|
|
925
945
|
fields: { renderNode: "function" },
|
|
926
946
|
},
|
|
947
|
+
// sc-1179 — page-level refresh signal. Host owns the trigger (pull-to-
|
|
948
|
+
// refresh on mobile, refresh button, etc.); widgets opt in via
|
|
949
|
+
// useRefresh(handler). The three datastore hooks auto-subscribe; other
|
|
950
|
+
// widgets subscribe when they need to re-run non-datastore work. Optional
|
|
951
|
+
// so a host that does not implement refresh can simply omit it — the hook
|
|
952
|
+
// collapses to a no-op there.
|
|
953
|
+
refresh: {
|
|
954
|
+
description:
|
|
955
|
+
"Optional page-level refresh slot. { subscribe(handler) -> unsubscribe }. " +
|
|
956
|
+
"Handler is called on every page refresh tick (pull-to-refresh on " +
|
|
957
|
+
"mobile, manual refresh button); may return a Promise the host " +
|
|
958
|
+
"awaits before clearing the refresh indicator. Backs useRefresh().",
|
|
959
|
+
required: false,
|
|
960
|
+
fields: { subscribe: "function" },
|
|
961
|
+
},
|
|
927
962
|
events: {
|
|
928
963
|
description: "{ emit(name, payload) }.",
|
|
929
964
|
required: true,
|
|
@@ -1582,7 +1617,17 @@ const CONTRACT = deepFreeze({
|
|
|
1582
1617
|
// manual key entry, and a `widget.<id>.<key>` override still wins per
|
|
1583
1618
|
// instance. No existing hook, primitive, manifest field, or token changed
|
|
1584
1619
|
// shape — minor bump on the pre-1.0 channel.
|
|
1585
|
-
|
|
1620
|
+
//
|
|
1621
|
+
// 1.29.0: additive (sc-1179) — page-level refresh signal. New hook
|
|
1622
|
+
// `useRefresh(handler)` and new optional WidgetContext slice
|
|
1623
|
+
// `refresh: { subscribe(handler) -> unsubscribe }`. The web Player's
|
|
1624
|
+
// pull-to-refresh gesture and the native Expo export's `RefreshControl`
|
|
1625
|
+
// both call into the same manager so a refresh tick fans out to every
|
|
1626
|
+
// widget on the page; the three datastore hooks (useDatastoreQuery /
|
|
1627
|
+
// useDatastoreRecord / useAsset) auto-subscribe their own `refetch`.
|
|
1628
|
+
// No existing hook, primitive, manifest field, or token changed shape
|
|
1629
|
+
// — minor bump on the pre-1.0 channel.
|
|
1630
|
+
version: "1.29.0",
|
|
1586
1631
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1587
1632
|
hooks: HOOKS,
|
|
1588
1633
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -188,6 +188,49 @@ export function WidgetTree({ node }) {
|
|
|
188
188
|
return ctx.renderer.renderNode(node);
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* sc-1179 — subscribe to the page-level refresh tick.
|
|
193
|
+
*
|
|
194
|
+
* Pull-to-refresh on mobile (web Player at touch widths + the native Expo
|
|
195
|
+
* export's `RefreshControl`) triggers a page-wide refresh. Widgets that
|
|
196
|
+
* need to re-run work on a refresh — re-fetch from a third-party API,
|
|
197
|
+
* recompute a derived value — pass a handler here:
|
|
198
|
+
*
|
|
199
|
+
* useRefresh(async () => { await refetchMyThirdPartyData(); });
|
|
200
|
+
*
|
|
201
|
+
* The handler may return a Promise — the host waits on Promise.allSettled
|
|
202
|
+
* of every subscriber before clearing the refresh indicator. Handler
|
|
203
|
+
* identity does not need to be stable: the subscription captures it in a
|
|
204
|
+
* ref and reads the latest on every tick.
|
|
205
|
+
*
|
|
206
|
+
* `useDatastoreQuery` / `useDatastoreRecord` / `useAsset` already
|
|
207
|
+
* auto-subscribe their own `refetch`, so a widget built on the data hooks
|
|
208
|
+
* gets refreshed for free — call `useRefresh` directly only for
|
|
209
|
+
* non-datastore work.
|
|
210
|
+
*
|
|
211
|
+
* Safe to call on a host that does not implement refresh — the hook
|
|
212
|
+
* collapses to a no-op there.
|
|
213
|
+
*/
|
|
214
|
+
export function useRefresh(handler) {
|
|
215
|
+
const ctx = useWidgetContextOrThrow("useRefresh");
|
|
216
|
+
const handlerRef = useRef(handler);
|
|
217
|
+
handlerRef.current = handler;
|
|
218
|
+
const subscribe = ctx.refresh && ctx.refresh.subscribe;
|
|
219
|
+
const subscribeRef = useRef(subscribe);
|
|
220
|
+
subscribeRef.current = subscribe;
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
const sub = subscribeRef.current;
|
|
223
|
+
if (typeof sub !== "function") return undefined;
|
|
224
|
+
const unsubscribe = sub(() => {
|
|
225
|
+
const fn = handlerRef.current;
|
|
226
|
+
if (typeof fn === "function") return fn();
|
|
227
|
+
return undefined;
|
|
228
|
+
});
|
|
229
|
+
return typeof unsubscribe === "function" ? unsubscribe : undefined;
|
|
230
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
231
|
+
}, []);
|
|
232
|
+
}
|
|
233
|
+
|
|
191
234
|
/**
|
|
192
235
|
* Returns the host-provided navigation surface:
|
|
193
236
|
* `{ goTo, goBack, push, replace, back, currentRoute }`.
|
|
@@ -478,6 +521,10 @@ export function useDatastoreQuery(table, query) {
|
|
|
478
521
|
await doFetch();
|
|
479
522
|
}, [doFetch]);
|
|
480
523
|
|
|
524
|
+
// sc-1179 — auto-subscribe to the page-level refresh tick so a
|
|
525
|
+
// pull-to-refresh on mobile re-runs the query without per-widget code.
|
|
526
|
+
useRefresh(refetch);
|
|
527
|
+
|
|
481
528
|
return { data, loading, error, refetch };
|
|
482
529
|
}
|
|
483
530
|
|
|
@@ -556,6 +603,9 @@ export function useDatastoreRecord(table, recordId) {
|
|
|
556
603
|
await doFetch();
|
|
557
604
|
}, [doFetch]);
|
|
558
605
|
|
|
606
|
+
// sc-1179 — auto-subscribe to the page-level refresh tick.
|
|
607
|
+
useRefresh(refetch);
|
|
608
|
+
|
|
559
609
|
return { data, loading, error, refetch };
|
|
560
610
|
}
|
|
561
611
|
|
|
@@ -1137,6 +1187,9 @@ export function useAsset(assetId) {
|
|
|
1137
1187
|
await doFetch();
|
|
1138
1188
|
}, [doFetch]);
|
|
1139
1189
|
|
|
1190
|
+
// sc-1179 — auto-subscribe to the page-level refresh tick.
|
|
1191
|
+
useRefresh(refetch);
|
|
1192
|
+
|
|
1140
1193
|
const url =
|
|
1141
1194
|
asset && typeof asset.url === "string" && asset.url.length > 0
|
|
1142
1195
|
? asset.url
|
package/dist/index.d.ts
CHANGED
|
@@ -812,6 +812,21 @@ export const Linking: {
|
|
|
812
812
|
canOpenURL(url: string): Promise<boolean>;
|
|
813
813
|
};
|
|
814
814
|
|
|
815
|
+
/**
|
|
816
|
+
* sc-1179 — subscribe to the page-level refresh tick (pull-to-refresh on
|
|
817
|
+
* mobile, manual refresh button, etc.). The handler is called every time
|
|
818
|
+
* the host triggers a refresh; it may return a Promise — the host waits
|
|
819
|
+
* on all settled subscribers before clearing the refresh indicator.
|
|
820
|
+
*
|
|
821
|
+
* `useDatastoreQuery` / `useDatastoreRecord` / `useAsset` already
|
|
822
|
+
* auto-subscribe their own `refetch`, so widgets only call this directly
|
|
823
|
+
* to re-run non-datastore work. Safe to call on a host that does not
|
|
824
|
+
* implement refresh — collapses to a no-op there.
|
|
825
|
+
*/
|
|
826
|
+
export function useRefresh(
|
|
827
|
+
handler: () => void | Promise<unknown>,
|
|
828
|
+
): void;
|
|
829
|
+
|
|
815
830
|
/**
|
|
816
831
|
* Error class thrown by useDatastoreMutation callbacks (and surfaced by
|
|
817
832
|
* useDatastoreQuery in its `error` slot). The `code` is a stable
|
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.41.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",
|