@colixsystems/widget-sdk 0.148.0 → 0.149.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
@@ -1626,7 +1626,20 @@ A widget that works but looks unfinished is only half done. `useTheme()` is the
1626
1626
 
1627
1627
  - **Pull spacing and corners from tokens.** Use `theme.spacing` (`xs / sm / md / lg / xl`) for a consistent padding and gap rhythm, and `theme.radii` (`sm / md / lg / pill`) for corners — `radii.lg` for cards and hero surfaces, `radii.md` for controls nested inside one. Don't hardcode raw pixel values.
1628
1628
  - **Build a hierarchy.** A clear title (large, bold, `colors.onSurface`), body text, and muted captions in `colors.onSurfaceMuted` — three weights, not one flat size. Reserve full-strength `colors.primary` (with `colors.onPrimary` for text on it) for the single most important action or metric.
1629
- - **Set the theme font on every `Text`.** React Native `Text` does not inherit `fontFamily` from a parent, so a text element that omits it falls back to the system font and ignores the workspace's configured font. Put `theme.typography.fontFamily` on every text style (a shared `StyleSheet` built from `theme` keeps it in one place) and size text with `theme.typography.sizes`. Use `theme.typography.headingFontFamily` instead on your widget's HEADING-tier text — titles and display numbers — so a workspace that pairs a display face with a body face gets that pairing inside your widget too. It defaults to `fontFamily`, so an unpaired workspace looks identical.
1629
+ - **Style text by its ROLE, not by inventing a size and weight.** `theme.typography.roles` carries the workspace's type scale as six semantic tiers — `display`, `title`, `subtitle`, `body`, `caption`, `overline` — each already resolved to a `fontSize`, `fontWeight`, `fontStyle`, `lineHeight` (a MULTIPLE of the size), `letterSpacing`, `textTransform` and a `colorToken`. Pick the tier the text IS, rather than choosing `fontSize: 16, fontWeight: "600"` yourself: your widget's section title is `title`, each card or row title is `subtitle`, running text is `body`, timestamps and helper text are `caption`, a small kicker or column label is `overline`, and a headline stat is `display`. This is what stops one widget rendering a subtitle bold while the widget beside it renders one italic, and it lets the workspace re-tune every subtitle in the app from Theme Settings in one edit. **Two tokens need converting rather than copying** — `lineHeight` is a multiple (React Native wants absolute pixels, so the raw `1.15` gives you a one-pixel line box) and `colorToken` names a colour rather than being one:
1630
+
1631
+ ```js
1632
+ const r = theme.typography.roles.subtitle;
1633
+ const titleStyle = {
1634
+ fontSize: r.fontSize,
1635
+ fontWeight: r.fontWeight,
1636
+ letterSpacing: r.letterSpacing,
1637
+ textTransform: r.textTransform,
1638
+ lineHeight: Math.round(r.fontSize * r.lineHeight),
1639
+ color: r.color || theme.colors[r.colorToken],
1640
+ };
1641
+ ```
1642
+ - **Set the theme font on every `Text`.** React Native `Text` does not inherit `fontFamily` from a parent, so a text element that omits it falls back to the system font and ignores the workspace's configured font. Put `theme.typography.fontFamily` on every text style (a shared `StyleSheet` built from `theme` keeps it in one place). A role already names its face for you — `display` and `title` take `theme.typography.headingFontFamily`, the rest take `fontFamily` — so a workspace that pairs a display face with a body face gets that pairing inside your widget without you naming a family. `headingFontFamily` defaults to `fontFamily`, so an unpaired workspace looks identical.
1630
1643
  - **Contain and elevate.** Wrap a logical unit in a surface: `colors.surface` + padding + `radii.lg` + `...theme.elevation.sm`. Give it the elevation **or** a `colors.border` hairline, not both — and prefer the elevation, because a hairline-only card reads as a wireframe. `theme.elevation` is a token table you spread into a style (`...theme.elevation.md`), covering `none / sm / md / lg / xl`; never hand-write `shadowOpacity` / `shadowRadius` / `boxShadow`. Use the status roles (`danger / success / warning / info`) for state.
1631
1644
  - **Tint the supporting cast.** `colors.primarySoft` is a tint of the workspace accent over the surface and `colors.onPrimarySoft` is guaranteed readable on it (WCAG AA, on light and dark themes alike). Use the pair for chips, secondary buttons, progress tracks, icon badges and selected rows. One saturated accent moment surrounded by several pale echoes of the same hue is what reads as designed — a row of grey-outlined buttons reads as a form. Never hand-mix a tint with `rgba(...)` or a translucent overlay.
