@colixsystems/widget-sdk 0.142.1 → 0.144.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 CHANGED
@@ -32,6 +32,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
32
32
  | **CORE** | `useContainerWidth()` | `[width, onLayout]` | No context slice, no scope. Measures the width the widget's OWN box has, so it can lay itself out for the space it is in rather than for the screen. Spread the handler onto your outermost primitive. Use it for any widget with a wide form and a narrow one (a table, a toolbar, a row of tiles) — never switch on the device or window width, because a widget in a one-of-three grid cell on desktop has phone-width room and a widget filling a phone page does not. Width is 0 until the first layout: render the WIDE form then. One implementation covers web (react-native-web) and the native export. |
33
33
  | **CORE** | `isNarrowWidth(width)` | `boolean` | No context slice, no scope. True when a MEASURED width is below `NARROW_WIDTH_PX` (480) — the one threshold every widget switches at, so a page reflows together rather than raggedly. An unmeasured width (0) is NOT narrow, so nothing flashes through the narrow form on first paint. |
34
34
  | **CORE** | `useSectionEmpty(isEmpty)` | `void` | `ctx.section.reportEmpty` — no scope. Declares that the widget has NO content to show, so the host drops its layout slot instead of reserving space (and its parent's `gap`) for it. Returning `null` is not enough: the host wraps every widget in an entrance element, so a widget rendering nothing still leaves an empty box the parent stack gaps around. For a CONDITIONALLY ABSENT section (a per-record child collection with no rows for this record), never to suppress a genuine empty state. Stays mounted while collapsed, so passing `false` brings it back. Authoring surfaces never collapse. No-op on a host that doesn't implement it. |
35
+ | **CORE** | `useScreenTitle(title)` | `void` | `ctx.screen.reportTitle` — no scope. Names the screen the widget is on, so the host paints that name in the app header beside the back control instead of the app brand. The page's own name is the default; only the widget knows the screen is really "Order #1423" rather than "Orders". The MOST RECENT call wins — with several widgets on one screen the last to report owns the title, and on unmount (or an empty title) the host falls back to the most recent surviving report and finally to the page name. Only a screen showing the back control paints a title; a root page keeps the brand. No-op on a host that doesn't implement it. |
35
36
  | **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. |
36
37
  | **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
37
38
  | **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` — wired by the Player and the Expo export; an authoring preview omits it and the call is a no-op — no scope |
@@ -70,7 +71,28 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
70
71
 
71
72
  ## Status
72
73
 
73
- `v0.142.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**.
74
+ `v0.144.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**.
75
+
76
+ ### What's new in 0.144.0 (contract 1.112.0)
77
+
78
+ **The global mobile quick bar is retired — `quickBarMaxItems` and `quickBarCap` leave the contract (sc-7753, REQ-NAV-STRUCTURE / REQ-NAV-LOCAL).** The quick bar was a second global menu bolted to the sidebar: its own per-page flag, its own five-item cap, its own strip on the app's bottom edge. Since sc-7695 a page can carry a bottom navigation bar of its own drawing a saved menu, which is the same strip with an author-chosen list behind it — so the global one was a duplicate shape.
79
+
80
+ - `CONTRACT.themeMenuTypes.sidebar` is now `{ name, summary, maxItems }`. `menuItemCap` is unchanged; `quickBarCap` is **removed** — a page's bottom bar draws the menu the author composed, so there is no global number to state.
81
+ - `resolveFooterTokens` stays exactly as it is. It was never the quick bar's alone: it is the BOTTOM EDGE's token set, and the page's own bottom bar reads it, so a workspace's bottom-edge colours survive the retirement untouched.
82
+ - The sc-7753 backfill turns each workspace's quick-bar pages into a saved menu and points the pages that drew the strip at it BEFORE this ships, so no destination is lost.
83
+ - Host-integration surface only; no author-facing hook, prop or manifest field changed. `CONTRACT.version` → `1.112.0`.
84
+ **A widget can name the screen it is on — `useScreenTitle(title)` + the optional `ctx.screen` slice (sc-7955).** The app header now shows the PAGE title beside the back control on a sub-page, instead of repeating the app brand on every screen. The page's own name is the default, but a page is often about one record, and only the widget has it:
85
+
86
+ ```js
87
+ const { data: order } = useDatastoreRecord(tableId, recordId);
88
+ useScreenTitle(order ? `Order #${order.reference}` : null);
89
+ ```
90
+
91
+ - The header reads "Order #1423" rather than "Orders" as soon as the record lands; before it does, the blank title leaves the page name standing (only a non-empty string registers).
92
+ - `reportTitle` returns a release function, so the title reverts cleanly when the widget unmounts or its title empties.
93
+ - **The most recent call wins.** With two title-setting widgets on one screen the last to report owns the header; when it goes, the host falls back to the most recent surviving report, then to the page name.
94
+ - Only a screen that shows the back control paints a title — a root/menu page keeps the app brand or logo untouched.
95
+ - Optional slice: on the Studio canvas and the Agent Mode edit preview there is no app header, so the hook is a no-op rather than an error.
74
96
 
75
97
  ### What's new in 0.142.1
76
98
 
@@ -99,8 +121,8 @@ if (triggeredBy && triggeredBy.type === "app_user") {
99
121
 
100
122
  **The app's global navigation is the sidebar alone — `top-bar` and `bottom-tabs` leave `CONTRACT.themeMenuTypes` (sc-7352, REQ-NAV-STRUCTURE).** A top bar is now a PAGE's local bar drawing a saved menu (REQ-NAV-LOCAL, sc-7103): the same tab row the `top-bar` shape used to draw, themed by the same `topBar` tokens (`resolveTopBarTokens` is unchanged), worn by one page under the sidebar. Two "top bars" competing for one word and one token set was the reason.
101
123
 
102
- - `normaliseNavigation` resolves a stored `top-bar` or `bottom-tabs` to `sidebar`, so a host never has to guard the value. `menuItemCap` / `quickBarCap` keep their signatures (`quickBarCap("sidebar")` is still 5).
103
- - The global menu never lived in the shape (it is the pages' `show_in_menu` flags), so a workspace that stored a retired shape already draws the sidebar with the same menu. The sc-7352 backfill deletes the stored key BEFORE the shapes disappear; a bottom-tabs app's phone strip becomes the sidebar's quick bar.
124
+ - `normaliseNavigation` resolves a stored `top-bar` or `bottom-tabs` to `sidebar`, so a host never has to guard the value. `menuItemCap` / `quickBarCap` keep their signatures (`quickBarCap("sidebar")` is still 5). *(`quickBarCap` was removed in 0.142.0.)*
125
+ - The global menu never lived in the shape (it is the pages' `show_in_menu` flags), so a workspace that stored a retired shape already draws the sidebar with the same menu. The sc-7352 backfill deletes the stored key BEFORE the shapes disappear; a bottom-tabs app's phone strip becomes the sidebar's quick bar. *(That strip was itself retired in 0.142.0.)*
104
126
  - Host-integration surface only; no author-facing hook, prop or manifest field changed. `CONTRACT.version` → `1.109.0`.
105
127
 
106
128
  ### What's new in 0.140.0 (contract 1.108.0)
package/dist/contract.cjs CHANGED
@@ -220,16 +220,13 @@ const THEME_WIDGET_STYLES = Object.freeze({
220
220
  // REQ-NAV-STRUCTURE (sc-7352): the app's GLOBAL navigation has exactly one
221
221
  // shape, the sidebar. A top bar is a PAGE's local bar drawing a saved menu
222
222
  // (REQ-NAV-LOCAL), themed by the `topBar` tokens, never an app-wide menu shape.
223
- // `maxItems` is the menu cap (none for a rail); `quickBarMaxItems` caps the
224
- // SECONDARY mobile quick bar, which may drop a page because the rail and the
225
- // drawer still list every one.
223
+ // `maxItems` is the menu cap (none for a rail).
226
224
  const THEME_MENU_TYPES = Object.freeze({
227
225
  sidebar: Object.freeze({
228
226
  name: "Sidebar",
229
227
  summary:
230
- "A persistent left rail on desktop; a hamburger drawer plus the optional bottom quick bar on mobile.",
228
+ "A persistent left rail on desktop; a hamburger drawer on mobile.",
231
229
  maxItems: null,
232
- quickBarMaxItems: 5,
233
230
  }),
234
231
  });
235
232
  const THEME_SPACING_SCALE = Object.freeze({
@@ -718,6 +715,26 @@ const HOOKS = [
718
715
  requiredContextSlice: ["section.reportEmpty"],
719
716
  scopes: null,
720
717
  },
718
+ {
719
+ name: "useScreenTitle",
720
+ signature: "useScreenTitle(title)",
721
+ description:
722
+ "Name the screen this widget is on, so the host paints that name in the " +
723
+ "app header beside the back control. The page's own name is the default; " +
724
+ "only the widget knows when the screen is really \"Order #1423\" rather " +
725
+ "than \"Orders\". The MOST RECENT call wins: with several widgets on one " +
726
+ "screen the last to report owns the title, and when that widget unmounts " +
727
+ "(or passes an empty title) the host falls back to the most recent " +
728
+ "surviving report and finally to the page name. Only a screen that shows " +
729
+ "the back control paints a title — a root page keeps the app brand, so a " +
730
+ "title reported there is held but not shown. Safe on a host that does not " +
731
+ "implement it (Studio canvas, Agent Mode edit preview) — a no-op there.",
732
+ returnShape: {
733
+ "(returns)": "void",
734
+ },
735
+ requiredContextSlice: ["screen.reportTitle"],
736
+ scopes: null,
737
+ },
721
738
  {
722
739
  name: "useNavigation",
723
740
  signature: "useNavigation()",
@@ -2321,6 +2338,22 @@ const WIDGET_CONTEXT_SHAPE = {
2321
2338
  required: false,
2322
2339
  fields: { subscribe: "function" },
2323
2340
  },
2341
+ // sc-7955 — widget -> host screen-chrome signal. The widget owns the knowledge
2342
+ // (it has the record), the host owns the chrome (the header title slot).
2343
+ // Optional so an AUTHORING host can omit it: with no slice the hook is a
2344
+ // no-op on the Studio canvas and the Agent Mode edit preview, neither of
2345
+ // which paints an app header at all.
2346
+ screen: {
2347
+ description:
2348
+ "Optional screen-chrome slot. { reportTitle(title) -> release }. " +
2349
+ "Registers this widget's title for the screen it is on and returns a " +
2350
+ "release function that withdraws it; the host paints the most recently " +
2351
+ "registered title in the header beside the back control and falls back " +
2352
+ "to the page name once every registration is released. Backs " +
2353
+ "useScreenTitle().",
2354
+ required: false,
2355
+ fields: { reportTitle: "function" },
2356
+ },
2324
2357
  // sc-4416 — widget -> host emptiness signal. The widget owns the knowledge
2325
2358
  // (it has the rows); the host owns the spacing (the slot and its gap).
2326
2359
  // Optional so an AUTHORING host can omit it on purpose: with no slice the
@@ -3983,6 +4016,14 @@ const CONTRACT = deepFreeze({
3983
4016
  // and the sc-7352 backfill deletes the stored key (a bottom-tabs app's
3984
4017
  // strip becomes the sidebar's quick bar) BEFORE this ships.
3985
4018
  // `menuItemCap` / `quickBarCap` keep their signatures.
4019
+ // 1.111.0: additive (sc-7955) -- `screen` joins `widgetContextShape` and
4020
+ // `useScreenTitle(title)` joins `hooks`: a widget names the screen it is
4021
+ // on, and the host paints that name in the header beside the back control
4022
+ // instead of the app brand. `reportTitle` returns a release function; the
4023
+ // MOST RECENT registration wins and the host falls back to the most recent
4024
+ // surviving one, then to the page name. The slice is OPTIONAL, so an
4025
+ // authoring host (Studio canvas, Agent Mode edit preview) omits it and the
4026
+ // hook is a no-op there rather than an error.
3986
4027
  // 1.110.0: additive (sc-7885) — `triggeredBy` joins `actionScriptGlobals`:
3987
4028
  // WHO initiated this run, as `{ type, id, name, email, role }`, or `null`
3988
4029
  // on `schedule` / `record_*` where nothing did. `type` is `"app_user"` (an
@@ -3992,7 +4033,15 @@ const CONTRACT = deepFreeze({
3992
4033
  // integration identity; this describes the caller, it does not grant
3993
4034
  // anything. The same commit adds `properties` to this list, which the
3994
4035
  // action-sdk copy had carried since 1.50.0 while this one had not.
3995
- version: "1.110.0",
4036
+ // 1.112.0: BREAKING on the pre-1.0 channel (sc-7753) — `themeMenuTypes`
4037
+ // loses `quickBarMaxItems` and the `quickBarCap` host export goes with
4038
+ // it. The app's global mobile quick bar is retired: it was a SELECTION
4039
+ // over the global menu, and the rail and drawer list every menu page
4040
+ // regardless, so nothing was reachable only through it. The bottom edge
4041
+ // belongs to a PAGE's own bottom navigation bar (`local_nav = BOTTOM_BAR`,
4042
+ // REQ-NAV-LOCAL), chosen per page. `resolveFooterTokens` is unchanged —
4043
+ // it is the bottom edge's token set and the surviving bar reads it.
4044
+ version: "1.112.0",
3996
4045
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3997
4046
  hooks: HOOKS,
3998
4047
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -220,16 +220,13 @@ const THEME_WIDGET_STYLES = Object.freeze({
220
220
  // REQ-NAV-STRUCTURE (sc-7352): the app's GLOBAL navigation has exactly one
221
221
  // shape, the sidebar. A top bar is a PAGE's local bar drawing a saved menu
222
222
  // (REQ-NAV-LOCAL), themed by the `topBar` tokens, never an app-wide menu shape.
223
- // `maxItems` is the menu cap (none for a rail); `quickBarMaxItems` caps the
224
- // SECONDARY mobile quick bar, which may drop a page because the rail and the
225
- // drawer still list every one.
223
+ // `maxItems` is the menu cap (none for a rail).
226
224
  const THEME_MENU_TYPES = Object.freeze({
227
225
  sidebar: Object.freeze({
228
226
  name: "Sidebar",
229
227
  summary:
230
- "A persistent left rail on desktop; a hamburger drawer plus the optional bottom quick bar on mobile.",
228
+ "A persistent left rail on desktop; a hamburger drawer on mobile.",
231
229
  maxItems: null,
232
- quickBarMaxItems: 5,
233
230
  }),
234
231
  });
235
232
  const THEME_SPACING_SCALE = Object.freeze({
@@ -718,6 +715,26 @@ const HOOKS = [
718
715
  requiredContextSlice: ["section.reportEmpty"],
719
716
  scopes: null,
720
717
  },
718
+ {
719
+ name: "useScreenTitle",
720
+ signature: "useScreenTitle(title)",
721
+ description:
722
+ "Name the screen this widget is on, so the host paints that name in the " +
723
+ "app header beside the back control. The page's own name is the default; " +
724
+ "only the widget knows when the screen is really \"Order #1423\" rather " +
725
+ "than \"Orders\". The MOST RECENT call wins: with several widgets on one " +
726
+ "screen the last to report owns the title, and when that widget unmounts " +
727
+ "(or passes an empty title) the host falls back to the most recent " +
728
+ "surviving report and finally to the page name. Only a screen that shows " +
729
+ "the back control paints a title — a root page keeps the app brand, so a " +
730
+ "title reported there is held but not shown. Safe on a host that does not " +
731
+ "implement it (Studio canvas, Agent Mode edit preview) — a no-op there.",
732
+ returnShape: {
733
+ "(returns)": "void",
734
+ },
735
+ requiredContextSlice: ["screen.reportTitle"],
736
+ scopes: null,
737
+ },
721
738
  {
722
739
  name: "useNavigation",
723
740
  signature: "useNavigation()",
@@ -2317,6 +2334,22 @@ const WIDGET_CONTEXT_SHAPE = {
2317
2334
  required: false,
2318
2335
  fields: { subscribe: "function" },
2319
2336
  },
2337
+ // sc-7955 — widget -> host screen-chrome signal. The widget owns the knowledge
2338
+ // (it has the record), the host owns the chrome (the header title slot).
2339
+ // Optional so an AUTHORING host can omit it: with no slice the hook is a
2340
+ // no-op on the Studio canvas and the Agent Mode edit preview, neither of
2341
+ // which paints an app header at all.
2342
+ screen: {
2343
+ description:
2344
+ "Optional screen-chrome slot. { reportTitle(title) -> release }. " +
2345
+ "Registers this widget's title for the screen it is on and returns a " +
2346
+ "release function that withdraws it; the host paints the most recently " +
2347
+ "registered title in the header beside the back control and falls back " +
2348
+ "to the page name once every registration is released. Backs " +
2349
+ "useScreenTitle().",
2350
+ required: false,
2351
+ fields: { reportTitle: "function" },
2352
+ },
2320
2353
  // sc-4416 — widget -> host emptiness signal. The widget owns the knowledge
2321
2354
  // (it has the rows); the host owns the spacing (the slot and its gap).
2322
2355
  // Optional so an AUTHORING host can omit it on purpose: with no slice the
@@ -3979,6 +4012,14 @@ const CONTRACT = deepFreeze({
3979
4012
  // and the sc-7352 backfill deletes the stored key (a bottom-tabs app's
3980
4013
  // strip becomes the sidebar's quick bar) BEFORE this ships.
3981
4014
  // `menuItemCap` / `quickBarCap` keep their signatures.
4015
+ // 1.111.0: additive (sc-7955) -- `screen` joins `widgetContextShape` and
4016
+ // `useScreenTitle(title)` joins `hooks`: a widget names the screen it is
4017
+ // on, and the host paints that name in the header beside the back control
4018
+ // instead of the app brand. `reportTitle` returns a release function; the
4019
+ // MOST RECENT registration wins and the host falls back to the most recent
4020
+ // surviving one, then to the page name. The slice is OPTIONAL, so an
4021
+ // authoring host (Studio canvas, Agent Mode edit preview) omits it and the
4022
+ // hook is a no-op there rather than an error.
3982
4023
  // 1.110.0: additive (sc-7885) — `triggeredBy` joins `actionScriptGlobals`:
3983
4024
  // WHO initiated this run, as `{ type, id, name, email, role }`, or `null`
3984
4025
  // on `schedule` / `record_*` where nothing did. `type` is `"app_user"` (an
@@ -3988,7 +4029,15 @@ const CONTRACT = deepFreeze({
3988
4029
  // integration identity; this describes the caller, it does not grant
3989
4030
  // anything. The same commit adds `properties` to this list, which the
3990
4031
  // action-sdk copy had carried since 1.50.0 while this one had not.
3991
- version: "1.110.0",
4032
+ // 1.112.0: BREAKING on the pre-1.0 channel (sc-7753) — `themeMenuTypes`
4033
+ // loses `quickBarMaxItems` and the `quickBarCap` host export goes with
4034
+ // it. The app's global mobile quick bar is retired: it was a SELECTION
4035
+ // over the global menu, and the rail and drawer list every menu page
4036
+ // regardless, so nothing was reachable only through it. The bottom edge
4037
+ // belongs to a PAGE's own bottom navigation bar (`local_nav = BOTTOM_BAR`,
4038
+ // REQ-NAV-LOCAL), chosen per page. `resolveFooterTokens` is unchanged —
4039
+ // it is the bottom edge's token set and the surviving bar reads it.
4040
+ version: "1.112.0",
3992
4041
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3993
4042
  hooks: HOOKS,
3994
4043
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -386,6 +386,36 @@ export function useSectionEmpty(isEmpty) {
386
386
  }, [empty]);
387
387
  }
388
388
 
389
+ /**
390
+ * sc-7955 — name the screen this widget is on, so the host paints that name in
391
+ * the header beside the back control. The page name is the default; only the
392
+ * widget knows when the screen is really "Order #1423" rather than "Orders".
393
+ *
394
+ * The MOST RECENT call wins: with several widgets on one screen the last to
395
+ * report owns the title, and when that widget unmounts (or passes an empty
396
+ * title) the host falls back to the most recent surviving report, then to the
397
+ * page name. Pass a blank or non-string title to bow out without unmounting
398
+ * (only a non-empty string registers, so a numeric id registers nothing).
399
+ *
400
+ * Keep it short — roughly 25 characters. The slot is one line and truncates
401
+ * with an ellipsis, sharing the row with back and the bell/identity cluster.
402
+ *
403
+ * Only a screen that shows the back control paints a title — a root page keeps
404
+ * the app brand, so a title reported there is held but not shown.
405
+ *
406
+ * Safe to call on a host that does not implement it: the hook is a no-op on the
407
+ * Studio canvas and the Agent Mode edit preview, which paint no app header.
408
+ */
409
+ export function useScreenTitle(title) {
410
+ const ctx = useWidgetContextOrThrow("useScreenTitle");
411
+ const report = ctx.screen && ctx.screen.reportTitle;
412
+ const text = typeof title === "string" ? title.trim() : "";
413
+ useEffect(() => {
414
+ if (typeof report !== "function" || !text) return undefined;
415
+ return report(text);
416
+ }, [report, text]);
417
+ }
418
+
389
419
  /**
390
420
  * sc-4399 — measure the width the widget's own box actually has, so it can lay
391
421
  * itself out for the space it is in rather than for the screen. A widget in a
package/dist/host.d.ts CHANGED
@@ -173,15 +173,9 @@ export function normaliseNavigation(navigation: unknown): ResolvedNavigation;
173
173
  */
174
174
  export function menuItemCap(menuType: string): number | null;
175
175
 
176
- /**
177
- * How many pages the SECONDARY mobile quick bar draws, or `null` where the shape
178
- * draws none (only `sidebar` has one). Unlike `menuItemCap` this cap MAY drop a
179
- * page: the rail and drawer still list every menu page.
180
- */
181
- export function quickBarCap(menuType: string): number | null;
182
-
183
- /** The footer strip's resolved tokens. A `null` colour means the host keeps its
184
- * own default; a `null` `borderColor` means no divider is drawn at all. */
176
+ /** The app's bottom edge — a page's own bottom navigation bar. A `null` colour
177
+ * means the host keeps its own default; a `null` `borderColor` means no
178
+ * divider is drawn at all. */
185
179
  export interface FooterTokens {
186
180
  backgroundColor: string | null;
187
181
  textColor: string | null;
package/dist/host.js CHANGED
@@ -55,18 +55,17 @@ export {
55
55
  // REQ-NAV-STRUCTURE: the navigation resolver both hosts draw their chrome from.
56
56
  // `normaliseNavigation` turns a stored `theme_config.navigation` block into the
57
57
  // resolved `{ menuType }` — always `sidebar` since sc-7352 retired the other
58
- // shapes, so a stored value never needs guarding; `menuItemCap` / `quickBarCap`
59
- // state the caps. One implementation, so the chrome cannot mean one thing in
60
- // the Player and another in the Expo export. `resolveFooterTokens` is the quick
61
- // bar's own token set, falling back to the sidebar's so an untouched workspace
62
- // keeps the appearance it has.
58
+ // shapes, so a stored value never needs guarding; `menuItemCap` states the cap.
59
+ // One implementation, so the chrome cannot mean one thing in the Player and
60
+ // another in the Expo export. `resolveFooterTokens` is the BOTTOM EDGE's token
61
+ // set — a page's own bottom navigation bar (REQ-NAV-LOCAL) — falling back to
62
+ // the sidebar's so an untouched workspace keeps the appearance it has.
63
63
  // REQ-THEME-14: `resolveSidebarTokens` and `resolveTopBarTokens` are the same
64
64
  // idea for the rail and the app bar, whose defaults used to live inline in each
65
65
  // host and had already drifted apart.
66
66
  export {
67
67
  normaliseNavigation,
68
68
  menuItemCap,
69
- quickBarCap,
70
69
  resolveFooterTokens,
71
70
  resolveSidebarTokens,
72
71
  resolveTopBarTokens,
package/dist/index.d.ts CHANGED
@@ -1473,6 +1473,8 @@ export function useRefresh(
1473
1473
  * collapse. Safe on a host that does not implement it — a no-op there.
1474
1474
  */
1475
1475
  export function useSectionEmpty(isEmpty: boolean): void;
1476
+ /** sc-7955 — name this screen for the header title slot; most recent call wins. */
1477
+ export function useScreenTitle(title: string | null | undefined): void;
1476
1478
 
1477
1479
  /** The layout event a react-native primitive passes to `onLayout`. */
1478
1480
  export interface WidgetLayoutEvent {
package/dist/index.js CHANGED
@@ -75,6 +75,7 @@ export {
75
75
  useChildRenderer,
76
76
  useRefresh,
77
77
  useSectionEmpty,
78
+ useScreenTitle,
78
79
  useContainerWidth,
79
80
  useGeolocation,
80
81
  GeolocationError,
@@ -75,6 +75,7 @@ export {
75
75
  useChildRenderer,
76
76
  useRefresh,
77
77
  useSectionEmpty,
78
+ useScreenTitle,
78
79
  useContainerWidth,
79
80
  useGeolocation,
80
81
  GeolocationError,
@@ -60,7 +60,7 @@ function normaliseNavigation(navigation) {
60
60
 
61
61
  /**
62
62
  * How many MENU pages the chrome may draw at once, or `null` for no cap. The
63
- * sidebar has none. Contrast `quickBarCap` below.
63
+ * sidebar has none.
64
64
  *
65
65
  * @param {string} menuType — a resolved menu type.
66
66
  * @returns {number|null}
@@ -70,35 +70,12 @@ function menuItemCap(menuType) {
70
70
  return entry ? entry.maxItems : null;
71
71
  }
72
72
 
73
- /**
74
- * How many pages the SECONDARY mobile quick bar draws, or `null` where that
75
- * shape draws none. Only the `sidebar` shape has one.
76
- *
77
- * This cap MAY drop a page, and that is the difference from `menuItemCap`: the
78
- * rail and the drawer still list every menu page, so the bar is a shortcut
79
- * rather than the menu. It lived as a constant on one host and a literal on the
80
- * other before this, which is exactly how the two would have drifted.
81
- *
82
- * @param {string} menuType — a resolved menu type.
83
- * @returns {number|null}
84
- */
85
- function quickBarCap(menuType) {
86
- const entry = CONTRACT.themeMenuTypes[menuType];
87
- return entry ? entry.quickBarMaxItems : null;
88
- }
89
-
90
- // The strip's surface was hard-coded white on BOTH hosts and its items read the
91
- // sidebar's tokens. `footer` gives the quick bar tokens of its own.
92
- //
93
- // Every colour falls back to the sidebar's, so a workspace that never opens the
94
- // panel keeps exactly the appearance it has today; only an explicit `footer`
95
- // value moves anything.
73
+ // REQ-NAV-LOCAL: the app's BOTTOM EDGE — the tokens a page's own bottom
74
+ // navigation bar is drawn from. Every colour falls back to the sidebar's, so a
75
+ // workspace that never opens the panel keeps the appearance it has.
96
76
  //
97
- // sc-7695: this block is the BOTTOM EDGE's, not the quick bar's alone. A page's
98
- // own bottom bar (REQ-NAV-LOCAL) is the same strip worn by one page and reads
99
- // exactly these values — the Design panel that edits them is named for that
100
- // bar, since the quick bar is on its way out (sc-7753) and the look outlives
101
- // it. No key is the bar's alone: two strips, one look.
77
+ // sc-7753: the global quick bar these tokens were introduced for is retired;
78
+ // the bar that reads them now is the page's own (`local_nav = BOTTOM_BAR`).
102
79
  function resolveFooterTokens(theme) {
103
80
  const config = isPlainObject(theme) ? theme : {};
104
81
  const footer = isPlainObject(config.footer) ? config.footer : {};
@@ -113,8 +90,8 @@ function resolveFooterTokens(theme) {
113
90
  // colour resolves to the brand primary, which the caller supplies.
114
91
  activeColor: pick("activeColor"),
115
92
  // REQ-THEME-LOOK: the divider's COLOUR is its switch. Unset draws no line at
116
- // all — which the native quick bar did not honour before this, ruling a
117
- // hairline across a tinted strip that had asked for none.
93
+ // all — which the native strip did not honour before this, ruling a
94
+ // hairline across a tinted bar that had asked for none.
118
95
  borderColor,
119
96
  borderWidth: borderColor ? borderWidthOr(footer.borderWidth, sidebar.borderWidth) : null,
120
97
  activeStyle: activeStyle === "accent" ? "accent" : "filled",
@@ -362,4 +339,4 @@ function borderWidthOr(...values) {
362
339
  return 1;
363
340
  }
364
341
 
365
- module.exports = { normaliseNavigation, menuItemCap, quickBarCap, resolveFooterTokens, resolveSidebarTokens, resolveTopBarTokens };
342
+ module.exports = { normaliseNavigation, menuItemCap, resolveFooterTokens, resolveSidebarTokens, resolveTopBarTokens };
@@ -51,7 +51,7 @@ export function normaliseNavigation(navigation) {
51
51
 
52
52
  /**
53
53
  * How many MENU pages the chrome may draw at once, or `null` for no cap. The
54
- * sidebar has none. Contrast `quickBarCap` below.
54
+ * sidebar has none.
55
55
  *
56
56
  * @param {string} menuType — a resolved menu type.
57
57
  * @returns {number|null}
@@ -61,35 +61,12 @@ export function menuItemCap(menuType) {
61
61
  return entry ? entry.maxItems : null;
62
62
  }
63
63
 
64
- /**
65
- * How many pages the SECONDARY mobile quick bar draws, or `null` where that
66
- * shape draws none. Only the `sidebar` shape has one.
67
- *
68
- * This cap MAY drop a page, and that is the difference from `menuItemCap`: the
69
- * rail and the drawer still list every menu page, so the bar is a shortcut
70
- * rather than the menu. It lived as a constant on one host and a literal on the
71
- * other before this, which is exactly how the two would have drifted.
72
- *
73
- * @param {string} menuType — a resolved menu type.
74
- * @returns {number|null}
75
- */
76
- export function quickBarCap(menuType) {
77
- const entry = CONTRACT.themeMenuTypes[menuType];
78
- return entry ? entry.quickBarMaxItems : null;
79
- }
80
-
81
- // The strip's surface was hard-coded white on BOTH hosts and its items read the
82
- // sidebar's tokens. `footer` gives the quick bar tokens of its own.
83
- //
84
- // Every colour falls back to the sidebar's, so a workspace that never opens the
85
- // panel keeps exactly the appearance it has today; only an explicit `footer`
86
- // value moves anything.
64
+ // REQ-NAV-LOCAL: the app's BOTTOM EDGE — the tokens a page's own bottom
65
+ // navigation bar is drawn from. Every colour falls back to the sidebar's, so a
66
+ // workspace that never opens the panel keeps the appearance it has.
87
67
  //
88
- // sc-7695: this block is the BOTTOM EDGE's, not the quick bar's alone. A page's
89
- // own bottom bar (REQ-NAV-LOCAL) is the same strip worn by one page and reads
90
- // exactly these values — the Design panel that edits them is named for that
91
- // bar, since the quick bar is on its way out (sc-7753) and the look outlives
92
- // it. No key is the bar's alone: two strips, one look.
68
+ // sc-7753: the global quick bar these tokens were introduced for is retired;
69
+ // the bar that reads them now is the page's own (`local_nav = BOTTOM_BAR`).
93
70
  export function resolveFooterTokens(theme) {
94
71
  const config = isPlainObject(theme) ? theme : {};
95
72
  const footer = isPlainObject(config.footer) ? config.footer : {};
@@ -104,8 +81,8 @@ export function resolveFooterTokens(theme) {
104
81
  // colour resolves to the brand primary, which the caller supplies.
105
82
  activeColor: pick("activeColor"),
106
83
  // REQ-THEME-LOOK: the divider's COLOUR is its switch. Unset draws no line at
107
- // all — which the native quick bar did not honour before this, ruling a
108
- // hairline across a tinted strip that had asked for none.
84
+ // all — which the native strip did not honour before this, ruling a
85
+ // hairline across a tinted bar that had asked for none.
109
86
  borderColor,
110
87
  borderWidth: borderColor ? borderWidthOr(footer.borderWidth, sidebar.borderWidth) : null,
111
88
  activeStyle: activeStyle === "accent" ? "accent" : "filled",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.142.1",
3
+ "version": "0.144.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",
@@ -49,7 +49,7 @@
49
49
  ],
50
50
  "scripts": {
51
51
  "build": "node scripts/build.js",
52
- "test": "node --test src/__tests__/contract.test.js src/__tests__/theme-token-refs.test.js src/__tests__/vetted-imports-audit.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-flex-basis-percent.test.js src/__tests__/linter-hardcoded-design.test.js src/__tests__/linter-editability.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/flatten-entry.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/overlay-tokens.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js src/__tests__/hooks-camera.test.js src/__tests__/hooks-barcode.test.js src/__tests__/hooks-image-editor.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js src/__tests__/linter-html-in-content.test.js src/__tests__/markdown.test.js src/__tests__/markdown-edit.test.js src/__tests__/richtext-tokens.test.js src/__tests__/style-group.test.js"
52
+ "test": "node --test src/__tests__/contract.test.js src/__tests__/theme-token-refs.test.js src/__tests__/vetted-imports-audit.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-screen-title.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-flex-basis-percent.test.js src/__tests__/linter-hardcoded-design.test.js src/__tests__/linter-editability.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/flatten-entry.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/overlay-tokens.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js src/__tests__/hooks-camera.test.js src/__tests__/hooks-barcode.test.js src/__tests__/hooks-image-editor.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js src/__tests__/linter-html-in-content.test.js src/__tests__/markdown.test.js src/__tests__/markdown-edit.test.js src/__tests__/richtext-tokens.test.js src/__tests__/style-group.test.js"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=18"