@colixsystems/widget-sdk 0.71.0 → 0.73.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,7 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
54
54
 
55
55
  ## Status
56
56
 
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**.
57
+ `v0.73.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
58
 
59
59
  ### What's new in 0.71.0
60
60
 
@@ -170,6 +170,10 @@ useEffect(() => {
170
170
 
171
171
  **New linter rule `react-not-imported` — widget source must be self-contained (sc-2353).** The automatic JSX runtime binds `jsx`/`jsxs` from `react/jsx-runtime` but never `React` itself, so source that reaches for the bare `React` global (`React.createElement` / `React.Fragment` / `React.memo` / `React.useMemo`) without importing it bundles cleanly, then throws `ReferenceError: React is not defined` the moment a non-initial code path hits the reference. The platform does **not** inject a `React` binding — earlier behaviour that auto-injected one left source that broke as soon as it was downloaded and re-uploaded through a path that doesn't inject. The linter now flags a bare-`React` reference with no `import React from "react"` (or `import * as React`) as an error, so the failure becomes a publish/upload finding (and an AI-agent repair-loop finding) instead of a broken shipped widget. Plain JSX, which needs no React import, is never flagged; a `React` mention inside a comment or string is masked. Author fix: add `import React from "react";` (`react` is already vetted), or prefer a JSX fragment `<>…</>` plus the SDK hooks/primitives over reaching for `React` directly. `CONTRACT` is unchanged (no new field).
172
172
 
173
+ ### What's new in 0.73.0
174
+
175
+ **`secrets` is now listed in `CONTRACT.actionScriptGlobals`.** The action runner has always exposed a frozen `secrets` object to server-action scripts — the tenant's stored key/value configuration, read as `secrets["STRIPE_API_KEY"]` (REQ-ACTION-SECRET) — but the contract never listed it, so the Developer guide and the AI widget agent's prompt, both of which derive their globals list from this array, never told an author it existed. That omission is why generated scripts inline a plaintext credential instead of reading one. **Never put an API key, token, or password in `scriptSource`** — a manifest is distributed with the widget, so a literal credential is published with it. Name the key, read it from `secrets`, and fail loudly when it is missing; the workspace operator supplies the value in the Studio. `CONTRACT.version` → `1.50.0`. Documentation-only against the runtime — the global was already there — but additive to the published contract, so it moves the minor.
176
+
173
177
  ### What's new in 0.54.0
174
178
 
175
179
  **Generate & save PDFs from a widget (sc-2314).** New `usePdfExport({ spaceType, folderId? })` hook. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to a PDF **server-side** (the platform's headless-Chromium pipeline) and saves it into the end-user's Filestore via `ctx.filestore.files.exportPdf`, resolving to the created file row (`application/pdf`). It reuses the filestore owner_id resolution + per-folder write gate and the existing `files.write:*` scope. Because the rendering is server-side, the capability behaves identically on the web Player and the native Expo export — no browser-only PDF library is added to the vetted set. Pairs with `@colixsystems/filestore-client@0.6.0`'s new `files.exportPdf(...)`. `CONTRACT.version` → `1.38.0`. Additive — no existing hook, primitive, manifest field, or token changed shape.
@@ -432,7 +436,7 @@ A runtime schema resolver so widgets can render by column type.
432
436
  The tenant's **Theme Settings** now flow all the way into `useTheme()`.
433
437
 
434
438
  - **`themeTokens.colors` gains `secondary` + `onSecondary`.** `useTheme().colors.secondary` reflects the tenant's *Secondary Color* picker (with `onSecondary` as its readable contrast color), alongside the existing `primary` / `onPrimary`. Built-in widgets like Button use it for their secondary variant; third-party widgets can use it for a branded second accent. The full `colors` shape is now `{ primary, onPrimary, secondary, onSecondary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }`.
435
- - **`colors.primary` / `colors.secondary` / `typography.fontFamily` are tenant-resolved.** The host maps the Studio Theme Settings blob (Primary Color, Secondary Color, Global Font) onto the default tokens before handing them to `useTheme()`, on both the live Player and the exported app — so a widget that reads tokens re-themes automatically. (Custom Google fonts render in the Player today; the exported app falls back to the system face for non-system fonts until font bundling lands.)
439
+ - **`colors.primary` / `colors.secondary` / `typography.fontFamily` / `typography.headingFontFamily` are tenant-resolved.** The host maps the Studio Theme Settings blob (Primary Color, Secondary Color, Global Font, Heading Font) onto the default tokens before handing them to `useTheme()`, on both the live Player and the exported app — so a widget that reads tokens re-themes automatically. Both families are loaded by the Player and bundled into the exported app, so they render the same on web and native.
436
440
  - **`CONTRACT.version` → `1.6.0`** (additive: two new `themeTokens.colors` keys). No existing export changed signature.
437
441
 
438
442
  ### What's new in 0.15.0
@@ -562,11 +566,12 @@ A widget that works but looks unfinished is only half done. `useTheme()` is the
562
566
 
563
567
  - **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.
564
568
  - **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.
565
- - **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`.
569
+ - **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.
566
570
  - **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.