1632
1645
  - **Spend one gradient.** `<Gradient colors={[theme.colors.primary, theme.colors.primaryStrong]} angle={160} style={…}>` is a `View` that paints a gradient behind its children, so it replaces the `View` you'd otherwise give a flat `backgroundColor`. `angle` is CSS degrees (0 = to top, 90 = to right, default 180); text on it uses `colors.onPrimary`. Exactly **one** per widget — on the focal element — and never behind body text. Both hosts render it identically (web paints CSS, native uses `expo-linear-gradient`), so there is no per-platform branching to write; don't import `expo-linear-gradient` yourself and don't write a `backgroundImage` string.
package/dist/contract.cjs CHANGED
@@ -108,6 +108,58 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
108
108
  headingFontFamily:
109
109
  'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
110
110
  sizes: Object.freeze({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, xxl: 32 }),
111
+ // REQ-THEME-ROLE (sc-8058): the app's type scale as six SEMANTIC roles.
112
+ // Typography carried two faces and a size scale and nothing else, so a
113
+ // widget needing a "subtitle" had nothing to reach for and each invented
114
+ // its own literal — bold in Notifications, italic in User Management,
115
+ // unweighted in Files. A role names the TIER; the theme owns its look.
116
+ //
117
+ // Which FACE a role takes lives in `_shared/textStyle.js` ROLE_FACES —
118
+ // structural, not themeable, so it resolves without theme data. `colorToken` is
119
+ // the SURFACE token the role is painted with, resolved per render against
120
+ // the surface-aware theme (REQ-THEME-DARK / REQ-THEME-SURFACE) rather than
121
+ // frozen as a hex — a theme may override it with an explicit `color`.
122
+ roles: Object.freeze({
123
+ display: Object.freeze({
124
+ fontSize: 32,
125
+ fontWeight: "700",
126
+ lineHeight: 1.05,
127
+ letterSpacing: -0.5,
128
+ colorToken: "onSurface",
129
+ }),
130
+ title: Object.freeze({
131
+ fontSize: 24,
132
+ fontWeight: "700",
133
+ lineHeight: 1.15,
134
+ letterSpacing: -0.2,
135
+ colorToken: "onSurface",
136
+ }),
137
+ subtitle: Object.freeze({
138
+ fontSize: 16,
139
+ fontWeight: "600",
140
+ lineHeight: 1.3,
141
+ colorToken: "onSurface",
142
+ }),
143
+ body: Object.freeze({
144
+ fontSize: 16,
145
+ fontWeight: "400",
146
+ lineHeight: 1.5,
147
+ colorToken: "onSurface",
148
+ }),
149
+ caption: Object.freeze({
150
+ fontSize: 12,
151
+ fontWeight: "400",
152
+ lineHeight: 1.4,
153
+ colorToken: "onSurfaceMuted",
154
+ }),
155
+ overline: Object.freeze({
156
+ fontSize: 12,
157
+ fontWeight: "600",
158
+ lineHeight: 1.4,
159
+ letterSpacing: 1.5,
160
+ colorToken: "onSurfaceMuted",
161
+ }),
162
+ }),
111
163
  }),
112
164
  // REQ-THEME-15: the tenant's per-component style tokens, folded in by the
113
165
  // host so ONE channel carries them to the Player and the export. Empty by
@@ -172,6 +224,44 @@ const THEME_COMPONENT_TEXT_TRANSFORMS = Object.freeze([
172
224
  "capitalize",
173
225
  ]);
174
226
 
