@colixsystems/widget-sdk 0.229.0 → 0.231.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 +52 -2
- package/dist/contract.cjs +67 -5
- package/dist/contract.js +67 -5
- package/dist/hooks.js +74 -5
- package/dist/index.d.ts +46 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/package.json +1 -1
- package/src/contract.js +67 -5
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
45
45
|
| **CORE** | `useGeolocation(options)` (optional: options) | `{ latitude, longitude, accuracy, loading, error, getCurrentPosition, backgroundSupported, backgroundWatching, startBackgroundWatch, stopBackgroundWatch }` | `ctx.device.geolocation` — no scope. Capture is IMPERATIVE: call `getCurrentPosition()` from a user gesture (a tap), never on mount. Resolves to `{ latitude, longitude, accuracy }`; rejects with `GeolocationError` (`.code` in `PERMISSION_DENIED \| UNAVAILABLE \| TIMEOUT \| UNSUPPORTED \| INTERNAL`). Identical on web (`navigator.geolocation`) and the Expo export (`expo-location`). **Background watch (sc-6450)** — `startBackgroundWatch({ enableHighAccuracy, distanceIntervalMeters, timeIntervalMs })` keeps positions arriving while the app is backgrounded; `stopBackgroundWatch()` releases it. NATIVE-ONLY and opt-in per app: gate the control on `backgroundSupported` (false on web, and in an export whose workspace did not opt in). The watch outlives the widget's mount, and its positions land in the same `latitude`/`longitude`/`accuracy` slots. |
|
|
46
46
|
| **CORE** | `useSpeechToText(options)` (optional: options) | `{ transcript, partial, listening, supported, error, start, stop, abort, reset }` | `ctx.device.speech` — no scope. Dictation with the device's **on-device** recogniser: no audio is uploaded and no AI credit is spent. Capture is IMPERATIVE: call `start()` from a user gesture (a tap), never on mount. `transcript` accumulates finalised speech, `partial` holds the uncommitted guess (needs `options.interimResults`); `stop()` keeps it, `abort()` discards it. Rejects with `SpeechToTextError` (`.code` in `PERMISSION_DENIED \| NO_SPEECH \| LANGUAGE_UNSUPPORTED \| NETWORK \| ABORTED \| UNSUPPORTED \| INTERNAL`). **Gate your mic button on `supported`** — Firefox ships no `SpeechRecognition`. Identical on web (`SpeechRecognition`) and the Expo export (`expo-speech-recognition`). |
|
|
47
47
|
| **CORE** | `useCamera(options)` (optional: options) | `{ asset, loading, error, supported, capture, pick, reset }` | `ctx.device.camera` — no scope. Take a photo (`capture()`) or choose one (`pick()`). Capture is IMPERATIVE: call from a user gesture (a tap), never on mount. Both resolve a normalised `{ uri, name, mimeType, width, height, size, file }`, or **`null` when the user dismisses the picker** — dismissal is not an error, so no `try/catch` is needed on the happy path. Rejects with `CameraError` (`.code` in `PERMISSION_DENIED \| UNSUPPORTED \| INTERNAL`). `asset.file` is already the right upload part for the host, so `fd.append("file", asset.file)` → `ctx.assets.upload(fd)` is ONE code path on both. **Gate your camera button on `supported`.** Identical on web (a live `getUserMedia` preview, phones included; a file input only where getUserMedia is absent) and the Expo export (`expo-image-picker`). |
|
|
48
|
-
| **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. |
|
|
48
|
+
| **CORE** | `useI18n()` | `{ t, locale, languages, canSetLocale, setLocale }` | `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. `languages` lists the app's active languages (`[{ code, name, is_base }]`); `setLocale(code)` switches the whole app live — saved to a signed-in user's account, kept on the device for a visitor, `null` for the app default. Rejects with `.code` `INVALID_LOCALE` \| `UNSUPPORTED`; `canSetLocale` is false where the host cannot switch (the Builder canvas). |
|
|
49
49
|
| **CORE** | `useTranslate()` | `{ translate, translating, error, language, available }` | `ctx.i18n.translate` — no scope. Machine-translates **user-generated content** (record text, file names, API payloads) into the app user's language; `useI18n().t()` is still the answer for your own copy. `translate(str)` → `Promise<string>`, `translate(str[])` → `Promise<string[]>` in ONE request. Target defaults to the app user's language. Cached per session, per pod, and durably per workspace, so repeat text is free. Limits: 50 segments / 5 000 chars each / 20 000 total. Rejects with `TranslateError`; `available` is false where the host cannot translate. |
|
|
50
50
|
| **CORE** | `useLinkPreview(url)` | `{ preview, loading, error, available }` — `preview` is `{ url, title, description, site_name, image_url } \| null` | `ctx.linkPreviews.get` — no scope. A link's title, description, site name and image, fetched by the platform (the device never contacts the linked site). A page with nothing to show resolves with null fields, not an error — render the plain link. A non-http(s) `url` makes no request. Memoized for up to an hour across widgets; concurrent calls for one URL share a request. `error` is a `LinkPreviewError` (`VALIDATION` \| `AUTH_REQUIRED` \| `RATE_LIMITED` \| `NETWORK` \| `INTERNAL`). `available` is false on a host with no link-preview client |
|
|
51
51
|
| **CORE** | `useStableQuery(buildQuery)` | `T \| undefined` (whatever `buildQuery()` returns) | No context slice, no scope. Keeps `buildQuery()`'s result at a STABLE reference across renders when its (JSON-serialised) content hasn't changed, so `useDatastoreQuery(tableId, useStableQuery(() => ({...})))` replaces a hand-rolled `useMemo` with an easy-to-get-wrong deps array. Never throws: a `buildQuery` that itself throws degrades to a stable `undefined`; a result that can't be diffed (e.g. circular) degrades to "always a new reference". |
|
|
@@ -84,7 +84,57 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
84
84
|
|
|
85
85
|
## Status
|
|
86
86
|
|
|
87
|
-
`v0.
|
|
87
|
+
`v0.231.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**.
|
|
88
|
+
|
|
89
|
+
### What's new in 0.231.0 (contract 1.186.0)
|
|
90
|
+
|
|
91
|
+
**A whole-screen widget can run to the bottom edge of a phone — `useFitScreenInsets()`.** In the exported app, a Fit to screen section whose widget scrolls itself (`prefersFitToScreen: true`) and calls the new `useFitScreenInsets()` runs all the way to the screen's bottom edge, under the iPhone home indicator or the Android navigation bar, so your widget's background fills the screen with no strip of page colour below it. Keep your bottom-anchored controls clear of that area with the new `useFitScreenInsets()`: it returns `{ top, right, bottom, left }` in px, the parts of the device's safe area your surface runs under. Pad your composer, a pinned bar or the end of a list by `bottom`, with your background behind the padding.
|
|
92
|
+
|
|
93
|
+
```jsx
|
|
94
|
+
import * as sdk from "@colixsystems/widget-sdk";
|
|
95
|
+
import { View, TextInput } from "@colixsystems/widget-sdk";
|
|
96
|
+
|
|
97
|
+
// A host older than 0.231.0 has no useFitScreenInsets.
|
|
98
|
+
const NO_INSETS = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
99
|
+
const useFitScreenInsets =
|
|
100
|
+
typeof sdk.useFitScreenInsets === "function" ? sdk.useFitScreenInsets : () => NO_INSETS;
|
|
101
|
+
|
|
102
|
+
function ComposerBar({ theme }) {
|
|
103
|
+
const { bottom } = useFitScreenInsets();
|
|
104
|
+
return (
|
|
105
|
+
<View style={{ backgroundColor: theme.colors.surface, padding: 10, paddingBottom: 10 + bottom }}>
|
|
106
|
+
<TextInput placeholder="Message" />
|
|
107
|
+
</View>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The host decides the value, so you never subtract anything yourself: it is `0` while the keyboard is up (the keyboard covers that area, and your composer sits right on it), when the app's bottom bar already holds the inset, on web (the browser keeps the page inside its own safe area), and for every widget that is not a self-scrolling fit surface ending the page. Calling the hook is the opt-in: a widget that never calls it, or does not scroll itself, keeps the inset below its section, as before, so an existing widget's controls never move under the indicator. Nor does a widget with something drawn below it inside the section, such as a bottom tab bar. Use this hook rather than `useSafeAreaInsets()` from `react-native-safe-area-context`: the device's raw inset would also pad a composer that sits on a bottom bar or on the keyboard.
|
|
113
|
+
|
|
114
|
+
Additive: a host without the `fitScreenFoot` hint reads zeros, so a widget that pads by it looks exactly as before there.
|
|
115
|
+
|
|
116
|
+
### What's new in 0.230.0 (contract 1.185.0)
|
|
117
|
+
|
|
118
|
+
**A widget can switch the app's language — `useI18n().setLocale(code)`.** `useI18n()` now also returns `languages`, the app's active languages as `[{ code, name, is_base }]`, and `setLocale(code)`, which switches the whole app to that language at once, with no reload. A signed-in user's choice is saved to their account, so it follows them to other devices; a visitor who is not signed in keeps it on this device. `setLocale(null)` returns to the app's default language. It rejects with an `Error` whose `.code` is `INVALID_LOCALE` (not one of `languages`) or `UNSUPPORTED` (the host cannot switch — the Builder canvas). Gate your controls on `canSetLocale` and render them disabled when it is false. The same on the web Player and the exported app.
|
|
119
|
+
|
|
120
|
+
```jsx
|
|
121
|
+
import { useI18n, Pressable, Text } from "@colixsystems/widget-sdk";
|
|
122
|
+
|
|
123
|
+
export default function Languages() {
|
|
124
|
+
const { languages, locale, setLocale, canSetLocale } = useI18n();
|
|
125
|
+
return languages.map((l) => (
|
|
126
|
+
<Pressable
|
|
127
|
+
key={l.code}
|
|
128
|
+
disabled={!canSetLocale}
|
|
129
|
+
onPress={() => setLocale(l.code).catch(() => { /* show "try again" */ })}
|
|
130
|
+
>
|
|
131
|
+
<Text>{l.code === locale ? `✓ ${l.name}` : l.name}</Text>
|
|
132
|
+
</Pressable>
|
|
133
|
+
));
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
AppStudio also ships a ready-made **Language picker** built-in widget on the same hook.
|
|
88
138
|
|
|
89
139
|
### What's new in 0.229.0 (contract unchanged)
|
|
90
140
|
|
package/dist/contract.cjs
CHANGED
|
@@ -824,9 +824,21 @@ const HOOKS = [
|
|
|
824
824
|
{
|
|
825
825
|
name: "useI18n",
|
|
826
826
|
signature: "useI18n()",
|
|
827
|
+
description:
|
|
828
|
+
"The app's translations and language. t(key, fallback?) resolves an author-written key; locale is the language the app " +
|
|
829
|
+
"renders in. The app user can switch it from a widget: languages lists the app's active languages as " +
|
|
830
|
+
"[{ code, name, is_base }] (empty when the host offers none), and setLocale(code) switches the WHOLE app live, with no " +
|
|
831
|
+
"reload. A signed-in user's choice is saved to their account; a visitor who is not signed in keeps it on this device. " +
|
|
832
|
+
"setLocale(null) returns to the app's default language. setLocale resolves once the choice is saved and rejects with " +
|
|
833
|
+
"an Error whose .code is INVALID_LOCALE (the code is not one of languages) or UNSUPPORTED (this host cannot switch, " +
|
|
834
|
+
"e.g. the Builder canvas) — always catch it. canSetLocale is false on such a host: render the choices disabled then, never a dead button. " +
|
|
835
|
+
"Identical on web (Player) and the Expo export.",
|
|
827
836
|
returnShape: {
|
|
828
837
|
t: "(key: string, fallback?: string) => string",
|
|
829
838
|
locale: "string",
|
|
839
|
+
languages: "Array<{ code: string, name: string, is_base: boolean }>",
|
|
840
|
+
canSetLocale: "boolean",
|
|
841
|
+
setLocale: "(code: string | null) => Promise<void> // rejects with Error .code INVALID_LOCALE | UNSUPPORTED",
|
|
830
842
|
},
|
|
831
843
|
requiredContextSlice: ["i18n.t", "i18n.locale"],
|
|
832
844
|
scopes: null,
|
|
@@ -1009,6 +1021,31 @@ const HOOKS = [
|
|
|
1009
1021
|
requiredContextSlice: ["fitScreen"],
|
|
1010
1022
|
scopes: null,
|
|
1011
1023
|
},
|
|
1024
|
+
{
|
|
1025
|
+
name: "useFitScreenInsets",
|
|
1026
|
+
signature: "useFitScreenInsets()",
|
|
1027
|
+
description:
|
|
1028
|
+
"Returns { top, right, bottom, left } in px: the parts of the device's " +
|
|
1029
|
+
"safe area (the home indicator, a system navigation bar) that this " +
|
|
1030
|
+
"widget's surface runs under. Calling it opts the widget in: only then " +
|
|
1031
|
+
"does a Fit to screen section whose widget scrolls itself (manifest " +
|
|
1032
|
+
"prefersFitToScreen) run to the screen's bottom edge in the exported " +
|
|
1033
|
+
"app, so the widget's background fills it; a widget that never calls it " +
|
|
1034
|
+
"keeps the inset below its section. Pad the bottom-anchored controls (a composer, a pinned bar, the end of " +
|
|
1035
|
+
"a list) by bottom so they stay clear, with the background behind the " +
|
|
1036
|
+
"padding. The host reports 0 while the keyboard is up (it covers that " +
|
|
1037
|
+
"area), when the app's bottom bar holds the inset, on web (the browser " +
|
|
1038
|
+
"keeps the page inside its own safe area) and for every widget that is " +
|
|
1039
|
+
"not such a surface, so calling it is always safe.",
|
|
1040
|
+
returnShape: {
|
|
1041
|
+
top: "number",
|
|
1042
|
+
right: "number",
|
|
1043
|
+
bottom: "number",
|
|
1044
|
+
left: "number",
|
|
1045
|
+
},
|
|
1046
|
+
requiredContextSlice: ["fitScreenFoot"],
|
|
1047
|
+
scopes: null,
|
|
1048
|
+
},
|
|
1012
1049
|
{
|
|
1013
1050
|
name: "useChildRenderer",
|
|
1014
1051
|
signature: "useChildRenderer()",
|
|
@@ -3594,7 +3631,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3594
3631
|
type: "boolean",
|
|
3595
3632
|
required: false,
|
|
3596
3633
|
description:
|
|
3597
|
-
"Optional (default false). TRUE when the widget is built to own the rest of the screen: it scrolls its own content and keeps its controls in view, like a messenger, a map or a full-height board. Dropping the widget at the top level of a page in the Builder turns that section's \"Fit to screen\" setting on (the maker can switch it off, and it stays off), and the app-builder fits the section it plans the widget into. A fit section runs edge to edge across the content area and takes the rest of the screen; the widget learns it through useFill() and useFitScreen(), and the host gives it a box exactly the height left, which it scrolls inside (a widget without the field grows with its content there, and the page scrolls). Set it only for a widget that honours useFill() and scrolls inside the height it is given; leave it false (or omit) for everything else.",
|
|
3634
|
+
"Optional (default false). TRUE when the widget is built to own the rest of the screen: it scrolls its own content and keeps its controls in view, like a messenger, a map or a full-height board. Dropping the widget at the top level of a page in the Builder turns that section's \"Fit to screen\" setting on (the maker can switch it off, and it stays off), and the app-builder fits the section it plans the widget into. A fit section runs edge to edge across the content area and takes the rest of the screen; the widget learns it through useFill() and useFitScreen(), and the host gives it a box exactly the height left, which it scrolls inside (a widget without the field grows with its content there, and the page scrolls). In the exported app a widget that also calls useFitScreenInsets() has that box run to the screen's bottom edge, under the home indicator: keep the background filling it and pad bottom-anchored controls by useFitScreenInsets().bottom (a widget that never calls it keeps the inset below its section). Set it only for a widget that honours useFill() and scrolls inside the height it is given; leave it false (or omit) for everything else.",
|
|
3598
3635
|
default: false,
|
|
3599
3636
|
},
|
|
3600
3637
|
events: {
|
|
@@ -3682,6 +3719,21 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3682
3719
|
required: false,
|
|
3683
3720
|
fields: {},
|
|
3684
3721
|
},
|
|
3722
|
+
// sc-11615 — optional host hint backing useFitScreenInsets(): the screen's
|
|
3723
|
+
// foot a self-scrolling fit surface may claim.
|
|
3724
|
+
fitScreenFoot: {
|
|
3725
|
+
description:
|
|
3726
|
+
"Optional host layout hint ({ claim } or null). Set only for a " +
|
|
3727
|
+
"self-scrolling Fit to screen surface with nothing below it in the " +
|
|
3728
|
+
"exported app: claim(onInset) runs the surface under the bottom safe " +
|
|
3729
|
+
"area, calls onInset(px) now and on every change, and returns the " +
|
|
3730
|
+
"release. Backs useFitScreenInsets(); null or absent reads as zeros " +
|
|
3731
|
+
"and leaves the inset below the section.",
|
|
3732
|
+
required: false,
|
|
3733
|
+
fields: {
|
|
3734
|
+
claim: "(onInset: (bottom: number) => void) => () => void",
|
|
3735
|
+
},
|
|
3736
|
+
},
|
|
3685
3737
|
user: {
|
|
3686
3738
|
description:
|
|
3687
3739
|
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. email is null for an account created without one. Not a data-client.",
|
|
@@ -3924,12 +3976,14 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3924
3976
|
// available:false (same convention as ctx.toast / ctx.device) rather than
|
|
3925
3977
|
// throwing at render. Both the web Player and the Expo export inject it.
|
|
3926
3978
|
description:
|
|
3927
|
-
"{ t(key, fallback?), locale, translate? } — `translate` is the injected " +
|
|
3979
|
+
"{ t(key, fallback?), locale, translate?, languages?, setLocale? } — `translate` is the injected " +
|
|
3928
3980
|
"@colixsystems/translation-client ({ translate(body), status() }) that backs " +
|
|
3929
|
-
"useTranslate() for user-generated content."
|
|
3981
|
+
"useTranslate() for user-generated content. `languages` ([{ code, name, is_base }]) and " +
|
|
3982
|
+
"`setLocale(code)` back useI18n()'s language switch (sc-11482); a host that cannot " +
|
|
3983
|
+
"switch omits setLocale.",
|
|
3930
3984
|
required: true,
|
|
3931
3985
|
fields: { t: "function", locale: "string" },
|
|
3932
|
-
optionalFields: { translate: "object" },
|
|
3986
|
+
optionalFields: { translate: "object", languages: "array", setLocale: "function" },
|
|
3933
3987
|
},
|
|
3934
3988
|
logger: {
|
|
3935
3989
|
description:
|
|
@@ -5978,7 +6032,15 @@ const CONTRACT = deepFreeze({
|
|
|
5978
6032
|
// that section's Fit to screen on), and `useFitScreen()` with its
|
|
5979
6033
|
// optional `fitScreen` host hint: the widget is a fit section's surface,
|
|
5980
6034
|
// so it drops its own frame. Existing manifests and hosts read false.
|
|
5981
|
-
|
|
6035
|
+
// 1.185.0: additive (sc-11482) — `useI18n()` also returns `languages`,
|
|
6036
|
+
// `canSetLocale` and `setLocale(code)`, backed by the optional
|
|
6037
|
+
// `i18n.languages` / `i18n.setLocale` host slice: a widget can switch
|
|
6038
|
+
// the app's language. Hosts without them read [] / false.
|
|
6039
|
+
// 1.186.0: additive (sc-11615) — `useFitScreenInsets()` with its optional
|
|
6040
|
+
// `fitScreenFoot` host hint: a self-scrolling Fit to screen surface that
|
|
6041
|
+
// calls it runs under the bottom safe area on native and pads its
|
|
6042
|
+
// controls by it. Hosts without the hint read zeros.
|
|
6043
|
+
version: "1.186.0",
|
|
5982
6044
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5983
6045
|
hooks: HOOKS,
|
|
5984
6046
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -824,9 +824,21 @@ const HOOKS = [
|
|
|
824
824
|
{
|
|
825
825
|
name: "useI18n",
|
|
826
826
|
signature: "useI18n()",
|
|
827
|
+
description:
|
|
828
|
+
"The app's translations and language. t(key, fallback?) resolves an author-written key; locale is the language the app " +
|
|
829
|
+
"renders in. The app user can switch it from a widget: languages lists the app's active languages as " +
|
|
830
|
+
"[{ code, name, is_base }] (empty when the host offers none), and setLocale(code) switches the WHOLE app live, with no " +
|
|
831
|
+
"reload. A signed-in user's choice is saved to their account; a visitor who is not signed in keeps it on this device. " +
|
|
832
|
+
"setLocale(null) returns to the app's default language. setLocale resolves once the choice is saved and rejects with " +
|
|
833
|
+
"an Error whose .code is INVALID_LOCALE (the code is not one of languages) or UNSUPPORTED (this host cannot switch, " +
|
|
834
|
+
"e.g. the Builder canvas) — always catch it. canSetLocale is false on such a host: render the choices disabled then, never a dead button. " +
|
|
835
|
+
"Identical on web (Player) and the Expo export.",
|
|
827
836
|
returnShape: {
|
|
828
837
|
t: "(key: string, fallback?: string) => string",
|
|
829
838
|
locale: "string",
|
|
839
|
+
languages: "Array<{ code: string, name: string, is_base: boolean }>",
|
|
840
|
+
canSetLocale: "boolean",
|
|
841
|
+
setLocale: "(code: string | null) => Promise<void> // rejects with Error .code INVALID_LOCALE | UNSUPPORTED",
|
|
830
842
|
},
|
|
831
843
|
requiredContextSlice: ["i18n.t", "i18n.locale"],
|
|
832
844
|
scopes: null,
|
|
@@ -1009,6 +1021,31 @@ const HOOKS = [
|
|
|
1009
1021
|
requiredContextSlice: ["fitScreen"],
|
|
1010
1022
|
scopes: null,
|
|
1011
1023
|
},
|
|
1024
|
+
{
|
|
1025
|
+
name: "useFitScreenInsets",
|
|
1026
|
+
signature: "useFitScreenInsets()",
|
|
1027
|
+
description:
|
|
1028
|
+
"Returns { top, right, bottom, left } in px: the parts of the device's " +
|
|
1029
|
+
"safe area (the home indicator, a system navigation bar) that this " +
|
|
1030
|
+
"widget's surface runs under. Calling it opts the widget in: only then " +
|
|
1031
|
+
"does a Fit to screen section whose widget scrolls itself (manifest " +
|
|
1032
|
+
"prefersFitToScreen) run to the screen's bottom edge in the exported " +
|
|
1033
|
+
"app, so the widget's background fills it; a widget that never calls it " +
|
|
1034
|
+
"keeps the inset below its section. Pad the bottom-anchored controls (a composer, a pinned bar, the end of " +
|
|
1035
|
+
"a list) by bottom so they stay clear, with the background behind the " +
|
|
1036
|
+
"padding. The host reports 0 while the keyboard is up (it covers that " +
|
|
1037
|
+
"area), when the app's bottom bar holds the inset, on web (the browser " +
|
|
1038
|
+
"keeps the page inside its own safe area) and for every widget that is " +
|
|
1039
|
+
"not such a surface, so calling it is always safe.",
|
|
1040
|
+
returnShape: {
|
|
1041
|
+
top: "number",
|
|
1042
|
+
right: "number",
|
|
1043
|
+
bottom: "number",
|
|
1044
|
+
left: "number",
|
|
1045
|
+
},
|
|
1046
|
+
requiredContextSlice: ["fitScreenFoot"],
|
|
1047
|
+
scopes: null,
|
|
1048
|
+
},
|
|
1012
1049
|
{
|
|
1013
1050
|
name: "useChildRenderer",
|
|
1014
1051
|
signature: "useChildRenderer()",
|
|
@@ -3590,7 +3627,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3590
3627
|
type: "boolean",
|
|
3591
3628
|
required: false,
|
|
3592
3629
|
description:
|
|
3593
|
-
"Optional (default false). TRUE when the widget is built to own the rest of the screen: it scrolls its own content and keeps its controls in view, like a messenger, a map or a full-height board. Dropping the widget at the top level of a page in the Builder turns that section's \"Fit to screen\" setting on (the maker can switch it off, and it stays off), and the app-builder fits the section it plans the widget into. A fit section runs edge to edge across the content area and takes the rest of the screen; the widget learns it through useFill() and useFitScreen(), and the host gives it a box exactly the height left, which it scrolls inside (a widget without the field grows with its content there, and the page scrolls). Set it only for a widget that honours useFill() and scrolls inside the height it is given; leave it false (or omit) for everything else.",
|
|
3630
|
+
"Optional (default false). TRUE when the widget is built to own the rest of the screen: it scrolls its own content and keeps its controls in view, like a messenger, a map or a full-height board. Dropping the widget at the top level of a page in the Builder turns that section's \"Fit to screen\" setting on (the maker can switch it off, and it stays off), and the app-builder fits the section it plans the widget into. A fit section runs edge to edge across the content area and takes the rest of the screen; the widget learns it through useFill() and useFitScreen(), and the host gives it a box exactly the height left, which it scrolls inside (a widget without the field grows with its content there, and the page scrolls). In the exported app a widget that also calls useFitScreenInsets() has that box run to the screen's bottom edge, under the home indicator: keep the background filling it and pad bottom-anchored controls by useFitScreenInsets().bottom (a widget that never calls it keeps the inset below its section). Set it only for a widget that honours useFill() and scrolls inside the height it is given; leave it false (or omit) for everything else.",
|
|
3594
3631
|
default: false,
|
|
3595
3632
|
},
|
|
3596
3633
|
events: {
|
|
@@ -3678,6 +3715,21 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3678
3715
|
required: false,
|
|
3679
3716
|
fields: {},
|
|
3680
3717
|
},
|
|
3718
|
+
// sc-11615 — optional host hint backing useFitScreenInsets(): the screen's
|
|
3719
|
+
// foot a self-scrolling fit surface may claim.
|
|
3720
|
+
fitScreenFoot: {
|
|
3721
|
+
description:
|
|
3722
|
+
"Optional host layout hint ({ claim } or null). Set only for a " +
|
|
3723
|
+
"self-scrolling Fit to screen surface with nothing below it in the " +
|
|
3724
|
+
"exported app: claim(onInset) runs the surface under the bottom safe " +
|
|
3725
|
+
"area, calls onInset(px) now and on every change, and returns the " +
|
|
3726
|
+
"release. Backs useFitScreenInsets(); null or absent reads as zeros " +
|
|
3727
|
+
"and leaves the inset below the section.",
|
|
3728
|
+
required: false,
|
|
3729
|
+
fields: {
|
|
3730
|
+
claim: "(onInset: (bottom: number) => void) => () => void",
|
|
3731
|
+
},
|
|
3732
|
+
},
|
|
3681
3733
|
user: {
|
|
3682
3734
|
description:
|
|
3683
3735
|
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. email is null for an account created without one. Not a data-client.",
|
|
@@ -3920,12 +3972,14 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3920
3972
|
// available:false (same convention as ctx.toast / ctx.device) rather than
|
|
3921
3973
|
// throwing at render. Both the web Player and the Expo export inject it.
|
|
3922
3974
|
description:
|
|
3923
|
-
"{ t(key, fallback?), locale, translate? } — `translate` is the injected " +
|
|
3975
|
+
"{ t(key, fallback?), locale, translate?, languages?, setLocale? } — `translate` is the injected " +
|
|
3924
3976
|
"@colixsystems/translation-client ({ translate(body), status() }) that backs " +
|
|
3925
|
-
"useTranslate() for user-generated content."
|
|
3977
|
+
"useTranslate() for user-generated content. `languages` ([{ code, name, is_base }]) and " +
|
|
3978
|
+
"`setLocale(code)` back useI18n()'s language switch (sc-11482); a host that cannot " +
|
|
3979
|
+
"switch omits setLocale.",
|
|
3926
3980
|
required: true,
|
|
3927
3981
|
fields: { t: "function", locale: "string" },
|
|
3928
|
-
optionalFields: { translate: "object" },
|
|
3982
|
+
optionalFields: { translate: "object", languages: "array", setLocale: "function" },
|
|
3929
3983
|
},
|
|
3930
3984
|
logger: {
|
|
3931
3985
|
description:
|
|
@@ -5974,7 +6028,15 @@ const CONTRACT = deepFreeze({
|
|
|
5974
6028
|
// that section's Fit to screen on), and `useFitScreen()` with its
|
|
5975
6029
|
// optional `fitScreen` host hint: the widget is a fit section's surface,
|
|
5976
6030
|
// so it drops its own frame. Existing manifests and hosts read false.
|
|
5977
|
-
|
|
6031
|
+
// 1.185.0: additive (sc-11482) — `useI18n()` also returns `languages`,
|
|
6032
|
+
// `canSetLocale` and `setLocale(code)`, backed by the optional
|
|
6033
|
+
// `i18n.languages` / `i18n.setLocale` host slice: a widget can switch
|
|
6034
|
+
// the app's language. Hosts without them read [] / false.
|
|
6035
|
+
// 1.186.0: additive (sc-11615) — `useFitScreenInsets()` with its optional
|
|
6036
|
+
// `fitScreenFoot` host hint: a self-scrolling Fit to screen surface that
|
|
6037
|
+
// calls it runs under the bottom safe area on native and pads its
|
|
6038
|
+
// controls by it. Hosts without the hint read zeros.
|
|
6039
|
+
version: "1.186.0",
|
|
5978
6040
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5979
6041
|
hooks: HOOKS,
|
|
5980
6042
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -23,6 +23,7 @@ import React, {
|
|
|
23
23
|
useContext,
|
|
24
24
|
useCallback,
|
|
25
25
|
useEffect,
|
|
26
|
+
useLayoutEffect,
|
|
26
27
|
useMemo,
|
|
27
28
|
useRef,
|
|
28
29
|
useState,
|
|
@@ -501,6 +502,47 @@ export function useFitScreen() {
|
|
|
501
502
|
return ctx.fitScreen === true;
|
|
502
503
|
}
|
|
503
504
|
|
|
505
|
+
const NO_FIT_SCREEN_INSETS = Object.freeze({ top: 0, right: 0, bottom: 0, left: 0 });
|
|
506
|
+
|
|
507
|
+
function insetPx(value) {
|
|
508
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Returns `{ top, right, bottom, left }` in px: the parts of the device's safe
|
|
513
|
+
* area (the home indicator, a system navigation bar) that this widget's
|
|
514
|
+
* surface runs under. Calling it OPTS the widget in: only then does the host
|
|
515
|
+
* run a self-scrolling Fit to screen surface (manifest `prefersFitToScreen`)
|
|
516
|
+
* to the screen's bottom edge in the exported app, so the widget's background
|
|
517
|
+
* fills it; pad your bottom-anchored controls (a composer, a pinned bar) by
|
|
518
|
+
* `bottom` to keep them clear. A widget that never calls it keeps the inset
|
|
519
|
+
* below its section. The host reports 0 while the keyboard is up (it covers
|
|
520
|
+
* that area), when the app's bottom bar holds the inset, on web (the browser
|
|
521
|
+
* keeps the page inside its own safe area) and for every widget that is not
|
|
522
|
+
* such a surface.
|
|
523
|
+
*/
|
|
524
|
+
export function useFitScreenInsets() {
|
|
525
|
+
const ctx = useWidgetContextOrThrow("useFitScreenInsets");
|
|
526
|
+
const foot = ctx.fitScreenFoot;
|
|
527
|
+
const claim = foot && typeof foot.claim === "function" ? foot.claim : null;
|
|
528
|
+
const [bottom, setBottom] = useState(0);
|
|
529
|
+
// Before paint, so the surface never shows a frame off the edge.
|
|
530
|
+
useLayoutEffect(() => {
|
|
531
|
+
if (!claim) {
|
|
532
|
+
setBottom(0);
|
|
533
|
+
return undefined;
|
|
534
|
+
}
|
|
535
|
+
const release = claim((value) => setBottom(insetPx(value)));
|
|
536
|
+
return () => {
|
|
537
|
+
if (typeof release === "function") release();
|
|
538
|
+
};
|
|
539
|
+
}, [claim]);
|
|
540
|
+
return useMemo(
|
|
541
|
+
() => (bottom === 0 ? NO_FIT_SCREEN_INSETS : { top: 0, right: 0, bottom, left: 0 }),
|
|
542
|
+
[bottom],
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
504
546
|
/**
|
|
505
547
|
* Returns the host's child-node renderer:
|
|
506
548
|
* { renderNode(node) }.
|
|
@@ -954,17 +996,44 @@ export function useWidgetRoute(initial) {
|
|
|
954
996
|
return [state, setState];
|
|
955
997
|
}
|
|
956
998
|
|
|
999
|
+
const NO_LANGUAGES = Object.freeze([]);
|
|
1000
|
+
|
|
1001
|
+
function localeError(code, message) {
|
|
1002
|
+
const err = new Error(message);
|
|
1003
|
+
err.code = code;
|
|
1004
|
+
return err;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
957
1007
|
/**
|
|
958
|
-
* Returns { t, locale }. `t(key,
|
|
959
|
-
* the host's translation table and
|
|
960
|
-
* the key is missing. The host's i18n.t
|
|
961
|
-
* form; we degrade gracefully either way.
|
|
1008
|
+
* Returns { t, locale, languages, canSetLocale, setLocale }. `t(key,
|
|
1009
|
+
* fallback)` resolves `{{t:key}}` against the host's translation table and
|
|
1010
|
+
* falls back to `fallback ?? key` when the key is missing. The host's i18n.t
|
|
1011
|
+
* may or may not honour the two-arg form; we degrade gracefully either way.
|
|
1012
|
+
*
|
|
1013
|
+
* sc-11482 — `setLocale(code)` switches the whole app through the host's own
|
|
1014
|
+
* language switch (the same one its account screen uses).
|
|
962
1015
|
*/
|
|
963
1016
|
export function useI18n() {
|
|
964
1017
|
const ctx = useWidgetContextOrThrow("useI18n");
|
|
965
1018
|
const i18n = ctx.i18n || {};
|
|
966
1019
|
const locale = typeof i18n.locale === "string" ? i18n.locale : "en";
|
|
967
1020
|
const hostT = typeof i18n.t === "function" ? i18n.t : null;
|
|
1021
|
+
const languages = Array.isArray(i18n.languages) ? i18n.languages : NO_LANGUAGES;
|
|
1022
|
+
const hostSetLocale = typeof i18n.setLocale === "function" ? i18n.setLocale : null;
|
|
1023
|
+
// Validated here so both hosts refuse an unknown code identically.
|
|
1024
|
+
const setLocale = useCallback(
|
|
1025
|
+
async (code) => {
|
|
1026
|
+
if (!hostSetLocale) {
|
|
1027
|
+
throw localeError("UNSUPPORTED", "The language cannot be changed here.");
|
|
1028
|
+
}
|
|
1029
|
+
const next = typeof code === "string" && code ? code : null;
|
|
1030
|
+
if (next !== null && !languages.some((l) => l && l.code === next)) {
|
|
1031
|
+
throw localeError("INVALID_LOCALE", `"${next}" is not one of this app's languages.`);
|
|
1032
|
+
}
|
|
1033
|
+
await hostSetLocale(next);
|
|
1034
|
+
},
|
|
1035
|
+
[hostSetLocale, languages],
|
|
1036
|
+
);
|
|
968
1037
|
// REQ-L10N-WIDGET: the widget's own manifest translations are stored in the
|
|
969
1038
|
// tenant dictionary under `widget.<id>.<key>`. Derive the namespace from the
|
|
970
1039
|
// host-provided widget id so the author calls `t("greeting")` and never
|
|
@@ -1022,7 +1091,7 @@ export function useI18n() {
|
|
|
1022
1091
|
},
|
|
1023
1092
|
[hostT, widgetId],
|
|
1024
1093
|
);
|
|
1025
|
-
return { t, locale };
|
|
1094
|
+
return { t, locale, languages, canSetLocale: hostSetLocale !== null, setLocale };
|
|
1026
1095
|
}
|
|
1027
1096
|
|
|
1028
1097
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -799,6 +799,15 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
799
799
|
* the rest of the screen). Absent / `false` everywhere else.
|
|
800
800
|
*/
|
|
801
801
|
fitScreen?: boolean;
|
|
802
|
+
/**
|
|
803
|
+
* sc-11615 — optional host hint backing `useFitScreenInsets()`: set only for
|
|
804
|
+
* a self-scrolling fit surface with nothing below it. `claim(onInset)` runs
|
|
805
|
+
* the surface under the bottom safe area, reports the inset in px now and
|
|
806
|
+
* on every change, and returns the release. Null / absent reads as zeros.
|
|
807
|
+
*/
|
|
808
|
+
fitScreenFoot?: {
|
|
809
|
+
claim(onInset: (bottom: number) => void): () => void;
|
|
810
|
+
} | null;
|
|
802
811
|
/** Active end-user identity from the host-built context (camelCase, not a wire payload). `id` is null when anonymous. */
|
|
803
812
|
user: {
|
|
804
813
|
id: string | null;
|
|
@@ -1961,9 +1970,26 @@ export function useAssetsByTag(
|
|
|
1961
1970
|
refetch(): Promise<void>;
|
|
1962
1971
|
};
|
|
1963
1972
|
|
|
1973
|
+
/** One of the app's active languages, as `useI18n().languages` lists it. */
|
|
1974
|
+
export interface AppLanguage {
|
|
1975
|
+
code: string;
|
|
1976
|
+
name: string;
|
|
1977
|
+
is_base: boolean;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
/**
|
|
1981
|
+
* The app's translations and language. `setLocale(code)` switches the whole
|
|
1982
|
+
* app live: a signed-in user's choice is saved to their account, a
|
|
1983
|
+
* visitor's stays on the device, and `null` returns to the app's default.
|
|
1984
|
+
* Rejects with an Error whose `.code` is `INVALID_LOCALE` or `UNSUPPORTED`
|
|
1985
|
+
* (`canSetLocale` is false on a host that cannot switch).
|
|
1986
|
+
*/
|
|
1964
1987
|
export function useI18n(): {
|
|
1965
1988
|
locale: string;
|
|
1966
1989
|
t(key: string, fallback?: string): string;
|
|
1990
|
+
languages: AppLanguage[];
|
|
1991
|
+
canSetLocale: boolean;
|
|
1992
|
+
setLocale(code: string | null): Promise<void>;
|
|
1967
1993
|
};
|
|
1968
1994
|
|
|
1969
1995
|
/**
|
|
@@ -2160,6 +2186,26 @@ export function useFill(): boolean;
|
|
|
2160
2186
|
*/
|
|
2161
2187
|
export function useFitScreen(): boolean;
|
|
2162
2188
|
|
|
2189
|
+
/** Insets in px, one per screen edge. */
|
|
2190
|
+
export interface FitScreenInsets {
|
|
2191
|
+
top: number;
|
|
2192
|
+
right: number;
|
|
2193
|
+
bottom: number;
|
|
2194
|
+
left: number;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
/**
|
|
2198
|
+
* sc-11615 — the parts of the device's safe area (the home indicator, a
|
|
2199
|
+
* system navigation bar) that this widget's surface runs under. Calling it
|
|
2200
|
+
* opts in: a Fit to screen section whose widget scrolls itself
|
|
2201
|
+
* (`prefersFitToScreen`) then runs to the screen's bottom edge in the exported
|
|
2202
|
+
* app; keep the background filling it and pad bottom-anchored controls (a
|
|
2203
|
+
* composer, a pinned bar) by `bottom`. Never called, the inset stays below.
|
|
2204
|
+
* All zeros while the keyboard is up, when the app's bottom bar holds the
|
|
2205
|
+
* inset, on web, and for every widget that is not such a surface.
|
|
2206
|
+
*/
|
|
2207
|
+
export function useFitScreenInsets(): FitScreenInsets;
|
|
2208
|
+
|
|
2163
2209
|
/**
|
|
2164
2210
|
* The host-provided navigation surface. `goTo(pageId, params?)` navigates
|
|
2165
2211
|
* to an internal app page; `goBack()` pops the stack. `goToStartPage()`
|
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.231.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|
package/src/contract.js
CHANGED
|
@@ -824,9 +824,21 @@ const HOOKS = [
|
|
|
824
824
|
{
|
|
825
825
|
name: "useI18n",
|
|
826
826
|
signature: "useI18n()",
|
|
827
|
+
description:
|
|
828
|
+
"The app's translations and language. t(key, fallback?) resolves an author-written key; locale is the language the app " +
|
|
829
|
+
"renders in. The app user can switch it from a widget: languages lists the app's active languages as " +
|
|
830
|
+
"[{ code, name, is_base }] (empty when the host offers none), and setLocale(code) switches the WHOLE app live, with no " +
|
|
831
|
+
"reload. A signed-in user's choice is saved to their account; a visitor who is not signed in keeps it on this device. " +
|
|
832
|
+
"setLocale(null) returns to the app's default language. setLocale resolves once the choice is saved and rejects with " +
|
|
833
|
+
"an Error whose .code is INVALID_LOCALE (the code is not one of languages) or UNSUPPORTED (this host cannot switch, " +
|
|
834
|
+
"e.g. the Builder canvas) — always catch it. canSetLocale is false on such a host: render the choices disabled then, never a dead button. " +
|
|
835
|
+
"Identical on web (Player) and the Expo export.",
|
|
827
836
|
returnShape: {
|
|
828
837
|
t: "(key: string, fallback?: string) => string",
|
|
829
838
|
locale: "string",
|
|
839
|
+
languages: "Array<{ code: string, name: string, is_base: boolean }>",
|
|
840
|
+
canSetLocale: "boolean",
|
|
841
|
+
setLocale: "(code: string | null) => Promise<void> // rejects with Error .code INVALID_LOCALE | UNSUPPORTED",
|
|
830
842
|
},
|
|
831
843
|
requiredContextSlice: ["i18n.t", "i18n.locale"],
|
|
832
844
|
scopes: null,
|
|
@@ -1009,6 +1021,31 @@ const HOOKS = [
|
|
|
1009
1021
|
requiredContextSlice: ["fitScreen"],
|
|
1010
1022
|
scopes: null,
|
|
1011
1023
|
},
|
|
1024
|
+
{
|
|
1025
|
+
name: "useFitScreenInsets",
|
|
1026
|
+
signature: "useFitScreenInsets()",
|
|
1027
|
+
description:
|
|
1028
|
+
"Returns { top, right, bottom, left } in px: the parts of the device's " +
|
|
1029
|
+
"safe area (the home indicator, a system navigation bar) that this " +
|
|
1030
|
+
"widget's surface runs under. Calling it opts the widget in: only then " +
|
|
1031
|
+
"does a Fit to screen section whose widget scrolls itself (manifest " +
|
|
1032
|
+
"prefersFitToScreen) run to the screen's bottom edge in the exported " +
|
|
1033
|
+
"app, so the widget's background fills it; a widget that never calls it " +
|
|
1034
|
+
"keeps the inset below its section. Pad the bottom-anchored controls (a composer, a pinned bar, the end of " +
|
|
1035
|
+
"a list) by bottom so they stay clear, with the background behind the " +
|
|
1036
|
+
"padding. The host reports 0 while the keyboard is up (it covers that " +
|
|
1037
|
+
"area), when the app's bottom bar holds the inset, on web (the browser " +
|
|
1038
|
+
"keeps the page inside its own safe area) and for every widget that is " +
|
|
1039
|
+
"not such a surface, so calling it is always safe.",
|
|
1040
|
+
returnShape: {
|
|
1041
|
+
top: "number",
|
|
1042
|
+
right: "number",
|
|
1043
|
+
bottom: "number",
|
|
1044
|
+
left: "number",
|
|
1045
|
+
},
|
|
1046
|
+
requiredContextSlice: ["fitScreenFoot"],
|
|
1047
|
+
scopes: null,
|
|
1048
|
+
},
|
|
1012
1049
|
{
|
|
1013
1050
|
name: "useChildRenderer",
|
|
1014
1051
|
signature: "useChildRenderer()",
|
|
@@ -3590,7 +3627,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3590
3627
|
type: "boolean",
|
|
3591
3628
|
required: false,
|
|
3592
3629
|
description:
|
|
3593
|
-
"Optional (default false). TRUE when the widget is built to own the rest of the screen: it scrolls its own content and keeps its controls in view, like a messenger, a map or a full-height board. Dropping the widget at the top level of a page in the Builder turns that section's \"Fit to screen\" setting on (the maker can switch it off, and it stays off), and the app-builder fits the section it plans the widget into. A fit section runs edge to edge across the content area and takes the rest of the screen; the widget learns it through useFill() and useFitScreen(), and the host gives it a box exactly the height left, which it scrolls inside (a widget without the field grows with its content there, and the page scrolls). Set it only for a widget that honours useFill() and scrolls inside the height it is given; leave it false (or omit) for everything else.",
|
|
3630
|
+
"Optional (default false). TRUE when the widget is built to own the rest of the screen: it scrolls its own content and keeps its controls in view, like a messenger, a map or a full-height board. Dropping the widget at the top level of a page in the Builder turns that section's \"Fit to screen\" setting on (the maker can switch it off, and it stays off), and the app-builder fits the section it plans the widget into. A fit section runs edge to edge across the content area and takes the rest of the screen; the widget learns it through useFill() and useFitScreen(), and the host gives it a box exactly the height left, which it scrolls inside (a widget without the field grows with its content there, and the page scrolls). In the exported app a widget that also calls useFitScreenInsets() has that box run to the screen's bottom edge, under the home indicator: keep the background filling it and pad bottom-anchored controls by useFitScreenInsets().bottom (a widget that never calls it keeps the inset below its section). Set it only for a widget that honours useFill() and scrolls inside the height it is given; leave it false (or omit) for everything else.",
|
|
3594
3631
|
default: false,
|
|
3595
3632
|
},
|
|
3596
3633
|
events: {
|
|
@@ -3678,6 +3715,21 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3678
3715
|
required: false,
|
|
3679
3716
|
fields: {},
|
|
3680
3717
|
},
|
|
3718
|
+
// sc-11615 — optional host hint backing useFitScreenInsets(): the screen's
|
|
3719
|
+
// foot a self-scrolling fit surface may claim.
|
|
3720
|
+
fitScreenFoot: {
|
|
3721
|
+
description:
|
|
3722
|
+
"Optional host layout hint ({ claim } or null). Set only for a " +
|
|
3723
|
+
"self-scrolling Fit to screen surface with nothing below it in the " +
|
|
3724
|
+
"exported app: claim(onInset) runs the surface under the bottom safe " +
|
|
3725
|
+
"area, calls onInset(px) now and on every change, and returns the " +
|
|
3726
|
+
"release. Backs useFitScreenInsets(); null or absent reads as zeros " +
|
|
3727
|
+
"and leaves the inset below the section.",
|
|
3728
|
+
required: false,
|
|
3729
|
+
fields: {
|
|
3730
|
+
claim: "(onInset: (bottom: number) => void) => () => void",
|
|
3731
|
+
},
|
|
3732
|
+
},
|
|
3681
3733
|
user: {
|
|
3682
3734
|
description:
|
|
3683
3735
|
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. email is null for an account created without one. Not a data-client.",
|
|
@@ -3920,12 +3972,14 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3920
3972
|
// available:false (same convention as ctx.toast / ctx.device) rather than
|
|
3921
3973
|
// throwing at render. Both the web Player and the Expo export inject it.
|
|
3922
3974
|
description:
|
|
3923
|
-
"{ t(key, fallback?), locale, translate? } — `translate` is the injected " +
|
|
3975
|
+
"{ t(key, fallback?), locale, translate?, languages?, setLocale? } — `translate` is the injected " +
|
|
3924
3976
|
"@colixsystems/translation-client ({ translate(body), status() }) that backs " +
|
|
3925
|
-
"useTranslate() for user-generated content."
|
|
3977
|
+
"useTranslate() for user-generated content. `languages` ([{ code, name, is_base }]) and " +
|
|
3978
|
+
"`setLocale(code)` back useI18n()'s language switch (sc-11482); a host that cannot " +
|
|
3979
|
+
"switch omits setLocale.",
|
|
3926
3980
|
required: true,
|
|
3927
3981
|
fields: { t: "function", locale: "string" },
|
|
3928
|
-
optionalFields: { translate: "object" },
|
|
3982
|
+
optionalFields: { translate: "object", languages: "array", setLocale: "function" },
|
|
3929
3983
|
},
|
|
3930
3984
|
logger: {
|
|
3931
3985
|
description:
|
|
@@ -5974,7 +6028,15 @@ const CONTRACT = deepFreeze({
|
|
|
5974
6028
|
// that section's Fit to screen on), and `useFitScreen()` with its
|
|
5975
6029
|
// optional `fitScreen` host hint: the widget is a fit section's surface,
|
|
5976
6030
|
// so it drops its own frame. Existing manifests and hosts read false.
|
|
5977
|
-
|
|
6031
|
+
// 1.185.0: additive (sc-11482) — `useI18n()` also returns `languages`,
|
|
6032
|
+
// `canSetLocale` and `setLocale(code)`, backed by the optional
|
|
6033
|
+
// `i18n.languages` / `i18n.setLocale` host slice: a widget can switch
|
|
6034
|
+
// the app's language. Hosts without them read [] / false.
|
|
6035
|
+
// 1.186.0: additive (sc-11615) — `useFitScreenInsets()` with its optional
|
|
6036
|
+
// `fitScreenFoot` host hint: a self-scrolling Fit to screen surface that
|
|
6037
|
+
// calls it runs under the bottom safe area on native and pads its
|
|
6038
|
+
// controls by it. Hosts without the hint read zeros.
|
|
6039
|
+
version: "1.186.0",
|
|
5978
6040
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5979
6041
|
hooks: HOOKS,
|
|
5980
6042
|
primitives: PRIMITIVES,
|