567
571
  - **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.
568
572
  - **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.
569
- - **Compose forms pair fields into rows, don't stack one per row.** Put short, related fields side by side (first + last name, city + postal code, expiry + CVC): a row of `{ flexDirection: 'row', flexWrap: 'wrap', gap: theme.spacing.md }` with each field cell `{ flexGrow: 1, flexBasis: 160 }` splits the width on a wide card and wraps to stacked on a narrow phone the native-safe way to go multi-column (widgets have no breakpoint hook, so never hard-code fixed columns). Keep wide fields (email, address, notes) full-width, cap it at two–three per row, group a long form into labelled sections, and label every input above it (not placeholder-only).
573
+ - **Size to your container measure it, don't stretch into it.** The same widget sits in a full-width desktop section (~1400px), a half-width grid cell (~700px) and a phone (~360px), so layout built only from `flex: 1` stretches to fill whatever it is handed — a month calendar ends up with 200px day cells and swallows the page. Measure your own width with `onLayout={(e) => setWidth(e.nativeEvent.layout.width)}` on the root `View` (a React Native primitive, so it behaves identically on both hosts), render nothing size-dependent while `width === 0`, and compute every threshold from the measured value: a widget gets no declared breakpoint prop, but it can always measure. **Cap a repeating cell** rather than giving a grid `flex: 1` — `const cell = Math.max(32, Math.min(Math.floor((usable - gap * (columns - 1)) / columns), 64));`, with a calendar day cell topping out at 56–72px on `aspectRatio: 1`, and the grid given its exact computed width plus `alignSelf: 'center'` when the cap leaves slack. **Split two co-equal surfaces above ~720px measured width** (`flexDirection: width >= 720 ? 'row' : 'column'`, each half `{ flex: 1, minWidth: 0 }`) — a picker beside the form it feeds on a wide canvas, stacked in reading order below it. Never hardcode a width, never put `flex: 1` / `height: '100%'` on a content widget's root, and don't read the screen with `Dimensions` — the screen is not the widget.
574
+ - **Compose forms — pair fields into rows, don't stack one per row.** Put short, related fields side by side (first + last name, city + postal code, expiry + CVC): a row of `{ flexDirection: 'row', flexWrap: 'wrap', gap: theme.spacing.md }` with each field cell `{ flexGrow: 1, flexBasis: 160 }` splits the width on a wide card and wraps to stacked on a narrow phone — the right recipe for field pairs because it needs no measurement (a fixed-width column overflows a phone; when a layout needs a real column count instead of wrapping, measure your width as above). Keep wide fields (email, address, notes) full-width, cap it at two–three per row, group a long form into labelled sections, and label every input above it (not placeholder-only).
570
575
  - **Respond to touch.** Give every `Pressable` a pressed state via the function-style `style={({ pressed }) => [base, pressed && { opacity: 0.7 }]}`.
