@colixsystems/widget-sdk 0.167.0 → 0.168.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
@@ -93,7 +93,7 @@ Render an **assistant** message with `<RichText value={m.content} stripLegacyHtm
93
93
  A binding may now carry an opacity: `"theme:colors.primary/20"` is the primary brand colour at 20%.
94
94
 
95
95
  - **`parseThemeColorTokenRef(value)` / `formatThemeColorTokenRef(path, percent)`** publish the grammar beside the existing `isThemeColorTokenRef` predicate. The BARE form inherits whatever opacity the token itself carries — a workspace may author a translucent brand colour — so `/100` is a real value, not a redundant spelling: it is how "opaque, whatever the brand does" is said. One opacity has one spelling otherwise (no leading zeros, nothing above 100).
96
- - **`withHexAlpha(hex, percent)` / `hexAlphaPercent(hex)`** are the percent ↔ alpha-byte conversion the Studio's opacity control and both hosts' resolver share, so the 20% a panel reads back is the 20% a render paints.
96
+ - **`withHexAlpha(hex, percent)` / `hexAlphaPercent(hex)`** are the percent ↔ alpha-byte conversion, so the 20% a panel reads back is the 20% a render paints. The Studio's `frontend/src/utils/color.js` keeps its own copy of this maths rather than importing it — that module is reached by a `node --test` suite, and the SDK is a Vite alias with no node resolution — and the two are pinned equal by the SDK's suite.
97
97
  - **An authored opacity replaces the token's.** With none authored the token is handed back verbatim, including a token that is itself translucent — reading the bare form as 100 would quietly paint a see-through brand colour solid.
98
98
  - **A token resolving to a non-hex colour stays opaque** — alpha cannot be applied without rewriting the author's notation — and the Studio disables the opacity control for that case rather than lying about it.
99
99
  - **Nothing changes for a widget author.** The host still hands your code an ordinary colour string; it is simply `#RRGGBBAA` when an opacity was authored.
