@colixsystems/widget-sdk 0.40.1 → 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 +11 -2
- package/dist/contract.cjs +46 -1
- package/dist/contract.js +46 -1
- package/dist/hooks.js +63 -1
- 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,15 @@ 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.
|
|
56
|
+
|
|
57
|
+
### What's new in 0.40.2
|
|
58
|
+
|
|
59
|
+
**`useI18n().t()` no longer leaks the host's `{{t:key}}` miss placeholder.** Resolution steps 1–2 (per-widget and shared namespaces) already treated a `{{t:…}}` return from the host as a miss, but the final raw-key step did not — on the web Player (whose host resolver returns the placeholder form on a miss and ignores the fallback argument) a key absent from the tenant dictionary rendered as literal `{{t:key}}` text instead of degrading to `fallback ?? key`. The same guard now applies to all three steps. **The public contract is unchanged** — this is the behaviour `useI18n` always documented. `CONTRACT.version` is unchanged.
|
|
51
60
|
|
|
52
61
|
### What's new in 0.40.1
|
|
53
62
|
|
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 }`.
|
|
@@ -270,8 +313,17 @@ export function useI18n() {
|
|
|
270
313
|
}
|
|
271
314
|
// 3) Fall back to the raw key (shared app keys + widgets with no
|
|
272
315
|
// manifest translations — unchanged from pre-1.10 behaviour).
|
|
316
|
+
// The same `{{t:…}}` miss guard as steps 1–2 applies: the Player's
|
|
317
|
+
// host t ignores the fallback arg and returns the placeholder on a
|
|
318
|
+
// miss, and without the guard that placeholder rendered verbatim
|
|
319
|
+
// instead of degrading to `fallback ?? key`.
|
|
273
320
|
const out = hostT(key, fallback);
|
|
274
|
-
if (
|
|
321
|
+
if (
|
|
322
|
+
typeof out === "string" &&
|
|
323
|
+
out.length > 0 &&
|
|
324
|
+
out !== key &&
|
|
325
|
+
!out.startsWith("{{t:")
|
|
326
|
+
) {
|
|
275
327
|
return out;
|
|
276
328
|
}
|
|
277
329
|
// Host returned the key (unresolved) or nothing usable.
|
|
@@ -469,6 +521,10 @@ export function useDatastoreQuery(table, query) {
|
|
|
469
521
|
await doFetch();
|
|
470
522
|
}, [doFetch]);
|
|
471
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
|
+
|
|
472
528
|
return { data, loading, error, refetch };
|
|
473
529
|
}
|
|
474
530
|
|
|
@@ -547,6 +603,9 @@ export function useDatastoreRecord(table, recordId) {
|
|
|
547
603
|
await doFetch();
|
|
548
604
|
}, [doFetch]);
|
|
549
605
|
|
|
606
|
+
// sc-1179 — auto-subscribe to the page-level refresh tick.
|
|
607
|
+
useRefresh(refetch);
|
|
608
|
+
|
|
550
609
|
return { data, loading, error, refetch };
|
|
551
610
|
}
|
|
552
611
|
|
|
@@ -1128,6 +1187,9 @@ export function useAsset(assetId) {
|
|
|
1128
1187
|
await doFetch();
|
|
1129
1188
|
}, [doFetch]);
|
|
1130
1189
|
|
|
1190
|
+
// sc-1179 — auto-subscribe to the page-level refresh tick.
|
|
1191
|
+
useRefresh(refetch);
|
|
1192
|
+
|
|
1131
1193
|
const url =
|
|
1132
1194
|
asset && typeof asset.url === "string" && asset.url.length > 0
|
|
1133
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",
|