@colixsystems/widget-sdk 0.230.0 → 0.232.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 +32 -1
- package/dist/contract.cjs +50 -2
- package/dist/contract.js +50 -2
- package/dist/hooks.js +42 -0
- package/dist/host.d.ts +28 -0
- package/dist/host.js +3 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/navigation.cjs +37 -4
- package/dist/navigation.js +36 -3
- package/package.json +1 -1
- package/src/contract.js +50 -2
package/README.md
CHANGED
|
@@ -84,7 +84,38 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
84
84
|
|
|
85
85
|
## Status
|
|
86
86
|
|
|
87
|
-
`v0.
|
|
87
|
+
`v0.232.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.232.0 (contract 1.187.0)
|
|
90
|
+
|
|
91
|
+
**The app's bottom navigation bar floats by default (host side only).** `resolveFooterTokens` from `@colixsystems/widget-sdk/host` now also resolves the bar's shape: `shape` is `"floating"` (a rounded bar inset from the screen's sides, with the page visible around and beneath it) unless the theme's `footer.shape` is `"docked"` (the edge-to-edge strip). `radius` (0–40, default 28), `sideMargin` (0–48, default 12) and `bottomSpacing` (0–32, default 8) are px, clamped onto their ranges, and `itemRadius` is a tab's corner radius inside that bar. `FOOTER_SIZES` exports each measurement's bound and default, `clampFooterSize(value, key)` clamps one onto it, and `FOOTER_HOME_LINE_CLEARANCE_PX` (14) is how much of the iOS bottom inset a bar keeps clear. Nothing a widget imports changes.
|
|
92
|
+
|
|
93
|
+
### What's new in 0.231.0 (contract 1.186.0)
|
|
94
|
+
|
|
95
|
+
**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.
|
|
96
|
+
|
|
97
|
+
```jsx
|
|
98
|
+
import * as sdk from "@colixsystems/widget-sdk";
|
|
99
|
+
import { View, TextInput } from "@colixsystems/widget-sdk";
|
|
100
|
+
|
|
101
|
+
// A host older than 0.231.0 has no useFitScreenInsets.
|
|
102
|
+
const NO_INSETS = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
103
|
+
const useFitScreenInsets =
|
|
104
|
+
typeof sdk.useFitScreenInsets === "function" ? sdk.useFitScreenInsets : () => NO_INSETS;
|
|
105
|
+
|
|
106
|
+
function ComposerBar({ theme }) {
|
|
107
|
+
const { bottom } = useFitScreenInsets();
|
|
108
|
+
return (
|
|
109
|
+
<View style={{ backgroundColor: theme.colors.surface, padding: 10, paddingBottom: 10 + bottom }}>
|
|
110
|
+
<TextInput placeholder="Message" />
|
|
111
|
+
</View>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
Additive: a host without the `fitScreenFoot` hint reads zeros, so a widget that pads by it looks exactly as before there.
|
|
88
119
|
|
|
89
120
|
### What's new in 0.230.0 (contract 1.185.0)
|
|
90
121
|
|
package/dist/contract.cjs
CHANGED
|
@@ -1021,6 +1021,31 @@ const HOOKS = [
|
|
|
1021
1021
|
requiredContextSlice: ["fitScreen"],
|
|
1022
1022
|
scopes: null,
|
|
1023
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
|
+
},
|
|
1024
1049
|
{
|
|
1025
1050
|
name: "useChildRenderer",
|
|
1026
1051
|
signature: "useChildRenderer()",
|
|
@@ -3606,7 +3631,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3606
3631
|
type: "boolean",
|
|
3607
3632
|
required: false,
|
|
3608
3633
|
description:
|
|
3609
|
-
"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.",
|
|
3610
3635
|
default: false,
|
|
3611
3636
|
},
|
|
3612
3637
|
events: {
|
|
@@ -3694,6 +3719,21 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3694
3719
|
required: false,
|
|
3695
3720
|
fields: {},
|
|
3696
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
|
+
},
|
|
3697
3737
|
user: {
|
|
3698
3738
|
description:
|
|
3699
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.",
|
|
@@ -5996,7 +6036,15 @@ const CONTRACT = deepFreeze({
|
|
|
5996
6036
|
// `canSetLocale` and `setLocale(code)`, backed by the optional
|
|
5997
6037
|
// `i18n.languages` / `i18n.setLocale` host slice: a widget can switch
|
|
5998
6038
|
// the app's language. Hosts without them read [] / false.
|
|
5999
|
-
|
|
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
|
+
// 1.187.0: additive (sc-11678) — `resolveFooterTokens` gains the bottom
|
|
6044
|
+
// bar's `shape` (unset floats), `radius`, `itemRadius`, `sideMargin` and
|
|
6045
|
+
// `bottomSpacing`; host exports `FOOTER_SIZES`, `clampFooterSize` and
|
|
6046
|
+
// `FOOTER_HOME_LINE_CLEARANCE_PX`. No widget-facing surface changes.
|
|
6047
|
+
version: "1.187.0",
|
|
6000
6048
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6001
6049
|
hooks: HOOKS,
|
|
6002
6050
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1021,6 +1021,31 @@ const HOOKS = [
|
|
|
1021
1021
|
requiredContextSlice: ["fitScreen"],
|
|
1022
1022
|
scopes: null,
|
|
1023
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
|
+
},
|
|
1024
1049
|
{
|
|
1025
1050
|
name: "useChildRenderer",
|
|
1026
1051
|
signature: "useChildRenderer()",
|
|
@@ -3602,7 +3627,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3602
3627
|
type: "boolean",
|
|
3603
3628
|
required: false,
|
|
3604
3629
|
description:
|
|
3605
|
-
"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.",
|
|
3606
3631
|
default: false,
|
|
3607
3632
|
},
|
|
3608
3633
|
events: {
|
|
@@ -3690,6 +3715,21 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3690
3715
|
required: false,
|
|
3691
3716
|
fields: {},
|
|
3692
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
|
+
},
|
|
3693
3733
|
user: {
|
|
3694
3734
|
description:
|
|
3695
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.",
|
|
@@ -5992,7 +6032,15 @@ const CONTRACT = deepFreeze({
|
|
|
5992
6032
|
// `canSetLocale` and `setLocale(code)`, backed by the optional
|
|
5993
6033
|
// `i18n.languages` / `i18n.setLocale` host slice: a widget can switch
|
|
5994
6034
|
// the app's language. Hosts without them read [] / false.
|
|
5995
|
-
|
|
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
|
+
// 1.187.0: additive (sc-11678) — `resolveFooterTokens` gains the bottom
|
|
6040
|
+
// bar's `shape` (unset floats), `radius`, `itemRadius`, `sideMargin` and
|
|
6041
|
+
// `bottomSpacing`; host exports `FOOTER_SIZES`, `clampFooterSize` and
|
|
6042
|
+
// `FOOTER_HOME_LINE_CLEARANCE_PX`. No widget-facing surface changes.
|
|
6043
|
+
version: "1.187.0",
|
|
5996
6044
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5997
6045
|
hooks: HOOKS,
|
|
5998
6046
|
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) }.
|
package/dist/host.d.ts
CHANGED
|
@@ -265,8 +265,36 @@ export interface FooterTokens {
|
|
|
265
265
|
/** The active tab's surface, or `null` for none — which is what "accent"
|
|
266
266
|
* resolves to unless the author names one. */
|
|
267
267
|
activeSurface: string | null;
|
|
268
|
+
/** sc-11678: a rounded bar inset from the screen's sides, the page showing
|
|
269
|
+
* around and beneath it, or the edge-to-edge strip. Unset is "floating". */
|
|
270
|
+
shape: "floating" | "docked";
|
|
271
|
+
/** A floating bar's corner radius, px (0–40). */
|
|
272
|
+
radius: number;
|
|
273
|
+
/** A tab's corner radius, px: a floating bar's curve one row inset in
|
|
274
|
+
* (min 6) for a filled tab; 6 for an accent tab or a docked bar. */
|
|
275
|
+
itemRadius: number;
|
|
276
|
+
/** A floating bar's distance from the screen's sides, px (0–48). */
|
|
277
|
+
sideMargin: number;
|
|
278
|
+
/** Space below the tabs, px (0–32), on top of the system's clearance. */
|
|
279
|
+
bottomSpacing: number;
|
|
268
280
|
}
|
|
269
281
|
|
|
282
|
+
/** sc-11678: each bar measurement's upper bound and default, px (min 0). */
|
|
283
|
+
export const FOOTER_SIZES: Readonly<
|
|
284
|
+
Record<"radius" | "sideMargin" | "bottomSpacing", Readonly<{ max: number; fallback: number }>>
|
|
285
|
+
>;
|
|
286
|
+
|
|
287
|
+
/** sc-11678: one bar measurement clamped onto its `FOOTER_SIZES` range, or
|
|
288
|
+
* `undefined` when the value is not a finite number. */
|
|
289
|
+
export function clampFooterSize(
|
|
290
|
+
value: unknown,
|
|
291
|
+
key: "radius" | "sideMargin" | "bottomSpacing",
|
|
292
|
+
): number | undefined;
|
|
293
|
+
|
|
294
|
+
/** sc-11678: the part of the iOS bottom inset a bar keeps clear (the home
|
|
295
|
+
* line's band), px. Android's inset is cleared whole. */
|
|
296
|
+
export const FOOTER_HOME_LINE_CLEARANCE_PX: number;
|
|
297
|
+
|
|
270
298
|
/**
|
|
271
299
|
* Resolves the footer strip's tokens from a whole `theme_config`. It reads
|
|
272
300
|
* `theme.footer` and NOTHING else (sc-8033): the colours used to fall back to
|
package/dist/host.js
CHANGED
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;
|
|
@@ -2177,6 +2186,26 @@ export function useFill(): boolean;
|
|
|
2177
2186
|
*/
|
|
2178
2187
|
export function useFitScreen(): boolean;
|
|
2179
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
|
+
|
|
2180
2209
|
/**
|
|
2181
2210
|
* The host-provided navigation surface. `goTo(pageId, params?)` navigates
|
|
2182
2211
|
* to an internal app page; `goBack()` pops the stack. `goToStartPage()`
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/navigation.cjs
CHANGED
|
@@ -83,10 +83,10 @@ function menuItemCap(menuType) {
|
|
|
83
83
|
// what each workspace was inheriting, so no shipped app lost its look when the
|
|
84
84
|
// chain went (migration `20260918000000_footer_tokens_uncouple`).
|
|
85
85
|
//
|
|
86
|
-
// The
|
|
87
|
-
//
|
|
86
|
+
// The authorable keys are the controls the Design panel offers, one for one:
|
|
87
|
+
// `backgroundColor`, `textColor`, `tabActiveBackgroundColor`,
|
|
88
88
|
// `tabActiveTextColor`, `borderColor`, `borderWidth`, `activeStyle`,
|
|
89
|
-
// `tabLayout`.
|
|
89
|
+
// `tabLayout`, and (sc-11678) `shape`, `radius`, `sideMargin`, `bottomSpacing`.
|
|
90
90
|
function resolveFooterTokens(theme) {
|
|
91
91
|
const config = isPlainObject(theme) ? theme : {};
|
|
92
92
|
const footer = isPlainObject(config.footer) ? config.footer : {};
|
|
@@ -103,6 +103,8 @@ function resolveFooterTokens(theme) {
|
|
|
103
103
|
// draw nothing.
|
|
104
104
|
const borderWidth = borderColor ? footerBorderWidthOr(footer.borderWidth) : 0;
|
|
105
105
|
const divider = borderWidth > 0;
|
|
106
|
+
const floats = footer.shape !== "docked";
|
|
107
|
+
const radius = footerSizeOr(footer, "radius");
|
|
106
108
|
return {
|
|
107
109
|
backgroundColor: hexOr(footer.backgroundColor, DEFAULT_FOOTER_SURFACE),
|
|
108
110
|
textColor: hexOr(footer.textColor, DEFAULT_CHROME_TEXT),
|
|
@@ -128,9 +130,40 @@ function resolveFooterTokens(theme) {
|
|
|
128
130
|
footer.tabActiveTextColor,
|
|
129
131
|
accent ? brandPrimary(config) : FILLED_TAB_LABEL,
|
|
130
132
|
),
|
|
133
|
+
// sc-11678: unset floats, so every app gets the rounded, inset bar.
|
|
134
|
+
shape: floats ? "floating" : "docked",
|
|
135
|
+
radius,
|
|
136
|
+
// A filled tab follows a floating bar's curve one row inset (8px) in,
|
|
137
|
+
// never sharper than the docked 6px chip. An accent tab paints no surface,
|
|
138
|
+
// and a curved top edge would bend its marker into an arc.
|
|
139
|
+
itemRadius:
|
|
140
|
+
floats && !accent ? Math.max(DOCKED_TAB_RADIUS, radius - 8) : DOCKED_TAB_RADIUS,
|
|
141
|
+
sideMargin: footerSizeOr(footer, "sideMargin"),
|
|
142
|
+
bottomSpacing: footerSizeOr(footer, "bottomSpacing"),
|
|
131
143
|
};
|
|
132
144
|
}
|
|
133
145
|
|
|
146
|
+
// sc-11678: the bar's measurements, in px. `bottomSpacing` sits ABOVE what the
|
|
147
|
+
// system needs (see FOOTER_HOME_LINE_CLEARANCE_PX). Clamped, never dropped.
|
|
148
|
+
const FOOTER_SIZES = Object.freeze({
|
|
149
|
+
radius: Object.freeze({ max: 40, fallback: 28 }),
|
|
150
|
+
sideMargin: Object.freeze({ max: 48, fallback: 12 }),
|
|
151
|
+
bottomSpacing: Object.freeze({ max: 32, fallback: 8 }),
|
|
152
|
+
});
|
|
153
|
+
const DOCKED_TAB_RADIUS = 6;
|
|
154
|
+
function clampFooterSize(value, key) {
|
|
155
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
156
|
+
return Math.min(Math.max(Math.round(value), 0), FOOTER_SIZES[key].max);
|
|
157
|
+
}
|
|
158
|
+
function footerSizeOr(footer, key) {
|
|
159
|
+
return clampFooterSize(footer[key], key) ?? FOOTER_SIZES[key].fallback;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// sc-11678: of the iOS bottom inset, only the home line's band is kept clear;
|
|
163
|
+
// a bar may sit in the rest of it, as Apple's and most apps' tab bars do.
|
|
164
|
+
// Android's inset is its navigation bar, so there the whole inset is cleared.
|
|
165
|
+
const FOOTER_HOME_LINE_CLEARANCE_PX = 14;
|
|
166
|
+
|
|
134
167
|
// REQ-THEME-14 — the rail's and the app bar's tokens, resolved once for both
|
|
135
168
|
// hosts.
|
|
136
169
|
//
|
|
@@ -844,4 +877,4 @@ function borderWidthOr(...values) {
|
|
|
844
877
|
return BORDER_WIDTH_MIN;
|
|
845
878
|
}
|
|
846
879
|
|
|
847
|
-
module.exports = { normaliseNavigation, menuItemCap, resolveFooterTokens, resolveSidebarTokens, resolveTopStripeLayout, resolveTopBarTokens, topStripeShowKey, TOP_STRIPE_ALWAYS_SHOWN_SLOTS, TOP_STRIPE_SLOTS, MIN_TOP_STRIPE_HEIGHT_PX, MAX_TOP_STRIPE_HEIGHT_PX, MIN_TOP_STRIPE_PADDING_X_PX, MAX_TOP_STRIPE_PADDING_X_PX, MAX_TOP_STRIPE_OFFSET_Y_PX, DEFAULT_TOP_STRIPE_HEIGHT_PX, DEFAULT_TOP_STRIPE_PADDING_X_PX, resolveSidebarUnfold, resolveLocalNavModel, drawsLocalNavAnywhere, isDarkSurface, resolveAppLogo };
|
|
880
|
+
module.exports = { normaliseNavigation, menuItemCap, resolveFooterTokens, FOOTER_SIZES, clampFooterSize, FOOTER_HOME_LINE_CLEARANCE_PX, resolveSidebarTokens, resolveTopStripeLayout, resolveTopBarTokens, topStripeShowKey, TOP_STRIPE_ALWAYS_SHOWN_SLOTS, TOP_STRIPE_SLOTS, MIN_TOP_STRIPE_HEIGHT_PX, MAX_TOP_STRIPE_HEIGHT_PX, MIN_TOP_STRIPE_PADDING_X_PX, MAX_TOP_STRIPE_PADDING_X_PX, MAX_TOP_STRIPE_OFFSET_Y_PX, DEFAULT_TOP_STRIPE_HEIGHT_PX, DEFAULT_TOP_STRIPE_PADDING_X_PX, resolveSidebarUnfold, resolveLocalNavModel, drawsLocalNavAnywhere, isDarkSurface, resolveAppLogo };
|
package/dist/navigation.js
CHANGED
|
@@ -74,10 +74,10 @@ export function menuItemCap(menuType) {
|
|
|
74
74
|
// what each workspace was inheriting, so no shipped app lost its look when the
|
|
75
75
|
// chain went (migration `20260918000000_footer_tokens_uncouple`).
|
|
76
76
|
//
|
|
77
|
-
// The
|
|
78
|
-
//
|
|
77
|
+
// The authorable keys are the controls the Design panel offers, one for one:
|
|
78
|
+
// `backgroundColor`, `textColor`, `tabActiveBackgroundColor`,
|
|
79
79
|
// `tabActiveTextColor`, `borderColor`, `borderWidth`, `activeStyle`,
|
|
80
|
-
// `tabLayout`.
|
|
80
|
+
// `tabLayout`, and (sc-11678) `shape`, `radius`, `sideMargin`, `bottomSpacing`.
|
|
81
81
|
export function resolveFooterTokens(theme) {
|
|
82
82
|
const config = isPlainObject(theme) ? theme : {};
|
|
83
83
|
const footer = isPlainObject(config.footer) ? config.footer : {};
|
|
@@ -94,6 +94,8 @@ export function resolveFooterTokens(theme) {
|
|
|
94
94
|
// draw nothing.
|
|
95
95
|
const borderWidth = borderColor ? footerBorderWidthOr(footer.borderWidth) : 0;
|
|
96
96
|
const divider = borderWidth > 0;
|
|
97
|
+
const floats = footer.shape !== "docked";
|
|
98
|
+
const radius = footerSizeOr(footer, "radius");
|
|
97
99
|
return {
|
|
98
100
|
backgroundColor: hexOr(footer.backgroundColor, DEFAULT_FOOTER_SURFACE),
|
|
99
101
|
textColor: hexOr(footer.textColor, DEFAULT_CHROME_TEXT),
|
|
@@ -119,9 +121,40 @@ export function resolveFooterTokens(theme) {
|
|
|
119
121
|
footer.tabActiveTextColor,
|
|
120
122
|
accent ? brandPrimary(config) : FILLED_TAB_LABEL,
|
|
121
123
|
),
|
|
124
|
+
// sc-11678: unset floats, so every app gets the rounded, inset bar.
|
|
125
|
+
shape: floats ? "floating" : "docked",
|
|
126
|
+
radius,
|
|
127
|
+
// A filled tab follows a floating bar's curve one row inset (8px) in,
|
|
128
|
+
// never sharper than the docked 6px chip. An accent tab paints no surface,
|
|
129
|
+
// and a curved top edge would bend its marker into an arc.
|
|
130
|
+
itemRadius:
|
|
131
|
+
floats && !accent ? Math.max(DOCKED_TAB_RADIUS, radius - 8) : DOCKED_TAB_RADIUS,
|
|
132
|
+
sideMargin: footerSizeOr(footer, "sideMargin"),
|
|
133
|
+
bottomSpacing: footerSizeOr(footer, "bottomSpacing"),
|
|
122
134
|
};
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
// sc-11678: the bar's measurements, in px. `bottomSpacing` sits ABOVE what the
|
|
138
|
+
// system needs (see FOOTER_HOME_LINE_CLEARANCE_PX). Clamped, never dropped.
|
|
139
|
+
export const FOOTER_SIZES = Object.freeze({
|
|
140
|
+
radius: Object.freeze({ max: 40, fallback: 28 }),
|
|
141
|
+
sideMargin: Object.freeze({ max: 48, fallback: 12 }),
|
|
142
|
+
bottomSpacing: Object.freeze({ max: 32, fallback: 8 }),
|
|
143
|
+
});
|
|
144
|
+
const DOCKED_TAB_RADIUS = 6;
|
|
145
|
+
export function clampFooterSize(value, key) {
|
|
146
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
147
|
+
return Math.min(Math.max(Math.round(value), 0), FOOTER_SIZES[key].max);
|
|
148
|
+
}
|
|
149
|
+
function footerSizeOr(footer, key) {
|
|
150
|
+
return clampFooterSize(footer[key], key) ?? FOOTER_SIZES[key].fallback;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// sc-11678: of the iOS bottom inset, only the home line's band is kept clear;
|
|
154
|
+
// a bar may sit in the rest of it, as Apple's and most apps' tab bars do.
|
|
155
|
+
// Android's inset is its navigation bar, so there the whole inset is cleared.
|
|
156
|
+
export const FOOTER_HOME_LINE_CLEARANCE_PX = 14;
|
|
157
|
+
|
|
125
158
|
// REQ-THEME-14 — the rail's and the app bar's tokens, resolved once for both
|
|
126
159
|
// hosts.
|
|
127
160
|
//
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.232.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
|
@@ -1021,6 +1021,31 @@ const HOOKS = [
|
|
|
1021
1021
|
requiredContextSlice: ["fitScreen"],
|
|
1022
1022
|
scopes: null,
|
|
1023
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
|
+
},
|
|
1024
1049
|
{
|
|
1025
1050
|
name: "useChildRenderer",
|
|
1026
1051
|
signature: "useChildRenderer()",
|
|
@@ -3602,7 +3627,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3602
3627
|
type: "boolean",
|
|
3603
3628
|
required: false,
|
|
3604
3629
|
description:
|
|
3605
|
-
"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.",
|
|
3606
3631
|
default: false,
|
|
3607
3632
|
},
|
|
3608
3633
|
events: {
|
|
@@ -3690,6 +3715,21 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3690
3715
|
required: false,
|
|
3691
3716
|
fields: {},
|
|
3692
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
|
+
},
|
|
3693
3733
|
user: {
|
|
3694
3734
|
description:
|
|
3695
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.",
|
|
@@ -5992,7 +6032,15 @@ const CONTRACT = deepFreeze({
|
|
|
5992
6032
|
// `canSetLocale` and `setLocale(code)`, backed by the optional
|
|
5993
6033
|
// `i18n.languages` / `i18n.setLocale` host slice: a widget can switch
|
|
5994
6034
|
// the app's language. Hosts without them read [] / false.
|
|
5995
|
-
|
|
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
|
+
// 1.187.0: additive (sc-11678) — `resolveFooterTokens` gains the bottom
|
|
6040
|
+
// bar's `shape` (unset floats), `radius`, `itemRadius`, `sideMargin` and
|
|
6041
|
+
// `bottomSpacing`; host exports `FOOTER_SIZES`, `clampFooterSize` and
|
|
6042
|
+
// `FOOTER_HOME_LINE_CLEARANCE_PX`. No widget-facing surface changes.
|
|
6043
|
+
version: "1.187.0",
|
|
5996
6044
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5997
6045
|
hooks: HOOKS,
|
|
5998
6046
|
primitives: PRIMITIVES,
|