571
576
  - **Drag and drop — show what is being dragged.** A drag where the item stays put reads as broken. Three things change the moment a drag starts: the **drag proxy** (the item lifts and follows the finger — `...theme.elevation.lg`, `{ scale: 1.03 }`, `opacity: 0.9`; for a tall or full-width item drag a compact `primarySoft` pill with its icon + one line of label instead), the **source placeholder** (the vacated slot keeps its height as a quiet `colors.surfaceMuted` block so the list doesn't collapse), and the **drop target** (one slot at a time highlighted with `primarySoft` or a 2px `colors.primary` border). Always animate the release — settle into the new slot, or `Animated.spring(pan, { toValue: { x: 0, y: 0 }, useNativeDriver: false })` back to the origin on cancel. Build it with `Animated` + `PanResponder` from `react-native` (the only mechanism that behaves identically on both hosts) — never HTML5 drag events (`draggable` / `onDragStart` / `dataTransfer` are web-only, and `document` / `window` are banned) — and start the drag from a `GripVertical` grip handle whenever the row is also tappable or sits in a `ScrollView`.
572
577
  - **Use icons for clarity.** Pair a `lucide-react-native` icon with its label at a consistent size, coloured from the theme. The label never repeats the icon as a character — with a `Plus` icon the button says "Add item", never "+ Add item" (that renders a doubled plus).
package/dist/contract.cjs CHANGED
@@ -76,6 +76,10 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
76
76
  typography: Object.freeze({
77
77
  fontFamily:
78
78
  'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
79
+ // The display face for heading-tier text. Defaults to `fontFamily` — an app
80
+ // that never pairs renders on one typeface exactly as before.
81
+ headingFontFamily:
82
+ 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
79
83
  sizes: Object.freeze({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, xxl: 32 }),
80
84
  }),
81
85
  // REQ-THEME-15: the tenant's per-component style tokens, folded in by the
@@ -1116,6 +1120,7 @@ const ACTION_TRIGGER_TYPES = [
1116
1120
  // linter does NOT scan action scripts.
1117
1121
  const ACTION_SCRIPT_GLOBALS = [
1118
1122
  "datastore",
1123
+ "secrets",
1119
1124
  "fetch",
1120
1125
  "connectors",
1121
1126
  "console",
@@ -2245,7 +2250,23 @@ const CONTRACT = deepFreeze({
2245
2250
  // multiplier) and `textTransform`, whose closed enum is published as
2246
2251
  // `themeComponentTextTransforms`. Additive: no export changed signature and
2247
2252
  // a theme with none of the new tokens renders exactly as before.
2248
- version: "1.48.0",
2253
+ //
2254
+ // 1.49.0: additive (REQ-THEME-12) — `themeTokens.typography` gains
2255
+ // `headingFontFamily`, the display face for heading-tier text, and the
2256
+ // per-instance text vocabulary gains `fontRole` (`body` | `heading`) so an
2257
+ // author or Mason marks which text is a heading. `headingFontFamily`
2258
+ // defaults to the resolved `fontFamily`, and `fontRole` defaults to
2259
+ // `body`, so an app that sets neither renders on one typeface exactly as
2260
+ // before. Genuine headings — rich-text h1-h3 and container titles — take
2261
+ // the heading face without the field.
2262
+ // 1.50.0: additive — `secrets` joins `actionScriptGlobals`. The action
2263
+ // runner has always frozen the tenant's resolved key/value config onto
2264
+ // the sandbox as `secrets` (REQ-ACTION-SECRET), but the contract never
2265
+ // listed it, so the Developer guide and the agent prompt — both derived
2266
+ // from this list — never told an author it existed. That omission is why
2267
+ // a generated script inlines a plaintext credential instead of reading
2268
+ // `secrets["KEY"]`. No runtime change; the global was already there.
2269
+ version: "1.50.0",
2249
2270
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
2250
2271
  hooks: HOOKS,
2251
2272
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -76,6 +76,10 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
76
76
  typography: Object.freeze({
77
77
  fontFamily:
78
78
  'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
79
+ // The display face for heading-tier text. Defaults to `fontFamily` — an app
80
+ // that never pairs renders on one typeface exactly as before.
81
+ headingFontFamily:
82
+ 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
79
83
  sizes: Object.freeze({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, xxl: 32 }),
80
84
  }),
81
85
  // REQ-THEME-15: the tenant's per-component style tokens, folded in by the
@@ -1116,6 +1120,7 @@ const ACTION_TRIGGER_TYPES = [
1116
1120
  // linter does NOT scan action scripts.
1117
1121
  const ACTION_SCRIPT_GLOBALS = [
1118
1122
  "datastore",
1123
+ "secrets",
1119
1124
  "fetch",
1120
1125
  "connectors",
1121
1126
  "console",
@@ -2245,7 +2250,23 @@ const CONTRACT = deepFreeze({
2245
2250
  // multiplier) and `textTransform`, whose closed enum is published as
2246
2251
  // `themeComponentTextTransforms`. Additive: no export changed signature and
2247
2252
  // a theme with none of the new tokens renders exactly as before.
2248
- version: "1.48.0",
2253
+ //
2254
+ // 1.49.0: additive (REQ-THEME-12) — `themeTokens.typography` gains
2255
+ // `headingFontFamily`, the display face for heading-tier text, and the
2256
+ // per-instance text vocabulary gains `fontRole` (`body` | `heading`) so an
2257
+ // author or Mason marks which text is a heading. `headingFontFamily`
2258
+ // defaults to the resolved `fontFamily`, and `fontRole` defaults to
2259
+ // `body`, so an app that sets neither renders on one typeface exactly as
2260
+ // before. Genuine headings — rich-text h1-h3 and container titles — take
2261
+ // the heading face without the field.
2262
+ // 1.50.0: additive — `secrets` joins `actionScriptGlobals`. The action
2263
+ // runner has always frozen the tenant's resolved key/value config onto
2264
+ // the sandbox as `secrets` (REQ-ACTION-SECRET), but the contract never
2265
+ // listed it, so the Developer guide and the agent prompt — both derived
2266
+ // from this list — never told an author it existed. That omission is why
2267
+ // a generated script inlines a plaintext credential instead of reading
2268
+ // `secrets["KEY"]`. No runtime change; the global was already there.
2269
+ version: "1.50.0",
2249
2270
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
2250
2271
  hooks: HOOKS,
2251
2272
  primitives: PRIMITIVES,
package/dist/index.d.ts CHANGED
@@ -306,6 +306,8 @@ export interface ThemeTokens {
306
306
  radii: { sm: number; md: number; lg: number; pill: number };
307
307
  typography: {
308
308
  fontFamily: string;
309
+ /** The display face for heading-tier text. Defaults to `fontFamily`. */
310
+ headingFontFamily: string;
309
311
  sizes: { xs: number; sm: number; md: number; lg: number; xl: number };
310
312
  };
311
313
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.71.0",
3
+ "version": "0.73.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",