@colixsystems/widget-sdk 0.109.0 → 0.110.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
@@ -69,7 +69,17 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
69
69
 
70
70
  ## Status
71
71
 
72
- `v0.108.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**.
72
+ `v0.110.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**.
73
+
74
+ ### What's new in 0.110.0 (contract 1.84.0)
75
+
76
+ **`resolveSidebarTokens` and `resolveTopBarTokens` — the rail's and the app bar's tokens, resolved once for both hosts (sc-6289).** The footer strip got a shared resolver in 1.71.0; the two chrome parts beside it did not, so their defaults lived inline in the web Player and again in the compiler — and had drifted. An unset `sidebar.backgroundColor` painted the app background in the Expo export where the Player painted it white, so a dark app shipped a dark drawer beside a white rail; an unset `topBar.textColor` painted the app name slate in the export where the Player has always used the brand colour.
77
+
78
+ Both resolvers now own their defaults, which are what the web Player renders, so the export follows the appearance the author approved in the Studio rather than the other way round. Unlike `resolveFooterTokens`, the colours are never `null` — a default that lives in the resolver cannot drift, and one that lived in each host already had. `borderColor` stays nullable: the colour is the divider's switch.
79
+
80
+ The top bar resolves **two** text colours. An unthemed bar paints its icons slate and its app name in the brand colour, and React Navigation's single `headerTintColor` cannot say both — so `tintColor` and `titleColor` are separate, and an authored `topBar.textColor` drives both. `show` is deliberately not among them: it depends on the menu type rather than the theme, and REQ-THEME-14 makes it a no-op on native.
81
+
82
+ Host-integration surface only — nothing a widget imports changed. `CONTRACT.version` → `1.84.0`.
73
83
 
74
84
  ### What's new in 0.108.0 (contract 1.82.0)
75
85
 