227
+ // REQ-THEME-ROLE (sc-8058) -- the THEME side of the six semantic text tiers, in
228
+ // the order the widgets' own TEXT_ROLES lists them. `themeTokens.typography.roles`
229
+ // carries the seeded values; this names what a TENANT may override, so the Studio
230
+ // control, the Mason `set_theme` coercion and the docs read ONE vocabulary --
231
+ // exactly as `themeComponents` already is for the per-component tokens.
232
+ const THEME_TEXT_ROLES = Object.freeze([
233
+ "display",
234
+ "title",
235
+ "subtitle",
236
+ "body",
237
+ "caption",
238
+ "overline",
239
+ ]);
240
+
241
+ // Mirrors the backend's SITE_HEADER_FONT_WEIGHTS so one theme speaks one weight
242
+ // vocabulary. Strings, because that is what both hosts' `fontWeight` takes.
243
+ const THEME_TEXT_ROLE_FONT_WEIGHTS = Object.freeze([
244
+ "100", "200", "300", "400", "500", "600", "700", "800", "900",
245
+ ]);
246
+
247
+ const THEME_TEXT_ROLE_FONT_STYLES = Object.freeze(["normal", "italic"]);
248
+
249
+ // The per-role token vocabulary, declared like a `themeComponents` scope's
250
+ // `tokens`: each field's value type and bounds stated ONCE, so the Studio
251
+ // control, the set_theme coercion and the host validator cannot disagree.
252
+ // `lineHeight` is a MULTIPLE of `fontSize` (REQ-THEME-13's grammar), so one
253
+ // value is right at every size; `color` overrides the role's surface-aware
254
+ // `colorToken`, which is why no role seeds a literal colour.
255
+ const THEME_TEXT_ROLE_FIELDS = Object.freeze({
256
+ fontSize: Object.freeze({ type: "size", min: 8, max: 96 }),
257
+ fontWeight: Object.freeze({ type: "enum", values: THEME_TEXT_ROLE_FONT_WEIGHTS }),
258
+ fontStyle: Object.freeze({ type: "enum", values: THEME_TEXT_ROLE_FONT_STYLES }),
259
+ lineHeight: Object.freeze({ type: "decimal", min: 0.8, max: 3, step: 0.05 }),
260
+ letterSpacing: Object.freeze({ type: "decimal", min: -2, max: 20, step: 0.1 }),
261
+ textTransform: Object.freeze({ type: "textTransform" }),
262
+ color: Object.freeze({ type: "color" }),
263
+ });
264
+
175
265
  // sc-3727 — the `gradient` token's value shape: two hex stops plus a CSS-degree
176
266
  // angle, the same grammar as `themeConfig.backgroundGradient` minus the radial
177
267
  // variant (a component fill projects through `<Gradient>`, which is linear-only