package/dist/contract.cjs CHANGED
@@ -4539,10 +4539,10 @@ const CONTRACT = deepFreeze({
4539
4539
  // field bound, so "our brand colour at 20%" forced the author back to a
4540
4540
  // frozen hex, which then stopped following a rebrand — the exact thing
4541
4541
  // binding exists to prevent. `parseThemeColorTokenRef` /
4542
- // `formatThemeColorTokenRef` publish the grammar and
4542
+ // `formatThemeColorTokenRef` publish the grammar, and
4543
4543
  // `withHexAlpha` / `hexAlphaPercent` the percent-to-byte conversion the
4544
- // Studio's control and both hosts' resolver share, so 20% authored is 20%
4545
- // painted. The BARE form inherits the token's own opacity (a brand colour
4544
+ // Studio's control and both hosts' resolver answer alike, so 20% authored
4545
+ // is 20% painted. The BARE form inherits the token's own opacity (a brand colour
4546
4546
  // may be authored translucent), which makes `/100` a real value rather
4547
4547
  // than a redundant spelling: it is how "opaque, whatever the brand does"
4548
4548
  // is said. Additive: an unsuffixed binding parses and resolves exactly as
package/dist/contract.js CHANGED
@@ -4535,10 +4535,10 @@ const CONTRACT = deepFreeze({
4535
4535
  // field bound, so "our brand colour at 20%" forced the author back to a
4536
4536
  // frozen hex, which then stopped following a rebrand — the exact thing
4537
4537
  // binding exists to prevent. `parseThemeColorTokenRef` /
4538
- // `formatThemeColorTokenRef` publish the grammar and
4538
+ // `formatThemeColorTokenRef` publish the grammar, and
4539
4539
  // `withHexAlpha` / `hexAlphaPercent` the percent-to-byte conversion the
4540
- // Studio's control and both hosts' resolver share, so 20% authored is 20%
4541
- // painted. The BARE form inherits the token's own opacity (a brand colour
4540
+ // Studio's control and both hosts' resolver answer alike, so 20% authored
4541
+ // is 20% painted. The BARE form inherits the token's own opacity (a brand colour
4542
4542
  // may be authored translucent), which makes `/100` a real value rather
4543
4543
  // than a redundant spelling: it is how "opaque, whatever the brand does"
4544
4544
  // is said. Additive: an unsuffixed binding parses and resolves exactly as
package/dist/host.js CHANGED
@@ -74,6 +74,9 @@ export {
74
74
  // SIDEBAR local menu hangs in the rail and the drawer. The compiler asks it
75
75
  // once per screen at build time and the Player once per render, so the two
76
76
  // chromes derive the same tree from the same code.
77
+ // REQ-NAV-LOCAL (sc-8901): `resolveLocalNavModel` answers WHICH model a page
78
+ // wears in one device view, so the same menu can be a bottom bar on a phone and
79
+ // unfold in the rail on a desktop.
77
80
  export {
78
81
  normaliseNavigation,
79
82
  menuItemCap,
@@ -94,6 +97,8 @@ export {
94
97
  DEFAULT_TOP_STRIPE_HEIGHT_PX,
95
98
  DEFAULT_TOP_STRIPE_PADDING_X_PX,
96
99
  resolveSidebarUnfold,
100
+ resolveLocalNavModel,
101
+ drawsLocalNavAnywhere,
97
102
  } from "./navigation.js";
98
103
 
99
104
  // sc-5717: the codec behind `useWidgetRoute()` — how a widget's own internal
@@ -675,6 +675,64 @@ function resolveSidebarUnfold(input) {
675
675
  return { under: section.key, items: marked, standalone: true };
676
676
  }
677
677
 
678
+ /**
679
+ * REQ-NAV-LOCAL (sc-8901) — which local-navigation model a page wears in ONE
680
+ * device view, so the same saved menu can be a bottom bar on a phone and unfold
681
+ * in the rail on a desktop.
682
+ *
683
+ * `local_nav` is the MOBILE value and the base of the cascade: a page authored
684
+ * before the wider views existed leaves both of them unset and therefore
685
+ * renders identically at every width. The cascade is REQ-LAY-07's — the one the
686
+ * layout grid and every responsive widget prop already use — because a page's
687
+ * chrome and its content must not disagree about what a tablet is.
688
+ *
689
+ * The vocabulary stays the API's: any non-empty string is a model and the
690
+ * host's own switch decides what it can draw, so a kind a newer backend allows
691
+ * cascades correctly here instead of being flattened to `NONE` on the way.
692
+ *
693
+ * @param {object|null|undefined} page — a page row carrying `local_nav` and,
694
+ * optionally, `local_nav_tablet` / `local_nav_desktop`.
695
+ * @param {"mobile"|"tablet"|"desktop"} view — the device view being drawn.
696
+ * @returns {string} the model for that view; `NONE` when the page sets none.
697
+ */
698
+ function resolveLocalNavModel(page, view) {
699
+ const at = (key) => {
700
+ const value = isPlainObject(page) ? page[key] : null;
701
+ return typeof value === "string" && value ? value : null;
702
+ };
703
+ const mobile = at("local_nav") || "NONE";
704
+ if (view === "desktop") {
705
+ return at("local_nav_desktop") || at("local_nav_tablet") || mobile;
706
+ }
707
+ if (view === "tablet") return at("local_nav_tablet") || mobile;
708
+ return mobile;
709
+ }
710
+
711
+ // sc-8901: the three device views a local bar can differ across — REQ-LAY-07's
712
+ // own list, so a page's chrome and its content classify a width alike. Ordered
713
+ // narrowest first, which is also the cascade's direction.
714
+ const LOCAL_NAV_VIEWS = ["mobile", "tablet", "desktop"];
715
+
716
+ /**
717
+ * REQ-NAV-LOCAL (sc-8901) — does this page draw a local bar in ANY device view?
718
+ *
719
+ * The saved-menu reference is shared by all three views, so it is required — and
720
+ * resolved onto the wire — as soon as one of them draws. Asking
721
+ * `local_nav !== "NONE"` alone would miss a page that carries a bar only on the
722
+ * wider views.
723
+ *
724
+ * Beside the cascade it is derived from, so the API rule, the export's screen
725
+ * bake and Copy Workspace cannot disagree about which pages have a bar.
726
+ *
727
+ * @param {object|null|undefined} page — a page row.
728
+ * @returns {boolean}
729
+ */
730
+ function drawsLocalNavAnywhere(page) {
731
+ return LOCAL_NAV_VIEWS.some(
732
+ (view) => resolveLocalNavModel(page, view) !== "NONE",
733
+ );
734
+ }
735
+
678
736
  // REQ-THEME-LOOK: the divider's width in px, bounded HERE so both hosts inherit
679
737
  // one range. The export clamped it on its way into the StyleSheet
680
738
  // (`coerceBorderWidth(...) ?? 1`) where the Player passed any finite number
@@ -711,4 +769,4 @@ function borderWidthOr(...values) {
711
769
  return BORDER_WIDTH_MIN;
712
770
  }
713
771
 
714
- 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 };
772
+ 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 };
@@ -666,6 +666,64 @@ export function resolveSidebarUnfold(input) {
666
666
  return { under: section.key, items: marked, standalone: true };
667
667
  }
668
668
 
669
+ /**
670
+ * REQ-NAV-LOCAL (sc-8901) — which local-navigation model a page wears in ONE
671
+ * device view, so the same saved menu can be a bottom bar on a phone and unfold
672
+ * in the rail on a desktop.
673
+ *
674
+ * `local_nav` is the MOBILE value and the base of the cascade: a page authored
675
+ * before the wider views existed leaves both of them unset and therefore
676
+ * renders identically at every width. The cascade is REQ-LAY-07's — the one the
677
+ * layout grid and every responsive widget prop already use — because a page's
678
+ * chrome and its content must not disagree about what a tablet is.
679
+ *
680
+ * The vocabulary stays the API's: any non-empty string is a model and the
681
+ * host's own switch decides what it can draw, so a kind a newer backend allows
682
+ * cascades correctly here instead of being flattened to `NONE` on the way.
683
+ *
684
+ * @param {object|null|undefined} page — a page row carrying `local_nav` and,
685
+ * optionally, `local_nav_tablet` / `local_nav_desktop`.
686
+ * @param {"mobile"|"tablet"|"desktop"} view — the device view being drawn.
687
+ * @returns {string} the model for that view; `NONE` when the page sets none.
688
+ */
689
+ export function resolveLocalNavModel(page, view) {
690
+ const at = (key) => {
691
+ const value = isPlainObject(page) ? page[key] : null;
692
+ return typeof value === "string" && value ? value : null;
693
+ };
694
+ const mobile = at("local_nav") || "NONE";
695
+ if (view === "desktop") {
696
+ return at("local_nav_desktop") || at("local_nav_tablet") || mobile;
697
+ }
698
+ if (view === "tablet") return at("local_nav_tablet") || mobile;
699
+ return mobile;
700
+ }
701
+
702
+ // sc-8901: the three device views a local bar can differ across — REQ-LAY-07's
703
+ // own list, so a page's chrome and its content classify a width alike. Ordered
704
+ // narrowest first, which is also the cascade's direction.
705
+ const LOCAL_NAV_VIEWS = ["mobile", "tablet", "desktop"];
706
+
707
+ /**
708
+ * REQ-NAV-LOCAL (sc-8901) — does this page draw a local bar in ANY device view?
709
+ *
710
+ * The saved-menu reference is shared by all three views, so it is required — and
711
+ * resolved onto the wire — as soon as one of them draws. Asking
712
+ * `local_nav !== "NONE"` alone would miss a page that carries a bar only on the
713
+ * wider views.
714
+ *
715
+ * Beside the cascade it is derived from, so the API rule, the export's screen
716
+ * bake and Copy Workspace cannot disagree about which pages have a bar.
717
+ *
718
+ * @param {object|null|undefined} page — a page row.
719
+ * @returns {boolean}
720
+ */
721
+ export function drawsLocalNavAnywhere(page) {
722
+ return LOCAL_NAV_VIEWS.some(
723
+ (view) => resolveLocalNavModel(page, view) !== "NONE",
724
+ );
725
+ }
726
+
669
727
  // REQ-THEME-LOOK: the divider's width in px, bounded HERE so both hosts inherit
670
728
  // one range. The export clamped it on its way into the StyleSheet
671
729
  // (`coerceBorderWidth(...) ?? 1`) where the Player passed any finite number
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.167.0",
3
+ "version": "0.168.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",