@colixsystems/widget-sdk 0.70.0 → 0.71.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
@@ -54,7 +54,19 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
54
54
 
55
55
  ## Status
56
56
 
57
- `v0.70.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**.
57
+ `v0.71.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**.
58
+
59
+ ### What's new in 0.71.0
60
+
61
+ **A theme can set leading, tracking and case for text app-wide (sc-3857).** The `text` scope of `themeConfig.components` gains three tokens beside `color` and `fontSize`:
62
+
63
+ - **`lineHeight`** — leading as a MULTIPLE of the font size (0.8–3), so one app-wide value stays right at every size. Tight leading (1.0–1.15) is what makes a 32px+ headline read as a headline rather than as oversized body text.
64
+ - **`letterSpacing`** — tracking in pixels (−2 to 20). Wide positive tracking is what makes a small uppercase kicker read as a kicker.
65
+ - **`textTransform`** — `none` | `uppercase` | `capitalize`, published as `CONTRACT.themeComponentTextTransforms`.
66
+
67
+ Two new token value types back them: **`decimal`** (a clamped, 2-decimal number — `size` rounds, so it cannot carry a 1.05 multiplier) and **`textTransform`**. Each token binds to the identically named per-instance style field the target widgets read, so the author rule is unchanged: read `props.style` / `useWidgetStyle()`, and a per-instance value still wins over the app-wide token.
68
+
69
+ `CONTRACT.version` → `1.48.0`. Additive: no existing export changed signature, and a theme with none of the new tokens renders exactly as before.
58
70
 
59
71
  ### What's new in 0.70.0
60
72
 
package/dist/contract.cjs CHANGED
@@ -107,6 +107,14 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
107
107
  // cannot drift between what Mason may emit and what a host actually applies.
108
108
  const THEME_COMPONENT_SHADOWS = Object.freeze(["none", "sm", "md", "lg"]);
109
109
 
110
+ // sc-3857 — the `textTransform` token's closed enum, matching the per-instance
111
+ // Text/Label/Data Value style field and React Native's own vocabulary.
112
+ const THEME_COMPONENT_TEXT_TRANSFORMS = Object.freeze([
113
+ "none",
114
+ "uppercase",
115
+ "capitalize",
116
+ ]);
117
+
110
118
  // sc-3727 — the `gradient` token's value shape: two hex stops plus a CSS-degree
111
119
  // angle, the same grammar as `themeConfig.backgroundGradient` minus the radial
112
120
  // variant (a component fill projects through `<Gradient>`, which is linear-only
@@ -129,6 +137,16 @@ const CARD_SURFACE_FIELDS = Object.freeze({
129
137
  gradient: "cardGradient",
130
138
  });
131
139
 
140
+ // The text field names every text-bearing built-in reads
141
+ // (frontend/src/components/widgets/_shared/textStyle.js TYPOGRAPHY_STYLE_FIELDS).
142
+ const TEXT_TYPOGRAPHY_FIELDS = Object.freeze({
143
+ color: "color",
144
+ fontSize: "fontSize",
145
+ lineHeight: "lineHeight",
146
+ letterSpacing: "letterSpacing",
147
+ textTransform: "textTransform",
148
+ });
149
+
132
150
  // A form widget's submit button — the `button` scope reaches it through the
133
151
  // form's own submit* fields, so "make the buttons coral" does not skip forms.
134
152
  const FORM_SUBMIT_FIELDS = Object.freeze({
@@ -190,14 +208,16 @@ const THEME_COMPONENTS = Object.freeze({
190
208
  tokens: Object.freeze({
191
209
  color: Object.freeze({ type: "color", uiDefault: "colors.onSurface" }),
192
210
  fontSize: Object.freeze({ type: "size", min: 8, max: 96, uiDefault: "typography.sizes.md" }),
211
+ // sc-3857 — leading as a MULTIPLE of the font size, so one app-wide value
212
+ // is right at every size; `size` cannot carry it because it rounds.
213
+ lineHeight: Object.freeze({ type: "decimal", min: 0.8, max: 3, step: 0.05 }),
214
+ letterSpacing: Object.freeze({ type: "decimal", min: -2, max: 20, step: 0.1 }),
215
+ textTransform: Object.freeze({ type: "textTransform" }),
193
216
  }),
194
217
  targets: Object.freeze({
195
- "appstudio.text": Object.freeze({ color: "color", fontSize: "fontSize" }),
196
- "appstudio.label": Object.freeze({ color: "color", fontSize: "fontSize" }),
197
- "appstudio.data-value": Object.freeze({
198
- color: "color",
199
- fontSize: "fontSize",
200
- }),
218
+ "appstudio.text": TEXT_TYPOGRAPHY_FIELDS,
219
+ "appstudio.label": TEXT_TYPOGRAPHY_FIELDS,
220
+ "appstudio.data-value": TEXT_TYPOGRAPHY_FIELDS,
201
221
  }),
202
222
  }),
203
223
  });