@@ -4140,7 +4230,20 @@ const CONTRACT = deepFreeze({
4140
4230
  // where the rail's and the header's keep sc-7965's 1-8 clamp. A backfill
4141
4231
  // (`20270123000000_sc8033_footer_tokens_uncouple`) materialised what each
4142
4232
  // workspace was inheriting, so no shipped app moved.
4143
- version: "1.116.0",
4233
+ // 1.117.0: additive (sc-8058) -- the six SEMANTIC text roles.
4234
+ // `themeTokens.typography.roles` seeds display / title / subtitle / body /
4235
+ // caption / overline, and `themeTextRoles` + `themeTextRoleFields` publish
4236
+ // the tier names and the per-role token vocabulary (each field's type and
4237
+ // bounds) a TENANT may override -- the single source the Studio control,
4238
+ // the Mason `set_theme` coercion and the docs read, exactly as
4239
+ // `themeComponents` already is. Typography carried two faces and a size
4240
+ // scale and nothing else, so every widget drawing a "subtitle" invented its
4241
+ // own literal and no theme edit could move them together. Which FACE a role
4242
+ // takes is deliberately absent -- that is structural (`ROLE_FACES` in the
4243
+ // hosts' shared textStyle module), not a theme token. Additive: no export
4244
+ // changed signature, and a theme carrying no `roles` renders on the seeded
4245
+ // defaults exactly as before.
4246
+ version: "1.117.0",
4144
4247
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
4145
4248
  hooks: HOOKS,
4146
4249
  primitives: PRIMITIVES,
@@ -4156,6 +4259,8 @@ const CONTRACT = deepFreeze({
4156
4259
  themeComponentShadows: THEME_COMPONENT_SHADOWS,
4157
4260
  themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
4158
4261
  themeComponentGradient: THEME_COMPONENT_GRADIENT,
4262
+ themeTextRoles: THEME_TEXT_ROLES,
4263
+ themeTextRoleFields: THEME_TEXT_ROLE_FIELDS,
4159
4264
  themeSpacingScale: THEME_SPACING_SCALE,
4160
4265
  themeMenuTypes: THEME_MENU_TYPES,
4161
4266
  themeWidgetStyles: THEME_WIDGET_STYLES,
package/dist/contract.js CHANGED
@@ -108,6 +108,58 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
108
108
  headingFontFamily:
109
109
  'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
110
110
  sizes: Object.freeze({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, xxl: 32 }),
111
+ // REQ-THEME-ROLE (sc-8058): the app's type scale as six SEMANTIC roles.
112
+ // Typography carried two faces and a size scale and nothing else, so a
113
+ // widget needing a "subtitle" had nothing to reach for and each invented
114
+ // its own literal — bold in Notifications, italic in User Management,
115
+ // unweighted in Files. A role names the TIER; the theme owns its look.
116
+ //
117
+ // Which FACE a role takes lives in `_shared/textStyle.js` ROLE_FACES —
118
+ // structural, not themeable, so it resolves without theme data. `colorToken` is
119
+ // the SURFACE token the role is painted with, resolved per render against
120
+ // the surface-aware theme (REQ-THEME-DARK / REQ-THEME-SURFACE) rather than
121
+ // frozen as a hex — a theme may override it with an explicit `color`.
122
+ roles: Object.freeze({
123
+ display: Object.freeze({
124
+ fontSize: 32,
125
+ fontWeight: "700",
126
+ lineHeight: 1.05,
127
+ letterSpacing: -0.5,
128
+ colorToken: "onSurface",
129
+ }),
130
+ title: Object.freeze({
131
+ fontSize: 24,
132
+ fontWeight: "700",
133
+ lineHeight: 1.15,
134
+ letterSpacing: -0.2,
135
+ colorToken: "onSurface",
136
+ }),
137
+ subtitle: Object.freeze({
138
+ fontSize: 16,
139
+ fontWeight: "600",
140
+ lineHeight: 1.3,
141
+ colorToken: "onSurface",
142
+ }),
143
+ body: Object.freeze({
144
+ fontSize: 16,
145
+ fontWeight: "400",
146
+ lineHeight: 1.5,
147
+ colorToken: "onSurface",
148
+ }),
149
+ caption: Object.freeze({
150
+ fontSize: 12,
151
+ fontWeight: "400",
152
+ lineHeight: 1.4,
153
+ colorToken: "onSurfaceMuted",
154
+ }),
155
+ overline: Object.freeze({
156
+ fontSize: 12,
157
+ fontWeight: "600",
158
+ lineHeight: 1.4,
159
+ letterSpacing: 1.5,
160
+ colorToken: "onSurfaceMuted",
161
+ }),
162
+ }),
111
163
  }),
112
164
  // REQ-THEME-15: the tenant's per-component style tokens, folded in by the
113
165
  // host so ONE channel carries them to the Player and the export. Empty by
@@ -172,6 +224,44 @@ const THEME_COMPONENT_TEXT_TRANSFORMS = Object.freeze([
172
224
  "capitalize",
173
225
  ]);
174
226
 
227
+ // REQ-THEME-ROLE (sc-8058) -- the THEME side of the six semantic text tiers, in
228
+ // the order the widgets' own TEXT_ROLES lists them. `themeTokens.typography.roles`
229
+ // carries the seeded values; this names what a TENANT may override, so the Studio
230
+ // control, the Mason `set_theme` coercion and the docs read ONE vocabulary --
231
+ // exactly as `themeComponents` already is for the per-component tokens.
232
+ const THEME_TEXT_ROLES = Object.freeze([
233
+ "display",
234
+ "title",
235
+ "subtitle",
236
+ "body",
237
+ "caption",
238
+ "overline",
239
+ ]);
240
+
241
+ // Mirrors the backend's SITE_HEADER_FONT_WEIGHTS so one theme speaks one weight
242
+ // vocabulary. Strings, because that is what both hosts' `fontWeight` takes.
243
+ const THEME_TEXT_ROLE_FONT_WEIGHTS = Object.freeze([
244
+ "100", "200", "300", "400", "500", "600", "700", "800", "900",
245
+ ]);
246
+
247
+ const THEME_TEXT_ROLE_FONT_STYLES = Object.freeze(["normal", "italic"]);
248
+
249
+ // The per-role token vocabulary, declared like a `themeComponents` scope's
250
+ // `tokens`: each field's value type and bounds stated ONCE, so the Studio
251
+ // control, the set_theme coercion and the host validator cannot disagree.
252
+ // `lineHeight` is a MULTIPLE of `fontSize` (REQ-THEME-13's grammar), so one
253
+ // value is right at every size; `color` overrides the role's surface-aware
254
+ // `colorToken`, which is why no role seeds a literal colour.
255
+ const THEME_TEXT_ROLE_FIELDS = Object.freeze({
256
+ fontSize: Object.freeze({ type: "size", min: 8, max: 96 }),
257
+ fontWeight: Object.freeze({ type: "enum", values: THEME_TEXT_ROLE_FONT_WEIGHTS }),
258
+ fontStyle: Object.freeze({ type: "enum", values: THEME_TEXT_ROLE_FONT_STYLES }),
259
+ lineHeight: Object.freeze({ type: "decimal", min: 0.8, max: 3, step: 0.05 }),
260
+ letterSpacing: Object.freeze({ type: "decimal", min: -2, max: 20, step: 0.1 }),
261
+ textTransform: Object.freeze({ type: "textTransform" }),
262
+ color: Object.freeze({ type: "color" }),
263
+ });
264
+
175
265
  // sc-3727 — the `gradient` token's value shape: two hex stops plus a CSS-degree
176
266
  // angle, the same grammar as `themeConfig.backgroundGradient` minus the radial
177
267
  // variant (a component fill projects through `<Gradient>`, which is linear-only
@@ -4136,7 +4226,20 @@ const CONTRACT = deepFreeze({
4136
4226
  // where the rail's and the header's keep sc-7965's 1-8 clamp. A backfill
4137
4227
  // (`20270123000000_sc8033_footer_tokens_uncouple`) materialised what each
4138
4228
  // workspace was inheriting, so no shipped app moved.
4139
- version: "1.116.0",
4229
+ // 1.117.0: additive (sc-8058) -- the six SEMANTIC text roles.
4230
+ // `themeTokens.typography.roles` seeds display / title / subtitle / body /
4231
+ // caption / overline, and `themeTextRoles` + `themeTextRoleFields` publish
4232
+ // the tier names and the per-role token vocabulary (each field's type and
4233
+ // bounds) a TENANT may override -- the single source the Studio control,
4234
+ // the Mason `set_theme` coercion and the docs read, exactly as
4235
+ // `themeComponents` already is. Typography carried two faces and a size
4236
+ // scale and nothing else, so every widget drawing a "subtitle" invented its
4237
+ // own literal and no theme edit could move them together. Which FACE a role
4238
+ // takes is deliberately absent -- that is structural (`ROLE_FACES` in the
4239
+ // hosts' shared textStyle module), not a theme token. Additive: no export
4240
+ // changed signature, and a theme carrying no `roles` renders on the seeded
4241
+ // defaults exactly as before.
4242
+ version: "1.117.0",
4140
4243
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
4141
4244
  hooks: HOOKS,
4142
4245
  primitives: PRIMITIVES,
@@ -4152,6 +4255,8 @@ const CONTRACT = deepFreeze({
4152
4255
  themeComponentShadows: THEME_COMPONENT_SHADOWS,
4153
4256
  themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
4154
4257
  themeComponentGradient: THEME_COMPONENT_GRADIENT,
4258
+ themeTextRoles: THEME_TEXT_ROLES,
4259
+ themeTextRoleFields: THEME_TEXT_ROLE_FIELDS,
4155
4260
  themeSpacingScale: THEME_SPACING_SCALE,
4156
4261
  themeMenuTypes: THEME_MENU_TYPES,
4157
4262
  themeWidgetStyles: THEME_WIDGET_STYLES,
@@ -75,7 +75,7 @@ function normaliseImageUrl(raw) {
75
75
 
76
76
  // A single tint laid over the image. Alpha lives in the 8-digit hex, so the
77
77
  // author controls opacity with the colour itself rather than a second field —
78
- // the same way `siteHeader.backgroundColor` already floats a header.
78
+ // the same way `siteHeader.backgroundColor` already tints a header.
79
79
  function normaliseOverlay(raw) {
80
80
  return hexOrNull(raw);
81
81
  }
@@ -67,7 +67,7 @@ function normaliseImageUrl(raw) {
67
67
 
68
68
  // A single tint laid over the image. Alpha lives in the 8-digit hex, so the
69
69
  // author controls opacity with the colour itself rather than a second field —
70
- // the same way `siteHeader.backgroundColor` already floats a header.
70
+ // the same way `siteHeader.backgroundColor` already tints a header.
71
71
  function normaliseOverlay(raw) {
72
72
  return hexOrNull(raw);
73
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.148.0",
3
+ "version": "0.149.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",