package/dist/contract.cjs CHANGED
@@ -3362,7 +3362,19 @@ const CONTRACT = deepFreeze({
3362
3362
  // additionally requires an `inheritAcl` RELATION column, which is what
3363
3363
  // makes the stream a strict subset of what `list()` already returns.
3364
3364
  // Backed by datastore-client 0.15.0. Omitting `scope` is unchanged.
3365
- version: "1.83.0",
3365
+ // 1.84.0: additive (sc-6289) -- `resolveSidebarTokens` and
3366
+ // `resolveTopBarTokens` (host exports), the rail's and the app bar's
3367
+ // tokens beside the footer's. Their defaults used to be written separately
3368
+ // in each host and had drifted: an unset rail took the app background in
3369
+ // the export where the Player painted it white, and an unset header
3370
+ // painted the app name slate where the Player used the brand colour. The
3371
+ // resolvers own the defaults, so a host cannot keep its own. The top bar
3372
+ // resolves `tintColor` and `titleColor` separately because the web bar has
3373
+ // always painted its icons slate and its name in the brand colour, which
3374
+ // one `headerTintColor` cannot express; an authored `topBar.textColor`
3375
+ // drives both. `show` is deliberately absent -- it depends on the menu
3376
+ // type, not the theme, and is a no-op on native.
3377
+ version: "1.84.0",
3366
3378
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3367
3379
  hooks: HOOKS,
3368
3380
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -3362,7 +3362,19 @@ const CONTRACT = deepFreeze({
3362
3362
  // additionally requires an `inheritAcl` RELATION column, which is what
3363
3363
  // makes the stream a strict subset of what `list()` already returns.
3364
3364
  // Backed by datastore-client 0.15.0. Omitting `scope` is unchanged.
3365
- version: "1.83.0",
3365
+ // 1.84.0: additive (sc-6289) -- `resolveSidebarTokens` and
3366
+ // `resolveTopBarTokens` (host exports), the rail's and the app bar's
3367
+ // tokens beside the footer's. Their defaults used to be written separately
3368
+ // in each host and had drifted: an unset rail took the app background in
3369
+ // the export where the Player painted it white, and an unset header
3370
+ // painted the app name slate where the Player used the brand colour. The
3371
+ // resolvers own the defaults, so a host cannot keep its own. The top bar
3372
+ // resolves `tintColor` and `titleColor` separately because the web bar has
3373
+ // always painted its icons slate and its name in the brand colour, which
3374
+ // one `headerTintColor` cannot express; an authored `topBar.textColor`
3375
+ // drives both. `show` is deliberately absent -- it depends on the menu
3376
+ // type, not the theme, and is a no-op on native.
3377
+ version: "1.84.0",
3366
3378
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3367
3379
  hooks: HOOKS,
3368
3380
  primitives: PRIMITIVES,
package/dist/host.d.ts CHANGED
@@ -194,6 +194,43 @@ export interface FooterTokens {
194
194
  */
195
195
  export function resolveFooterTokens(theme: unknown): FooterTokens;
196
196
 
197
+ /** The sidebar rail's resolved tokens. Unlike the footer's, the colours are
198
+ * never `null`: the default lives here so the two hosts cannot each keep a
199
+ * different one. `borderColor` is still nullable — no colour, no divider. */
200
+ export interface SidebarTokens {
201
+ backgroundColor: string;
202
+ textColor: string;
203
+ activeColor: string;
204
+ activeStyle: "filled" | "accent";
205
+ borderColor: string | null;
206
+ borderWidth: number | null;
207
+ }
208
+
209
+ /**
210
+ * Resolves the sidebar rail's tokens from a whole `theme_config`. Every default
211
+ * is what the web Player renders, so the export follows the Player rather than
212
+ * the other way round.
213
+ */
214
+ export function resolveSidebarTokens(theme: unknown): SidebarTokens;
215
+
216
+ /** The top app bar's resolved tokens. `tintColor` and `titleColor` are separate
217
+ * because an unthemed bar paints its icons slate and its app name in the brand
218
+ * colour; an authored `topBar.textColor` drives both. */
219
+ export interface TopBarTokens {
220
+ backgroundColor: string;
221
+ tintColor: string;
222
+ titleColor: string;
223
+ borderColor: string | null;
224
+ borderWidth: number | null;
225
+ }
226
+
227
+ /**
228
+ * Resolves the top app bar's tokens from a whole `theme_config`. `show` is not
229
+ * among them: it depends on the menu type, not the theme, and is a no-op on
230
+ * native.
231
+ */
232
+ export function resolveTopBarTokens(theme: unknown): TopBarTokens;
233
+
197
234
  /** A value a widget may persist: a scalar, or a flat array of them. */
198
235
  export type WidgetRouteScalar = string | number | boolean;
199
236
  export type WidgetRouteValue = WidgetRouteScalar | WidgetRouteScalar[];
package/dist/host.js CHANGED
@@ -50,11 +50,16 @@ export {
50
50
  // the sidebar's so an untouched workspace keeps the appearance it has. It
51
51
  // exists because the strip IS the menu under `bottom-tabs`, where the sidebar
52
52
  // panel is hidden and those tokens can no longer be set at all.
53
+ // REQ-THEME-14: `resolveSidebarTokens` and `resolveTopBarTokens` are the same
54
+ // idea for the rail and the app bar, whose defaults used to live inline in each
55
+ // host and had already drifted apart.
53
56
  export {
54
57
  normaliseNavigation,
55
58
  menuItemCap,
56
59
  quickBarCap,
57
60
  resolveFooterTokens,
61
+ resolveSidebarTokens,
62
+ resolveTopBarTokens,
58
63
  } from "./navigation.js";
59
64
 
60
65
  // sc-5717: the codec behind `useWidgetRoute()` — how a widget's own internal
@@ -25,7 +25,7 @@
25
25
  // the SAME literal, so what a planner may persist and what a host draws cannot
26
26
  // diverge.
27
27
 
28
- const { CONTRACT } = require("./contract.cjs");
28
+ const { CONTRACT, isHexColor } = require("./contract.cjs");
29
29
 
30
30
  // Absent or unknown resolves here, so every app authored before menu types
31
31
  // existed renders and compiles byte-identically.
@@ -114,6 +114,98 @@ function resolveFooterTokens(theme) {
114
114
  };
115
115
  }
116
116
 
117
+ // REQ-THEME-14 — the rail's and the app bar's tokens, resolved once for both
118
+ // hosts.
119
+ //
120
+ // These return FULLY resolved values where `resolveFooterTokens` above returns
121
+ // raw-or-null, and the difference is deliberate. The footer's two hosts happen
122
+ // to agree on their separately-written defaults; the rail's and the bar's did
123
+ // not. An unset `sidebar.backgroundColor` painted #ffffff on web and the app
124
+ // background in the export, and an unset `topBar.textColor` painted the brand
125
+ // primary on web and slate-700 in the export. A default that lives in the
126
+ // resolver cannot drift; one that lives in each host already had. The footer
127
+ // wants this same shape next — the same fix, not yet applied.
128
+ //
129
+ // Every default is what the WEB Player renders today, so the Player does not
130
+ // move and the export converges onto it. That is the direction the footer was
131
+ // converged in too (compiler.service's footerBg defaults to the web's #ffffff).
132
+
133
+ const DEFAULT_PRIMARY_COLOR = "#3b82f6";
134
+ // The web rail's and app bar's `bg-white`.
135
+ const DEFAULT_CHROME_SURFACE = "#ffffff";
136
+ // The web nav item's `text-slate-600`.
137
+ const DEFAULT_CHROME_TEXT = "#475569";
138
+ // The web mobile header's icon colour, `text-slate-700`.
139
+ const DEFAULT_TOP_BAR_TINT = "#334155";
140
+
141
+ // The contract's own guard, which already admits the 8-digit form a look uses
142
+ // to float a translucent rail (#RRGGBBAA).
143
+ function hexOrNull(value) {
144
+ return isHexColor(value) ? value : null;
145
+ }
146
+
147
+ function hexOr(value, fallback) {
148
+ return hexOrNull(value) || fallback;
149
+ }
150
+
151
+ function brandPrimary(config) {
152
+ return hexOr(config.primaryColor, DEFAULT_PRIMARY_COLOR);
153
+ }
154
+
155
+ /**
156
+ * The sidebar rail's surface, labels, active item and opt-in divider.
157
+ *
158
+ * @param {unknown} theme — the whole `theme_config`; `primaryColor` is read
159
+ * because an unset active colour resolves to the brand.
160
+ * @returns {{ backgroundColor: string, textColor: string, activeColor: string,
161
+ * activeStyle: string, borderColor: string|null, borderWidth: number|null }}
162
+ */
163
+ function resolveSidebarTokens(theme) {
164
+ const config = isPlainObject(theme) ? theme : {};
165
+ const sidebar = isPlainObject(config.sidebar) ? config.sidebar : {};
166
+ const borderColor = hexOrNull(sidebar.borderColor);
167
+ return {
168
+ backgroundColor: hexOr(sidebar.backgroundColor, DEFAULT_CHROME_SURFACE),
169
+ textColor: hexOr(sidebar.textColor, DEFAULT_CHROME_TEXT),
170
+ activeColor: hexOr(sidebar.activeColor, brandPrimary(config)),
171
+ activeStyle: sidebar.activeStyle === "accent" ? "accent" : "filled",
172
+ // REQ-THEME-LOOK: the colour is the switch — no colour, no line.
173
+ borderColor,
174
+ borderWidth: borderColor ? borderWidthOr(sidebar.borderWidth) : null,
175
+ };
176
+ }
177
+
178
+ /**
179
+ * The top app bar's surface, its two text colours and its opt-in divider.
180
+ *
181
+ * `tintColor` and `titleColor` are SEPARATE because the web bar has always
182
+ * painted them differently when the author names nothing: the hamburger and
183
+ * icons take slate, the app name takes the brand. React Navigation's single
184
+ * `headerTintColor` could not express that, which is why the export used to
185
+ * paint the name slate. One token, two resolved fields, both hosts agree.
186
+ *
187
+ * `show` is deliberately absent: it depends on the menu TYPE, not the theme,
188
+ * and REQ-THEME-14 makes it a no-op on native — so it is not a shared token.
189
+ *
190
+ * @param {unknown} theme — the whole `theme_config`.
191
+ * @returns {{ backgroundColor: string, tintColor: string, titleColor: string,
192
+ * borderColor: string|null, borderWidth: number|null }}
193
+ */
194
+ function resolveTopBarTokens(theme) {
195
+ const config = isPlainObject(theme) ? theme : {};
196
+ const topBar = isPlainObject(config.topBar) ? config.topBar : {};
197
+ const borderColor = hexOrNull(topBar.borderColor);
198
+ // An explicit colour drives BOTH slots; only the unset case splits.
199
+ const authored = hexOrNull(topBar.textColor);
200
+ return {
201
+ backgroundColor: hexOr(topBar.backgroundColor, DEFAULT_CHROME_SURFACE),
202
+ tintColor: authored || DEFAULT_TOP_BAR_TINT,
203
+ titleColor: authored || brandPrimary(config),
204
+ borderColor,
205
+ borderWidth: borderColor ? borderWidthOr(topBar.borderWidth) : null,
206
+ };
207
+ }
208
+
117
209
  function borderWidthOr(...values) {
118
210
  for (const value of values) {
119
211
  if (typeof value === "number" && Number.isFinite(value)) return value;
@@ -121,4 +213,4 @@ function borderWidthOr(...values) {
121
213
  return 1;
122
214
  }
123
215
 
124
- module.exports = { normaliseNavigation, menuItemCap, quickBarCap, resolveFooterTokens };
216
+ module.exports = { normaliseNavigation, menuItemCap, quickBarCap, resolveFooterTokens, resolveSidebarTokens, resolveTopBarTokens };
@@ -16,7 +16,7 @@
16
16
  // the SAME literal, so what a planner may persist and what a host draws cannot
17
17
  // diverge.
18
18
 
19
- import { CONTRACT } from "./contract.js";
19
+ import { CONTRACT, isHexColor } from "./contract.js";
20
20
 
21
21
  // Absent or unknown resolves here, so every app authored before menu types
22
22
  // existed renders and compiles byte-identically.
@@ -105,6 +105,98 @@ export function resolveFooterTokens(theme) {
105
105
  };
106
106
  }
107
107
 
108
+ // REQ-THEME-14 — the rail's and the app bar's tokens, resolved once for both
109
+ // hosts.
110
+ //
111
+ // These return FULLY resolved values where `resolveFooterTokens` above returns
112
+ // raw-or-null, and the difference is deliberate. The footer's two hosts happen
113
+ // to agree on their separately-written defaults; the rail's and the bar's did
114
+ // not. An unset `sidebar.backgroundColor` painted #ffffff on web and the app
115
+ // background in the export, and an unset `topBar.textColor` painted the brand
116
+ // primary on web and slate-700 in the export. A default that lives in the
117
+ // resolver cannot drift; one that lives in each host already had. The footer
118
+ // wants this same shape next — the same fix, not yet applied.
119
+ //
120
+ // Every default is what the WEB Player renders today, so the Player does not
121
+ // move and the export converges onto it. That is the direction the footer was
122
+ // converged in too (compiler.service's footerBg defaults to the web's #ffffff).
123
+
124
+ const DEFAULT_PRIMARY_COLOR = "#3b82f6";
125
+ // The web rail's and app bar's `bg-white`.
126
+ const DEFAULT_CHROME_SURFACE = "#ffffff";
127
+ // The web nav item's `text-slate-600`.
128
+ const DEFAULT_CHROME_TEXT = "#475569";
129
+ // The web mobile header's icon colour, `text-slate-700`.
130
+ const DEFAULT_TOP_BAR_TINT = "#334155";
131
+
132
+ // The contract's own guard, which already admits the 8-digit form a look uses
133
+ // to float a translucent rail (#RRGGBBAA).
134
+ function hexOrNull(value) {
135
+ return isHexColor(value) ? value : null;
136
+ }
137
+
138
+ function hexOr(value, fallback) {
139
+ return hexOrNull(value) || fallback;
140
+ }
141
+
142
+ function brandPrimary(config) {
143
+ return hexOr(config.primaryColor, DEFAULT_PRIMARY_COLOR);
144
+ }
145
+
146
+ /**
147
+ * The sidebar rail's surface, labels, active item and opt-in divider.
148
+ *
149
+ * @param {unknown} theme — the whole `theme_config`; `primaryColor` is read
150
+ * because an unset active colour resolves to the brand.
151
+ * @returns {{ backgroundColor: string, textColor: string, activeColor: string,
152
+ * activeStyle: string, borderColor: string|null, borderWidth: number|null }}
153
+ */
154
+ export function resolveSidebarTokens(theme) {
155
+ const config = isPlainObject(theme) ? theme : {};
156
+ const sidebar = isPlainObject(config.sidebar) ? config.sidebar : {};
157
+ const borderColor = hexOrNull(sidebar.borderColor);
158
+ return {
159
+ backgroundColor: hexOr(sidebar.backgroundColor, DEFAULT_CHROME_SURFACE),
160
+ textColor: hexOr(sidebar.textColor, DEFAULT_CHROME_TEXT),
161
+ activeColor: hexOr(sidebar.activeColor, brandPrimary(config)),
162
+ activeStyle: sidebar.activeStyle === "accent" ? "accent" : "filled",
163
+ // REQ-THEME-LOOK: the colour is the switch — no colour, no line.
164
+ borderColor,
165
+ borderWidth: borderColor ? borderWidthOr(sidebar.borderWidth) : null,
166
+ };
167
+ }
168
+
169
+ /**
170
+ * The top app bar's surface, its two text colours and its opt-in divider.
171
+ *
172
+ * `tintColor` and `titleColor` are SEPARATE because the web bar has always
173
+ * painted them differently when the author names nothing: the hamburger and
174
+ * icons take slate, the app name takes the brand. React Navigation's single
175
+ * `headerTintColor` could not express that, which is why the export used to
176
+ * paint the name slate. One token, two resolved fields, both hosts agree.
177
+ *
178
+ * `show` is deliberately absent: it depends on the menu TYPE, not the theme,
179
+ * and REQ-THEME-14 makes it a no-op on native — so it is not a shared token.
180
+ *
181
+ * @param {unknown} theme — the whole `theme_config`.
182
+ * @returns {{ backgroundColor: string, tintColor: string, titleColor: string,
183
+ * borderColor: string|null, borderWidth: number|null }}
184
+ */
185
+ export function resolveTopBarTokens(theme) {
186
+ const config = isPlainObject(theme) ? theme : {};
187
+ const topBar = isPlainObject(config.topBar) ? config.topBar : {};
188
+ const borderColor = hexOrNull(topBar.borderColor);
189
+ // An explicit colour drives BOTH slots; only the unset case splits.
190
+ const authored = hexOrNull(topBar.textColor);
191
+ return {
192
+ backgroundColor: hexOr(topBar.backgroundColor, DEFAULT_CHROME_SURFACE),
193
+ tintColor: authored || DEFAULT_TOP_BAR_TINT,
194
+ titleColor: authored || brandPrimary(config),
195
+ borderColor,
196
+ borderWidth: borderColor ? borderWidthOr(topBar.borderWidth) : null,
197
+ };
198
+ }
199
+
108
200
  function borderWidthOr(...values) {
109
201
  for (const value of values) {
110
202
  if (typeof value === "number" && Number.isFinite(value)) return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.109.0",
3
+ "version": "0.110.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",