@@ -2215,7 +2235,17 @@ const CONTRACT = deepFreeze({
2215
2235
  // null, which is how one button stays flat while the rest are gradiented.
2216
2236
  // Additive: no export changed signature and a theme with no gradient token
2217
2237
  // renders exactly as before.
2218
- version: "1.47.0",
2238
+ // 1.48.0: additive (sc-3857) — leading, tracking and case on the `text` scope.
2239
+ // `lineHeight` (a MULTIPLE of the font size), `letterSpacing` (pixels) and
2240
+ // `textTransform` join the existing `color` / `fontSize` tokens and bind to
2241
+ // the identically named per-instance style fields Text, Label and Data Value
2242
+ // now read, so a theme can set app-wide typography and an author still
2243
+ // overrides it per instance. Two new token value types back them: `decimal`
2244
+ // (a clamped 2-decimal number — `size` rounds, so it cannot carry a 1.05
2245
+ // multiplier) and `textTransform`, whose closed enum is published as
2246
+ // `themeComponentTextTransforms`. Additive: no export changed signature and
2247
+ // a theme with none of the new tokens renders exactly as before.
2248
+ version: "1.48.0",
2219
2249
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
2220
2250
  hooks: HOOKS,
2221
2251
  primitives: PRIMITIVES,
@@ -2228,6 +2258,7 @@ const CONTRACT = deepFreeze({
2228
2258
  themeTokens: DEFAULT_THEME_TOKENS,
2229
2259
  themeComponents: THEME_COMPONENTS,
2230
2260
  themeComponentShadows: THEME_COMPONENT_SHADOWS,
2261
+ themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
2231
2262
  themeComponentGradient: THEME_COMPONENT_GRADIENT,
2232
2263
  widgetContextShape: WIDGET_CONTEXT_SHAPE,
2233
2264
  bundleExportContract: BUNDLE_EXPORT_CONTRACT,
package/dist/contract.js CHANGED
@@ -107,6 +107,14 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
107
107
  // cannot drift between what Mason may emit and what a host actually applies.
108
108
  const THEME_COMPONENT_SHADOWS = Object.freeze(["none", "sm", "md", "lg"]);
109
109
 
110
+ // sc-3857 — the `textTransform` token's closed enum, matching the per-instance
111
+ // Text/Label/Data Value style field and React Native's own vocabulary.
112
+ const THEME_COMPONENT_TEXT_TRANSFORMS = Object.freeze([
113
+ "none",
114
+ "uppercase",
115
+ "capitalize",
116
+ ]);
117
+
110
118
  // sc-3727 — the `gradient` token's value shape: two hex stops plus a CSS-degree
111
119
  // angle, the same grammar as `themeConfig.backgroundGradient` minus the radial
112
120
  // variant (a component fill projects through `<Gradient>`, which is linear-only
@@ -129,6 +137,16 @@ const CARD_SURFACE_FIELDS = Object.freeze({
129
137
  gradient: "cardGradient",
130
138
  });
131
139
 
140
+ // The text field names every text-bearing built-in reads
141
+ // (frontend/src/components/widgets/_shared/textStyle.js TYPOGRAPHY_STYLE_FIELDS).
142
+ const TEXT_TYPOGRAPHY_FIELDS = Object.freeze({
143
+ color: "color",
144
+ fontSize: "fontSize",
145
+ lineHeight: "lineHeight",
146
+ letterSpacing: "letterSpacing",
147
+ textTransform: "textTransform",
148
+ });
149
+
132
150
  // A form widget's submit button — the `button` scope reaches it through the
133
151
  // form's own submit* fields, so "make the buttons coral" does not skip forms.
134
152
  const FORM_SUBMIT_FIELDS = Object.freeze({
@@ -190,14 +208,16 @@ const THEME_COMPONENTS = Object.freeze({
190
208
  tokens: Object.freeze({
191
209
  color: Object.freeze({ type: "color", uiDefault: "colors.onSurface" }),
192
210
  fontSize: Object.freeze({ type: "size", min: 8, max: 96, uiDefault: "typography.sizes.md" }),
211
+ // sc-3857 — leading as a MULTIPLE of the font size, so one app-wide value
212
+ // is right at every size; `size` cannot carry it because it rounds.
213
+ lineHeight: Object.freeze({ type: "decimal", min: 0.8, max: 3, step: 0.05 }),
214
+ letterSpacing: Object.freeze({ type: "decimal", min: -2, max: 20, step: 0.1 }),
215
+ textTransform: Object.freeze({ type: "textTransform" }),
193
216
  }),
194
217
  targets: Object.freeze({
195
- "appstudio.text": Object.freeze({ color: "color", fontSize: "fontSize" }),
196
- "appstudio.label": Object.freeze({ color: "color", fontSize: "fontSize" }),
197
- "appstudio.data-value": Object.freeze({
198
- color: "color",
199
- fontSize: "fontSize",
200
- }),
218
+ "appstudio.text": TEXT_TYPOGRAPHY_FIELDS,
219
+ "appstudio.label": TEXT_TYPOGRAPHY_FIELDS,
220
+ "appstudio.data-value": TEXT_TYPOGRAPHY_FIELDS,
201
221
  }),
202
222
  }),
203
223
  });
@@ -2215,7 +2235,17 @@ const CONTRACT = deepFreeze({
2215
2235
  // null, which is how one button stays flat while the rest are gradiented.
2216
2236
  // Additive: no export changed signature and a theme with no gradient token
2217
2237
  // renders exactly as before.
2218
- version: "1.47.0",
2238
+ // 1.48.0: additive (sc-3857) — leading, tracking and case on the `text` scope.
2239
+ // `lineHeight` (a MULTIPLE of the font size), `letterSpacing` (pixels) and
2240
+ // `textTransform` join the existing `color` / `fontSize` tokens and bind to
2241
+ // the identically named per-instance style fields Text, Label and Data Value
2242
+ // now read, so a theme can set app-wide typography and an author still
2243
+ // overrides it per instance. Two new token value types back them: `decimal`
2244
+ // (a clamped 2-decimal number — `size` rounds, so it cannot carry a 1.05
2245
+ // multiplier) and `textTransform`, whose closed enum is published as
2246
+ // `themeComponentTextTransforms`. Additive: no export changed signature and
2247
+ // a theme with none of the new tokens renders exactly as before.
2248
+ version: "1.48.0",
2219
2249
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
2220
2250
  hooks: HOOKS,
2221
2251
  primitives: PRIMITIVES,
@@ -2228,6 +2258,7 @@ const CONTRACT = deepFreeze({
2228
2258
  themeTokens: DEFAULT_THEME_TOKENS,
2229
2259
  themeComponents: THEME_COMPONENTS,
2230
2260
  themeComponentShadows: THEME_COMPONENT_SHADOWS,
2261
+ themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
2231
2262
  themeComponentGradient: THEME_COMPONENT_GRADIENT,
2232
2263
  widgetContextShape: WIDGET_CONTEXT_SHAPE,
2233
2264
  bundleExportContract: BUNDLE_EXPORT_CONTRACT,
@@ -44,9 +44,22 @@ function coerceToken(def, value) {
44
44
  if (!Number.isFinite(n)) return undefined;
45
45
  return Math.min(def.max, Math.max(def.min, Math.round(n)));
46
46
  }
47
+ // sc-3857: a leading multiplier / tracking value must survive the decimals
48
+ // `size` rounds away — 1.05 leading is the whole point of the token.
49
+ if (def.type === "decimal") {
50
+ const n = typeof value === "number" ? value : Number(value);
51
+ if (!Number.isFinite(n)) return undefined;
52
+ const clamped = Math.min(def.max, Math.max(def.min, n));
53
+ return Math.round(clamped * 100) / 100;
54
+ }
47
55
  if (def.type === "shadow") {
48
56
  return CONTRACT.themeComponentShadows.includes(value) ? value : undefined;
49
57
  }
58
+ if (def.type === "textTransform") {
59
+ return CONTRACT.themeComponentTextTransforms.includes(value)
60
+ ? value
61
+ : undefined;
62
+ }
50
63
  // sc-3727: the gradient value has its own normaliser on the contract, shared
51
64
  // with the widget render path so both routes agree (CLAUDE.md §3).
52
65
  if (def.type === "gradient") {
@@ -36,9 +36,22 @@ function coerceToken(def, value) {
36
36
  if (!Number.isFinite(n)) return undefined;
37
37
  return Math.min(def.max, Math.max(def.min, Math.round(n)));
38
38
  }
39
+ // sc-3857: a leading multiplier / tracking value must survive the decimals
40
+ // `size` rounds away — 1.05 leading is the whole point of the token.
41
+ if (def.type === "decimal") {
42
+ const n = typeof value === "number" ? value : Number(value);
43
+ if (!Number.isFinite(n)) return undefined;
44
+ const clamped = Math.min(def.max, Math.max(def.min, n));
45
+ return Math.round(clamped * 100) / 100;
46
+ }
39
47
  if (def.type === "shadow") {
40
48
  return CONTRACT.themeComponentShadows.includes(value) ? value : undefined;
41
49
  }
50
+ if (def.type === "textTransform") {
51
+ return CONTRACT.themeComponentTextTransforms.includes(value)
52
+ ? value
53
+ : undefined;
54
+ }
42
55
  // sc-3727: the gradient value has its own normaliser on the contract, shared
43
56
  // with the widget render path so both routes agree (CLAUDE.md §3).
44
57
  if (def.type === "gradient") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.70.0",
3
+ "version": "0.71.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",