@colixsystems/widget-sdk 0.231.0 → 0.233.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
@@ -84,7 +84,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
84
84
 
85
85
  ## Status
86
86
 
87
- `v0.231.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
87
+ `v0.233.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.233.0 (contract 1.188.0)
90
+
91
+ **A floating bottom navigation bar is frosted glass by default (host side only).** `resolveFooterTokens` from `@colixsystems/widget-sdk/host` now also resolves `glass`, true for a floating bar unless the theme's `footer.glass` is `false` (a docked bar is never frosted, as nothing scrolls behind it), and `glassTint`, the 8-digit hex surface laid over the blur: the bar's `backgroundColor` at 72%, or its own alpha when it already has one. Nothing a widget imports changes.
92
+
93
+ ### What's new in 0.232.0 (contract 1.187.0)
94
+
95
+ **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.
88
96
 
89
97
  ### What's new in 0.231.0 (contract 1.186.0)
90
98
 
package/dist/contract.cjs CHANGED
@@ -6040,7 +6040,13 @@ const CONTRACT = deepFreeze({
6040
6040
  // `fitScreenFoot` host hint: a self-scrolling Fit to screen surface that
6041
6041
  // calls it runs under the bottom safe area on native and pads its
6042
6042
  // controls by it. Hosts without the hint read zeros.
6043
- version: "1.186.0",
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
+ // 1.188.0: additive (sc-11692) — `resolveFooterTokens` gains `glass` (a
6048
+ // floating bar is frosted unless `footer.glass` is false) and `glassTint`.
6049
+ version: "1.188.0",
6044
6050
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6045
6051
  hooks: HOOKS,
6046
6052
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -6036,7 +6036,13 @@ const CONTRACT = deepFreeze({
6036
6036
  // `fitScreenFoot` host hint: a self-scrolling Fit to screen surface that
6037
6037
  // calls it runs under the bottom safe area on native and pads its
6038
6038
  // controls by it. Hosts without the hint read zeros.
6039
- version: "1.186.0",
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
+ // 1.188.0: additive (sc-11692) — `resolveFooterTokens` gains `glass` (a
6044
+ // floating bar is frosted unless `footer.glass` is false) and `glassTint`.
6045
+ version: "1.188.0",
6040
6046
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6041
6047
  hooks: HOOKS,
6042
6048
  primitives: PRIMITIVES,
package/dist/host.d.ts CHANGED
@@ -265,8 +265,41 @@ 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-11692: a floating bar's frosted glass, on unless `glass: false`. */
269
+ glass: boolean;
270
+ /** The see-through surface laid over the blur (8-digit hex), or `null`
271
+ * when the bar is not frosted. */
272
+ glassTint: string | null;
273
+ /** sc-11678: a rounded bar inset from the screen's sides, the page showing
274
+ * around and beneath it, or the edge-to-edge strip. Unset is "floating". */
275
+ shape: "floating" | "docked";
276
+ /** A floating bar's corner radius, px (0–40). */
277
+ radius: number;
278
+ /** A tab's corner radius, px: a floating bar's curve one row inset in
279
+ * (min 6) for a filled tab; 6 for an accent tab or a docked bar. */
280
+ itemRadius: number;
281
+ /** A floating bar's distance from the screen's sides, px (0–48). */
282
+ sideMargin: number;
283
+ /** Space below the tabs, px (0–32), on top of the system's clearance. */
284
+ bottomSpacing: number;
268
285
  }
269
286
 
287
+ /** sc-11678: each bar measurement's upper bound and default, px (min 0). */
288
+ export const FOOTER_SIZES: Readonly<
289
+ Record<"radius" | "sideMargin" | "bottomSpacing", Readonly<{ max: number; fallback: number }>>
290
+ >;
291
+
292
+ /** sc-11678: one bar measurement clamped onto its `FOOTER_SIZES` range, or
293
+ * `undefined` when the value is not a finite number. */
294
+ export function clampFooterSize(
295
+ value: unknown,
296
+ key: "radius" | "sideMargin" | "bottomSpacing",
297
+ ): number | undefined;
298
+
299
+ /** sc-11678: the part of the iOS bottom inset a bar keeps clear (the home
300
+ * line's band), px. Android's inset is cleared whole. */
301
+ export const FOOTER_HOME_LINE_CLEARANCE_PX: number;
302
+
270
303
  /**
271
304
  * Resolves the footer strip's tokens from a whole `theme_config`. It reads
272
305
  * `theme.footer` and NOTHING else (sc-8033): the colours used to fall back to
package/dist/host.js CHANGED
@@ -92,6 +92,9 @@ export {
92
92
  normaliseNavigation,
93
93
  menuItemCap,
94
94
  resolveFooterTokens,
95
+ FOOTER_SIZES,
96
+ clampFooterSize,
97
+ FOOTER_HOME_LINE_CLEARANCE_PX,
95
98
  resolveSidebarTokens,
96
99
  resolveTopStripeLayout,
97
100
  resolveTopBarTokens,
@@ -83,10 +83,11 @@ 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 eight authorable keys are the eight controls the Design panel offers, one
87
- // for one: `backgroundColor`, `textColor`, `tabActiveBackgroundColor`,
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`, (sc-11678) `shape`, `radius`, `sideMargin`, `bottomSpacing`
90
+ // and (sc-11692) `glass`.
90
91
  function resolveFooterTokens(theme) {
91
92
  const config = isPlainObject(theme) ? theme : {};
92
93
  const footer = isPlainObject(config.footer) ? config.footer : {};
@@ -103,8 +104,17 @@ function resolveFooterTokens(theme) {
103
104
  // draw nothing.
104
105
  const borderWidth = borderColor ? footerBorderWidthOr(footer.borderWidth) : 0;
105
106
  const divider = borderWidth > 0;
107
+ const floats = footer.shape !== "docked";
108
+ const radius = footerSizeOr(footer, "radius");
109
+ const backgroundColor = hexOr(footer.backgroundColor, DEFAULT_FOOTER_SURFACE);
110
+ // sc-11692: frosted unless switched off; only a floating bar has a page
111
+ // behind it to frost.
112
+ const glass = floats && footer.glass !== false;
106
113
  return {
107
- backgroundColor: hexOr(footer.backgroundColor, DEFAULT_FOOTER_SURFACE),
114
+ backgroundColor,
115
+ glass,
116
+ // The surface over the blur: the bar's colour, see-through.
117
+ glassTint: glass ? translucentHex(backgroundColor) : null,
108
118
  textColor: hexOr(footer.textColor, DEFAULT_CHROME_TEXT),
109
119
  borderColor: divider ? borderColor : null,
110
120
  borderWidth: divider ? borderWidth : null,
@@ -128,9 +138,50 @@ function resolveFooterTokens(theme) {
128
138
  footer.tabActiveTextColor,
129
139
  accent ? brandPrimary(config) : FILLED_TAB_LABEL,
130
140
  ),
141
+ // sc-11678: unset floats, so every app gets the rounded, inset bar.
142
+ shape: floats ? "floating" : "docked",
143
+ radius,
144
+ // A filled tab follows a floating bar's curve one row inset (8px) in,
145
+ // never sharper than the docked 6px chip. An accent tab paints no surface,
146
+ // and a curved top edge would bend its marker into an arc.
147
+ itemRadius:
148
+ floats && !accent ? Math.max(DOCKED_TAB_RADIUS, radius - 8) : DOCKED_TAB_RADIUS,
149
+ sideMargin: footerSizeOr(footer, "sideMargin"),
150
+ bottomSpacing: footerSizeOr(footer, "bottomSpacing"),
131
151
  };
132
152
  }
133
153
 
154
+ // sc-11678: the bar's measurements, in px. `bottomSpacing` sits ABOVE what the
155
+ // system needs (see FOOTER_HOME_LINE_CLEARANCE_PX). Clamped, never dropped.
156
+ const FOOTER_SIZES = Object.freeze({
157
+ radius: Object.freeze({ max: 40, fallback: 28 }),
158
+ sideMargin: Object.freeze({ max: 48, fallback: 12 }),
159
+ bottomSpacing: Object.freeze({ max: 32, fallback: 8 }),
160
+ });
161
+ const DOCKED_TAB_RADIUS = 6;
162
+
163
+ // sc-11692: a colour that already carries its own alpha keeps it; an opaque
164
+ // one is laid over the blur at 72%.
165
+ const GLASS_TINT_ALPHA = "b8";
166
+ function translucentHex(hex) {
167
+ const digits = hex.slice(1);
168
+ if (digits.length === 8) return hex;
169
+ const six = digits.length === 3 ? digits.replace(/./g, (d) => d + d) : digits;
170
+ return `#${six}${GLASS_TINT_ALPHA}`;
171
+ }
172
+ function clampFooterSize(value, key) {
173
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
174
+ return Math.min(Math.max(Math.round(value), 0), FOOTER_SIZES[key].max);
175
+ }
176
+ function footerSizeOr(footer, key) {
177
+ return clampFooterSize(footer[key], key) ?? FOOTER_SIZES[key].fallback;
178
+ }
179
+
180
+ // sc-11678: of the iOS bottom inset, only the home line's band is kept clear;
181
+ // a bar may sit in the rest of it, as Apple's and most apps' tab bars do.
182
+ // Android's inset is its navigation bar, so there the whole inset is cleared.
183
+ const FOOTER_HOME_LINE_CLEARANCE_PX = 14;
184
+
134
185
  // REQ-THEME-14 — the rail's and the app bar's tokens, resolved once for both
135
186
  // hosts.
136
187
  //
@@ -844,4 +895,4 @@ function borderWidthOr(...values) {
844
895
  return BORDER_WIDTH_MIN;
845
896
  }
846
897
 
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 };
898
+ 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 };
@@ -74,10 +74,11 @@ 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 eight authorable keys are the eight controls the Design panel offers, one
78
- // for one: `backgroundColor`, `textColor`, `tabActiveBackgroundColor`,
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`, (sc-11678) `shape`, `radius`, `sideMargin`, `bottomSpacing`
81
+ // and (sc-11692) `glass`.
81
82
  export function resolveFooterTokens(theme) {
82
83
  const config = isPlainObject(theme) ? theme : {};
83
84
  const footer = isPlainObject(config.footer) ? config.footer : {};
@@ -94,8 +95,17 @@ export function resolveFooterTokens(theme) {
94
95
  // draw nothing.
95
96
  const borderWidth = borderColor ? footerBorderWidthOr(footer.borderWidth) : 0;
96
97
  const divider = borderWidth > 0;
98
+ const floats = footer.shape !== "docked";
99
+ const radius = footerSizeOr(footer, "radius");
100
+ const backgroundColor = hexOr(footer.backgroundColor, DEFAULT_FOOTER_SURFACE);
101
+ // sc-11692: frosted unless switched off; only a floating bar has a page
102
+ // behind it to frost.
103
+ const glass = floats && footer.glass !== false;
97
104
  return {
98
- backgroundColor: hexOr(footer.backgroundColor, DEFAULT_FOOTER_SURFACE),
105
+ backgroundColor,
106
+ glass,
107
+ // The surface over the blur: the bar's colour, see-through.
108
+ glassTint: glass ? translucentHex(backgroundColor) : null,
99
109
  textColor: hexOr(footer.textColor, DEFAULT_CHROME_TEXT),
100
110
  borderColor: divider ? borderColor : null,
101
111
  borderWidth: divider ? borderWidth : null,
@@ -119,9 +129,50 @@ export function resolveFooterTokens(theme) {
119
129
  footer.tabActiveTextColor,
120
130
  accent ? brandPrimary(config) : FILLED_TAB_LABEL,
121
131
  ),
132
+ // sc-11678: unset floats, so every app gets the rounded, inset bar.
133
+ shape: floats ? "floating" : "docked",
134
+ radius,
135
+ // A filled tab follows a floating bar's curve one row inset (8px) in,
136
+ // never sharper than the docked 6px chip. An accent tab paints no surface,
137
+ // and a curved top edge would bend its marker into an arc.
138
+ itemRadius:
139
+ floats && !accent ? Math.max(DOCKED_TAB_RADIUS, radius - 8) : DOCKED_TAB_RADIUS,
140
+ sideMargin: footerSizeOr(footer, "sideMargin"),
141
+ bottomSpacing: footerSizeOr(footer, "bottomSpacing"),
122
142
  };
123
143
  }
124
144
 
145
+ // sc-11678: the bar's measurements, in px. `bottomSpacing` sits ABOVE what the
146
+ // system needs (see FOOTER_HOME_LINE_CLEARANCE_PX). Clamped, never dropped.
147
+ export const FOOTER_SIZES = Object.freeze({
148
+ radius: Object.freeze({ max: 40, fallback: 28 }),
149
+ sideMargin: Object.freeze({ max: 48, fallback: 12 }),
150
+ bottomSpacing: Object.freeze({ max: 32, fallback: 8 }),
151
+ });
152
+ const DOCKED_TAB_RADIUS = 6;
153
+
154
+ // sc-11692: a colour that already carries its own alpha keeps it; an opaque
155
+ // one is laid over the blur at 72%.
156
+ const GLASS_TINT_ALPHA = "b8";
157
+ function translucentHex(hex) {
158
+ const digits = hex.slice(1);
159
+ if (digits.length === 8) return hex;
160
+ const six = digits.length === 3 ? digits.replace(/./g, (d) => d + d) : digits;
161
+ return `#${six}${GLASS_TINT_ALPHA}`;
162
+ }
163
+ export function clampFooterSize(value, key) {
164
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
165
+ return Math.min(Math.max(Math.round(value), 0), FOOTER_SIZES[key].max);
166
+ }
167
+ function footerSizeOr(footer, key) {
168
+ return clampFooterSize(footer[key], key) ?? FOOTER_SIZES[key].fallback;
169
+ }
170
+
171
+ // sc-11678: of the iOS bottom inset, only the home line's band is kept clear;
172
+ // a bar may sit in the rest of it, as Apple's and most apps' tab bars do.
173
+ // Android's inset is its navigation bar, so there the whole inset is cleared.
174
+ export const FOOTER_HOME_LINE_CLEARANCE_PX = 14;
175
+
125
176
  // REQ-THEME-14 — the rail's and the app bar's tokens, resolved once for both
126
177
  // hosts.
127
178
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.231.0",
3
+ "version": "0.233.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
@@ -6036,7 +6036,13 @@ const CONTRACT = deepFreeze({
6036
6036
  // `fitScreenFoot` host hint: a self-scrolling Fit to screen surface that
6037
6037
  // calls it runs under the bottom safe area on native and pads its
6038
6038
  // controls by it. Hosts without the hint read zeros.
6039
- version: "1.186.0",
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
+ // 1.188.0: additive (sc-11692) — `resolveFooterTokens` gains `glass` (a
6044
+ // floating bar is frosted unless `footer.glass` is false) and `glassTint`.
6045
+ version: "1.188.0",
6040
6046
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6041
6047
  hooks: HOOKS,
6042
6048
  primitives: PRIMITIVES,