@colixsystems/widget-sdk 0.259.0 → 0.261.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.
Files changed (75) hide show
  1. package/README.md +9 -1
  2. package/dist/accordion.js +2 -1
  3. package/dist/accordion.native.js +2 -1
  4. package/dist/banner.js +1 -1
  5. package/dist/banner.native.js +2 -1
  6. package/dist/bounded-text.native.js +29 -0
  7. package/dist/button.js +1 -1
  8. package/dist/button.native.js +2 -1
  9. package/dist/card-photo.js +1 -1
  10. package/dist/card.js +1 -1
  11. package/dist/chart.js +1 -1
  12. package/dist/chart.native.js +2 -1
  13. package/dist/checkbox.js +1 -1
  14. package/dist/checkbox.native.js +2 -1
  15. package/dist/chip.js +1 -1
  16. package/dist/chip.native.js +2 -1
  17. package/dist/confirm-dialog.js +1 -1
  18. package/dist/confirm-dialog.native.js +3 -3
  19. package/dist/contract.cjs +22 -18
  20. package/dist/contract.js +23 -18
  21. package/dist/data-table.js +1 -1
  22. package/dist/data-table.native.js +2 -1
  23. package/dist/datetimepicker.js +2 -1
  24. package/dist/datetimepicker.native.js +2 -1
  25. package/dist/detail-parts.js +1 -1
  26. package/dist/detail-parts.native.js +2 -1
  27. package/dist/emoji-picker.js +1 -1
  28. package/dist/emoji-picker.native.js +2 -1
  29. package/dist/file-image.js +1 -1
  30. package/dist/file-upload.js +2 -1
  31. package/dist/file-upload.native.js +2 -1
  32. package/dist/filter-bar.js +1 -1
  33. package/dist/form.js +1 -1
  34. package/dist/form.native.js +2 -1
  35. package/dist/gradient.js +1 -1
  36. package/dist/grid.js +1 -1
  37. package/dist/index.d.ts +2 -0
  38. package/dist/linter.cjs +1 -1
  39. package/dist/list.js +1 -1
  40. package/dist/list.native.js +2 -1
  41. package/dist/loading.js +1 -1
  42. package/dist/loading.native.js +2 -1
  43. package/dist/markdown-input.js +1 -1
  44. package/dist/markdown-input.native.js +2 -1
  45. package/dist/modal.js +1 -1
  46. package/dist/modal.native.js +1 -1
  47. package/dist/overlay.js +1 -1
  48. package/dist/people-picker.js +1 -1
  49. package/dist/popover.js +1 -1
  50. package/dist/popover.native.js +2 -1
  51. package/dist/primitives.js +1 -1
  52. package/dist/primitives.native.js +2 -2
  53. package/dist/radio-group.js +1 -1
  54. package/dist/radio-group.native.js +2 -1
  55. package/dist/richtext.js +1 -1
  56. package/dist/richtext.native.js +2 -1
  57. package/dist/search-field.js +1 -1
  58. package/dist/search-field.native.js +2 -1
  59. package/dist/select.js +1 -1
  60. package/dist/select.native.js +2 -1
  61. package/dist/sortable.js +1 -1
  62. package/dist/sortable.native.js +2 -1
  63. package/dist/surface-theme.js +2 -2
  64. package/dist/switch.js +1 -1
  65. package/dist/switch.native.js +2 -1
  66. package/dist/tabs.js +1 -1
  67. package/dist/tabs.native.js +2 -1
  68. package/dist/text-field.js +1 -1
  69. package/dist/text-field.native.js +2 -1
  70. package/dist/text-scale-style.js +60 -0
  71. package/dist/text-scale.d.ts +11 -0
  72. package/dist/text-scale.js +40 -0
  73. package/dist/video.js +1 -1
  74. package/package.json +7 -1
  75. package/src/contract.js +23 -18
package/README.md CHANGED
@@ -85,7 +85,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
85
85
 
86
86
  ## Status
87
87
 
88
- `v0.259.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
+ `v0.261.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**.
89
+
90
+ ### What's new in 0.261.0 (contract 1.211.0)
91
+
92
+ **The Builder can preview that bound.** On the web, `<Text>` and `<TextInput>` (and every SDK component built on them) follow a host's `TextScaleProvider`, so the Studio canvas can show a page at the largest phone text size an exported app allows. Nothing changes for a widget: outside the preview the scale is 1×.
93
+
94
+ ### What's new in 0.260.0 (contract 1.211.0)
95
+
96
+ **Text on a phone grows with the device's text-size setting only up to 1.3×** (`CONTRACT.maxFontSizeMultiplier`). React Native multiplies every font size by that setting and a browser has no equivalent, so the Player and the Builder canvas always show 1×; unbounded, a heading that fits there overflowed on a phone set to a large text size. The native `<Text>` and `<TextInput>` (and every SDK component built on them) now carry the bound, so accessibility scaling still works but a layout only has to survive 1.3×. Pass your own `maxFontSizeMultiplier` to change it for one element.
89
97
 
90
98
  ### What's new in 0.259.0 (contract 1.210.0)
91
99
 
package/dist/accordion.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // `<Accordion>` / `<AccordionItem>` (web). Binds ./accordion-view.js to
2
2
  // react-native-web; see ./primitives.js for why not the bare specifier.
3
3
 
4
- import { View, Text, Pressable } from "react-native-web";
4
+ import { View, Pressable } from "react-native-web";
5
+ import { Text } from "./text-scale.js";
5
6
  import { makeAccordion } from "./accordion-view.js";
6
7
 
7
8
  export const { Accordion, AccordionItem } = makeAccordion({ View, Text, Pressable });
@@ -1,7 +1,8 @@
1
1
  // `<Accordion>` / `<AccordionItem>` (native). Binds ./accordion-view.js to
2
2
  // React Native's own primitives; only the import differs from ./accordion.js.
3
3
 
4
- import { View, Text, Pressable } from "react-native";
4
+ import { View, Pressable } from "react-native";
5
+ import { Text } from "./bounded-text.native.js";
5
6
  import { makeAccordion } from "./accordion-view.js";
6
7
 
7
8
  export const { Accordion, AccordionItem } = makeAccordion({ View, Text, Pressable });
package/dist/banner.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // `<Banner>` (web). Binds ./banner-view.js to react-native-web; see
2
2
  // ./primitives.js for why not the bare specifier.
3
3
 
4
- import * as ReactNative from "react-native-web";
4
+ import { ReactNative } from "./text-scale.js";
5
5
  import { makeBanner } from "./banner-view.js";
6
6
 
7
7
  export const Banner = makeBanner(ReactNative);
@@ -1,7 +1,8 @@
1
1
  // `<Banner>` (native). Binds ./banner-view.js to React Native's own
2
2
  // primitives; only the import differs from ./banner.js.
3
3
 
4
- import { View, Text, Pressable } from "react-native";
4
+ import { View, Pressable } from "react-native";
5
+ import { Text } from "./bounded-text.native.js";
5
6
  import { makeBanner } from "./banner-view.js";
6
7
 
7
8
  export const Banner = makeBanner({ View, Text, Pressable });
@@ -0,0 +1,29 @@
1
+ // sc-9168 — native Text / TextInput, bounded to the contract's font-scale cap.
2
+ //
3
+ // React Native multiplies every font size by the device's font-scale setting;
4
+ // a browser has no equivalent (page zoom scales the whole layout, so nothing
5
+ // overflows), which is why this file has no web twin and the Player and the
6
+ // Builder canvas render at scale 1.0. Unbounded, a layout that fits there
7
+ // overflows on a phone set to a large text size. The cap keeps accessibility
8
+ // scaling working up to that bound; a caller's own prop still wins.
9
+ //
10
+ // Every SDK native module takes Text / TextInput from here, never from
11
+ // `react-native` directly — bounded-text-native.test.js enforces it.
12
+ import React from "react";
13
+ import { Text as RNText, TextInput as RNTextInput } from "react-native";
14
+ import { CONTRACT } from "./contract.js";
15
+
16
+ function bounded(Component, displayName) {
17
+ const Bounded = React.forwardRef(function BoundedText(props, ref) {
18
+ return React.createElement(Component, {
19
+ maxFontSizeMultiplier: CONTRACT.maxFontSizeMultiplier,
20
+ ...props,
21
+ ref,
22
+ });
23
+ });
24
+ Bounded.displayName = displayName;
25
+ return Bounded;
26
+ }
27
+
28
+ export const Text = bounded(RNText, "Text");
29
+ export const TextInput = bounded(RNTextInput, "TextInput");
package/dist/button.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // react-native-web, the CSS gradient and the web `<Spinner>`; see
3
3
  // ./primitives.js for why not the bare specifier.
4
4
 
5
- import * as ReactNative from "react-native-web";
5
+ import { ReactNative } from "./text-scale.js";
6
6
  import { Gradient } from "./gradient.js";
7
7
  import { Spinner } from "./loading.js";
8
8
  import { makeButton, BUTTON_VARIANTS } from "./button-view.js";
@@ -2,7 +2,8 @@
2
2
  // Native, the expo-linear-gradient `<Gradient>` and the native `<Spinner>`;
3
3
  // only the imports differ from ./button.js.
4
4
 
5
- import { View, Text, Pressable, StyleSheet, Platform } from "react-native";
5
+ import { View, Pressable, StyleSheet, Platform } from "react-native";
6
+ import { Text } from "./bounded-text.native.js";
6
7
  import { Gradient } from "./gradient.native.js";
7
8
  import { Spinner } from "./loading.native.js";
8
9
  import { makeButton, BUTTON_VARIANTS } from "./button-view.js";
@@ -2,7 +2,7 @@
2
2
  // `background-position` is what keeps the chosen part of a crop in frame.
3
3
 
4
4
  import React from "react";
5
- import * as ReactNative from "react-native-web";
5
+ import { ReactNative } from "./text-scale.js";
6
6
 
7
7
  // A quote, backslash or newline would end the CSS `url("…")` early.
8
8
  function cssUrl(uri) {
package/dist/card.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // `<Card>` (web). Binds ./card-view.js to react-native-web and the CSS
2
2
  // gradient; see ./primitives.js for why not the bare specifier.
3
3
 
4
- import * as ReactNative from "react-native-web";
4
+ import { ReactNative } from "./text-scale.js";
5
5
  import { Gradient } from "./gradient.js";
6
6
  import { Photo } from "./card-photo.js";
7
7
  import { makeCard } from "./card-view.js";
package/dist/chart.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // `<Chart>` (web). Binds ./chart-view.js to react-native-web and the host's
2
2
  // react-native-svg; see ./primitives.js for why not the bare specifier.
3
3
 
4
- import * as ReactNative from "react-native-web";
4
+ import { ReactNative } from "./text-scale.js";
5
5
  import Svg, { G, Path, Line, Circle, Text as SvgText } from "react-native-svg";
6
6
  import { Skeleton } from "./loading.js";
7
7
  import { EmptyState } from "./list.js";
@@ -1,7 +1,8 @@
1
1
  // `<Chart>` (native). Binds ./chart-view.js to React Native and
2
2
  // react-native-svg; only the imports differ from ./chart.js.
3
3
 
4
- import { View, Text } from "react-native";
4
+ import { View } from "react-native";
5
+ import { Text } from "./bounded-text.native.js";
5
6
  import Svg, { G, Path, Line, Circle, Text as SvgText } from "react-native-svg";
6
7
  import { Skeleton } from "./loading.native.js";
7
8
  import { EmptyState } from "./list.native.js";
package/dist/checkbox.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // `<Checkbox>` (web). Binds ./checkbox-view.js to react-native-web; see
2
2
  // ./primitives.js for why not the bare specifier.
3
3
 
4
- import * as ReactNative from "react-native-web";
4
+ import { ReactNative } from "./text-scale.js";
5
5
  import { makeCheckbox } from "./checkbox-view.js";
6
6
 
7
7
  export const Checkbox = makeCheckbox({
@@ -1,7 +1,8 @@
1
1
  // `<Checkbox>` (native). Binds ./checkbox-view.js to React Native's own
2
2
  // primitives; only the import differs from ./checkbox.js.
3
3
 
4
- import { View, Text, Pressable } from "react-native";
4
+ import { View, Pressable } from "react-native";
5
+ import { Text } from "./bounded-text.native.js";
5
6
  import { makeCheckbox } from "./checkbox-view.js";
6
7
 
7
8
  export const Checkbox = makeCheckbox({ View, Text, Pressable });
package/dist/chip.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // ./primitives.js for why not the bare specifier. Only the web keeps a single
3
3
  // tab stop per single-choice group.
4
4
 
5
- import * as ReactNative from "react-native-web";
5
+ import { ReactNative } from "./text-scale.js";
6
6
  import { makeChip } from "./chip-view.js";
7
7
 
8
8
  export const { Chip, ChipGroup } = makeChip(ReactNative, { web: true });
@@ -1,7 +1,8 @@
1
1
  // `<Chip>` / `<ChipGroup>` (native). Binds ./chip-view.js to React Native's own
2
2
  // primitives; only the import differs from ./chip.js.
3
3
 
4
- import { View, Text, Pressable } from "react-native";
4
+ import { View, Pressable } from "react-native";
5
+ import { Text } from "./bounded-text.native.js";
5
6
  import { makeChip } from "./chip-view.js";
6
7
 
7
8
  export const { Chip, ChipGroup } = makeChip({ View, Text, Pressable });
@@ -2,7 +2,7 @@
2
2
  // and the web `<Modal>`, `<Button>` and `<Spinner>`; see ./primitives.js for
3
3
  // why not the bare specifier.
4
4
 
5
- import { Text } from "react-native-web";
5
+ import { Text } from "./text-scale.js";
6
6
  import { makeConfirmDialog } from "./confirm-dialog-view.js";
7
7
  import { Modal } from "./modal.js";
8
8
  import { Button } from "./button.js";
@@ -1,8 +1,8 @@
1
- // `<ConfirmDialog>` (native). Binds ./confirm-dialog-view.js to React Native's
2
- // own primitives and the native `<Modal>`, `<Button>` and `<Spinner>`; only the
1
+ // `<ConfirmDialog>` (native). Binds ./confirm-dialog-view.js to the bounded
2
+ // native Text and the native `<Modal>`, `<Button>` and `<Spinner>`; only the
3
3
  // imports differ from ./confirm-dialog.js.
4
4
 
5
- import { Text } from "react-native";
5
+ import { Text } from "./bounded-text.native.js";
6
6
  import { makeConfirmDialog } from "./confirm-dialog-view.js";
7
7
  import { Modal } from "./modal.native.js";
8
8
  import { Button } from "./button.native.js";
package/dist/contract.cjs CHANGED
@@ -1,12 +1,10 @@
1
- // CommonJS-flavoured copy of the contract data so backend services (CJS
2
- // runtime) can `require()` it directly without dynamic import gymnastics.
3
- //
4
- // The ESM `contract.js` re-exports the SAME object as the `CONTRACT` named
5
- // export. The contract test asserts the two are identical (deep-equal) so
6
- // drift between this file and the ESM source-of-truth is caught in CI.
7
- //
8
- // Keep edits in lockstep between `contract.cjs` and `contract.js`. The
9
- // SDK build script copies both into `dist/`.
1
+ // GENERATED from contract.js by scripts/contract-cjs.js — DO NOT EDIT.
2
+ // Change contract.js, then run `npm run generate:contract` in packages/widget-sdk.
3
+
4
+ // The widget SDK contract: the ONE source the linter, the AI agents, the
5
+ // compiler and both hosts read. `contract.cjs` is GENERATED from `contract.js`
6
+ // (`npm run generate:contract`) for CommonJS consumers — never edit it by hand;
7
+ // contract-cjs-generated.test.js fails when it is stale.
10
8
 
11
9
  // REQ-AI-AGENT-DESIGN (sc-3696): the ONE cross-platform elevation vocabulary.
12
10
  // `shadow*` applies on iOS + react-native-web, `elevation` drives Android.
@@ -2645,7 +2643,7 @@ const HOOKS = [
2645
2643
  scopes: ["users.write:*"],
2646
2644
  },
2647
2645
  // REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in
2648
- // app-user. Self-service + JWT-gated (no widget scope). Mirror of contract.js.
2646
+ // app-user. Self-service + JWT-gated (no widget scope).
2649
2647
  {
2650
2648
  name: "useBankIdLink",
2651
2649
  signature: "useBankIdLink()",
@@ -2681,7 +2679,7 @@ const HOOKS = [
2681
2679
  scopes: null,
2682
2680
  },
2683
2681
  // REQ-IDENT (sc-4313) — identify a visitor who is NOT signed in and keep the
2684
- // result. Anonymous by design (no widget scope, no session). Mirror of contract.js.
2682
+ // result. Anonymous by design (no widget scope, no session).
2685
2683
  {
2686
2684
  name: "useIdentification",
2687
2685
  signature: "useIdentification(options)",
@@ -2732,7 +2730,7 @@ const HOOKS = [
2732
2730
  scopes: null,
2733
2731
  },
2734
2732
  // REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
2735
- // management for a single record. Mirror of contract.js.
2733
+ // management for a single record.
2736
2734
  {
2737
2735
  name: "useRecordPermissions",
2738
2736
  signature: "useRecordPermissions(tableId, recordId)",
@@ -2865,7 +2863,7 @@ const HOOKS = [
2865
2863
  requiredContextSlice: ["datastore.records"],
2866
2864
  scopes: ["datastore.read:<table>"],
2867
2865
  },
2868
- // REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
2866
+ // REQ-WSDK-PLATFORM §6 — Tier A SDK hooks.
2869
2867
  {
2870
2868
  name: "useClipboard",
2871
2869
  signature: "useClipboard()",
@@ -4217,14 +4215,14 @@ const WIDGET_CONTEXT_SHAPE = {
4217
4215
  required: true,
4218
4216
  fields: { requestPayment: "function", getPayment: "function" },
4219
4217
  },
4220
- // sc-890 — backs useSendNotification(). Mirror of contract.js.
4218
+ // sc-890 — backs useSendNotification().
4221
4219
  notifications: {
4222
4220
  description:
4223
4221
  "Injected @colixsystems/notifications-client instance (sc-890). { send(body) -> Promise<notification>, mutes: { get, set, clear } }. send backs useSendNotification(); requires the notifications.send:appUser scope. The host POSTs the body snake_case verbatim ({ recipient_user_id, title, body, link?, payload? }) to /notifications/send and returns the created notification row; the call is imperative (a send never fires on mount). mutes backs useRecordMute() and useRecordMutes() (sc-10268): get({ table_id, record_id }) / set({ table_id, record_id, muted_until }) -> { muted, muted_until }, clear({ table_id, record_id }) -> undefined, list({ table_id, limit?, offset? }) -> { data: [{ record_id, muted_until }], meta }. mutes is optional — a host without it (the Builder canvas preview) leaves useRecordMute() reading not muted.",
4224
4222
  required: true,
4225
4223
  fields: { send: "function", mutes: "object" },
4226
4224
  },
4227
- // REQ-IDENT (sc-4313) — backs useIdentification(). Mirror of contract.js.
4225
+ // REQ-IDENT (sc-4313) — backs useIdentification().
4228
4226
  identification: {
4229
4227
  description:
4230
4228
  "Injected @colixsystems/identification-client instance (REQ-IDENT). { available() -> Promise<{ available, providers }>, start(body) -> Promise<order>, get(id) -> Promise<state>, cancel(id) -> Promise<state> }. Backs useIdentification(); no widget scope and no signed-in user required — identifying a NOT-signed-in visitor is the point, so the endpoints are anonymous. Bodies and rows are snake_case verbatim. A completed identification exposes name + personal_number_masked + a stable subject_hash; the RAW personal number is never reachable from a widget (it lives behind a studio-admin endpoint).",
@@ -4265,7 +4263,7 @@ const WIDGET_CONTEXT_SHAPE = {
4265
4263
  error: "function",
4266
4264
  },
4267
4265
  },
4268
- // REQ-WSDK-PLATFORM §6 — backs useToast(). Mirror of contract.js.
4266
+ // REQ-WSDK-PLATFORM §6 — backs useToast().
4269
4267
  toast: {
4270
4268
  description:
4271
4269
  "Optional host toast slot. { showToast({ kind, message }): void }. " +
@@ -4709,7 +4707,7 @@ const VETTED_IMPORTS = [
4709
4707
  // `allowedBareImports` (so the loader's shim⊆vetted guard treats them as
4710
4708
  // vetted) WITHOUT bloating the rich `VETTED_IMPORTS` reference the AI prompt /
4711
4709
  // Developer guide enumerate — the SAME treatment `react/jsx-runtime` gets, but
4712
- // kept out of the author-facing list. Mirror of contract.js.
4710
+ // kept out of the author-facing list.
4713
4711
  const CORE_INFRA_BARE_IMPORTS = ["react-dom", "react-dom/client"];
4714
4712
 
4715
4713
  // Back-compat shape — every existing consumer (widgetLoader, the static
@@ -6379,7 +6377,9 @@ const CONTRACT = deepFreeze({
6379
6377
  // `<testID>-option-<id>`.
6380
6378
  // 1.210.0: behaviour (sc-12245) — `<Avatar url>` falls back to the
6381
6379
  // initials when its picture fails to load. No API change.
6382
- version: "1.210.0",
6380
+ // 1.211.0: additive (sc-9168) — `maxFontSizeMultiplier`: native `<Text>` /
6381
+ // `<TextInput>` follow the device font scale up to this bound.
6382
+ version: "1.211.0",
6383
6383
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6384
6384
  hooks: HOOKS,
6385
6385
  primitives: PRIMITIVES,
@@ -6408,6 +6408,10 @@ const CONTRACT = deepFreeze({
6408
6408
  bundleExportContract: BUNDLE_EXPORT_CONTRACT,
6409
6409
  // sc-10829 — the hook a `badgeCounter: true` entry exports by name.
6410
6410
  badgeCounterExport: "useBadgeCount",
6411
+ // sc-9168 — the most native text grows with the device font-scale setting.
6412
+ // Above it a layout sized against the Player overflows; the browser has no
6413
+ // such setting to preview it with.
6414
+ maxFontSizeMultiplier: 1.3,
6411
6415
  bannedApis: BANNED_APIS,
6412
6416
  vettedImports: VETTED_IMPORTS,
6413
6417
  allowedBareImports: ALLOWED_BARE_IMPORTS,
package/dist/contract.js CHANGED
@@ -1,12 +1,7 @@
1
- // ESM mirror of contract.cjs. Generated from contract.cjs — keep both in lockstep.
2
- // runtime) can `require()` it directly without dynamic import gymnastics.
3
- //
4
- // The ESM `contract.js` re-exports the SAME object as the `CONTRACT` named
5
- // export. The contract test asserts the two are identical (deep-equal) so
6
- // drift between this file and the ESM source-of-truth is caught in CI.
7
- //
8
- // Keep edits in lockstep between `contract.cjs` and `contract.js`. The
9
- // SDK build script copies both into `dist/`.
1
+ // The widget SDK contract: the ONE source the linter, the AI agents, the
2
+ // compiler and both hosts read. `contract.cjs` is GENERATED from `contract.js`
3
+ // (`npm run generate:contract`) for CommonJS consumers — never edit it by hand;
4
+ // contract-cjs-generated.test.js fails when it is stale.
10
5
 
11
6
  // REQ-AI-AGENT-DESIGN (sc-3696): the ONE cross-platform elevation vocabulary.
12
7
  // `shadow*` applies on iOS + react-native-web, `elevation` drives Android.
@@ -2645,7 +2640,7 @@ const HOOKS = [
2645
2640
  scopes: ["users.write:*"],
2646
2641
  },
2647
2642
  // REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in
2648
- // app-user. Self-service + JWT-gated (no widget scope). Mirror of contract.cjs.
2643
+ // app-user. Self-service + JWT-gated (no widget scope).
2649
2644
  {
2650
2645
  name: "useBankIdLink",
2651
2646
  signature: "useBankIdLink()",
@@ -2681,7 +2676,7 @@ const HOOKS = [
2681
2676
  scopes: null,
2682
2677
  },
2683
2678
  // REQ-IDENT (sc-4313) — identify a visitor who is NOT signed in and keep the
2684
- // result. Anonymous by design (no widget scope, no session). Mirror of contract.cjs.
2679
+ // result. Anonymous by design (no widget scope, no session).
2685
2680
  {
2686
2681
  name: "useIdentification",
2687
2682
  signature: "useIdentification(options)",
@@ -2732,7 +2727,7 @@ const HOOKS = [
2732
2727
  scopes: null,
2733
2728
  },
2734
2729
  // REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
2735
- // management for a single record. Mirror of contract.js.
2730
+ // management for a single record.
2736
2731
  {
2737
2732
  name: "useRecordPermissions",
2738
2733
  signature: "useRecordPermissions(tableId, recordId)",
@@ -2865,7 +2860,7 @@ const HOOKS = [
2865
2860
  requiredContextSlice: ["datastore.records"],
2866
2861
  scopes: ["datastore.read:<table>"],
2867
2862
  },
2868
- // REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
2863
+ // REQ-WSDK-PLATFORM §6 — Tier A SDK hooks.
2869
2864
  {
2870
2865
  name: "useClipboard",
2871
2866
  signature: "useClipboard()",
@@ -3701,6 +3696,10 @@ const ACTION_TRIGGER_TYPES = [
3701
3696
  // Globals the action script runs against (the runner's surface) — distinct
3702
3697
  // from the React/SDK widget surface, so the component import/banned-API
3703
3698
  // linter does NOT scan action scripts.
3699
+ // sc-7877 deliberately does NOT add `directory` here. That global is
3700
+ // reachable only by a MARKETPLACE action whose install consented to a
3701
+ // `directory:` scope; a widget-declared action has no install, so
3702
+ // resolveForActionRow grants it nothing and the global is never defined.
3704
3703
  const ACTION_SCRIPT_GLOBALS = [
3705
3704
  "datastore",
3706
3705
  "secrets",
@@ -4213,14 +4212,14 @@ const WIDGET_CONTEXT_SHAPE = {
4213
4212
  required: true,
4214
4213
  fields: { requestPayment: "function", getPayment: "function" },
4215
4214
  },
4216
- // sc-890 — backs useSendNotification(). Mirror of contract.cjs.
4215
+ // sc-890 — backs useSendNotification().
4217
4216
  notifications: {
4218
4217
  description:
4219
4218
  "Injected @colixsystems/notifications-client instance (sc-890). { send(body) -> Promise<notification>, mutes: { get, set, clear } }. send backs useSendNotification(); requires the notifications.send:appUser scope. The host POSTs the body snake_case verbatim ({ recipient_user_id, title, body, link?, payload? }) to /notifications/send and returns the created notification row; the call is imperative (a send never fires on mount). mutes backs useRecordMute() and useRecordMutes() (sc-10268): get({ table_id, record_id }) / set({ table_id, record_id, muted_until }) -> { muted, muted_until }, clear({ table_id, record_id }) -> undefined, list({ table_id, limit?, offset? }) -> { data: [{ record_id, muted_until }], meta }. mutes is optional — a host without it (the Builder canvas preview) leaves useRecordMute() reading not muted.",
4220
4219
  required: true,
4221
4220
  fields: { send: "function", mutes: "object" },
4222
4221
  },
4223
- // REQ-IDENT (sc-4313) — backs useIdentification(). Mirror of contract.cjs.
4222
+ // REQ-IDENT (sc-4313) — backs useIdentification().
4224
4223
  identification: {
4225
4224
  description:
4226
4225
  "Injected @colixsystems/identification-client instance (REQ-IDENT). { available() -> Promise<{ available, providers }>, start(body) -> Promise<order>, get(id) -> Promise<state>, cancel(id) -> Promise<state> }. Backs useIdentification(); no widget scope and no signed-in user required — identifying a NOT-signed-in visitor is the point, so the endpoints are anonymous. Bodies and rows are snake_case verbatim. A completed identification exposes name + personal_number_masked + a stable subject_hash; the RAW personal number is never reachable from a widget (it lives behind a studio-admin endpoint).",
@@ -4261,7 +4260,7 @@ const WIDGET_CONTEXT_SHAPE = {
4261
4260
  error: "function",
4262
4261
  },
4263
4262
  },
4264
- // REQ-WSDK-PLATFORM §6 — backs useToast(). Mirror of contract.js.
4263
+ // REQ-WSDK-PLATFORM §6 — backs useToast().
4265
4264
  toast: {
4266
4265
  description:
4267
4266
  "Optional host toast slot. { showToast({ kind, message }): void }. " +
@@ -4705,7 +4704,7 @@ const VETTED_IMPORTS = [
4705
4704
  // `allowedBareImports` (so the loader's shim⊆vetted guard treats them as
4706
4705
  // vetted) WITHOUT bloating the rich `VETTED_IMPORTS` reference the AI prompt /
4707
4706
  // Developer guide enumerate — the SAME treatment `react/jsx-runtime` gets, but
4708
- // kept out of the author-facing list. Mirror of contract.cjs.
4707
+ // kept out of the author-facing list.
4709
4708
  const CORE_INFRA_BARE_IMPORTS = ["react-dom", "react-dom/client"];
4710
4709
 
4711
4710
  // Back-compat shape — every existing consumer (widgetLoader, the static
@@ -6375,7 +6374,9 @@ const CONTRACT = deepFreeze({
6375
6374
  // `<testID>-option-<id>`.
6376
6375
  // 1.210.0: behaviour (sc-12245) — `<Avatar url>` falls back to the
6377
6376
  // initials when its picture fails to load. No API change.
6378
- version: "1.210.0",
6377
+ // 1.211.0: additive (sc-9168) — `maxFontSizeMultiplier`: native `<Text>` /
6378
+ // `<TextInput>` follow the device font scale up to this bound.
6379
+ version: "1.211.0",
6379
6380
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6380
6381
  hooks: HOOKS,
6381
6382
  primitives: PRIMITIVES,
@@ -6404,6 +6405,10 @@ const CONTRACT = deepFreeze({
6404
6405
  bundleExportContract: BUNDLE_EXPORT_CONTRACT,
6405
6406
  // sc-10829 — the hook a `badgeCounter: true` entry exports by name.
6406
6407
  badgeCounterExport: "useBadgeCount",
6408
+ // sc-9168 — the most native text grows with the device font-scale setting.
6409
+ // Above it a layout sized against the Player overflows; the browser has no
6410
+ // such setting to preview it with.
6411
+ maxFontSizeMultiplier: 1.3,
6407
6412
  bannedApis: BANNED_APIS,
6408
6413
  vettedImports: VETTED_IMPORTS,
6409
6414
  allowedBareImports: ALLOWED_BARE_IMPORTS,
@@ -2,7 +2,7 @@
2
2
  // web list, loading, menu and button parts; see ./primitives.js for why not
3
3
  // the bare specifier.
4
4
 
5
- import * as ReactNative from "react-native-web";
5
+ import { ReactNative } from "./text-scale.js";
6
6
  import { List, ListRow, ListHeader, EmptyState } from "./list.js";
7
7
  import { Skeleton } from "./loading.js";
8
8
  import { Menu } from "./popover.js";
@@ -1,7 +1,8 @@
1
1
  // `<DataTable>` (native). Binds ./data-table-view.js to React Native's own
2
2
  // primitives and the native parts; only the imports differ from ./data-table.js.
3
3
 
4
- import { View, Text, Pressable, ScrollView, Platform, AccessibilityInfo } from "react-native";
4
+ import { View, Pressable, ScrollView, Platform, AccessibilityInfo } from "react-native";
5
+ import { Text } from "./bounded-text.native.js";
5
6
  import { List, ListRow, ListHeader, EmptyState } from "./list.native.js";
6
7
  import { Skeleton } from "./loading.native.js";
7
8
  import { Menu } from "./popover.native.js";
@@ -2,7 +2,8 @@
2
2
  // and the themed calendar sheet; see ./primitives.js for why not the bare
3
3
  // specifier.
4
4
 
5
- import { View, Text, Pressable, ScrollView, Platform } from "react-native-web";
5
+ import { View, Pressable, ScrollView, Platform } from "react-native-web";
6
+ import { Text } from "./text-scale.js";
6
7
  import { makeDateTimePicker } from "./datetimepicker-view.js";
7
8
  import { makeDateTimeSheet } from "./datetimepicker-sheet.js";
8
9
  import { Overlay } from "./overlay.js";
@@ -6,7 +6,8 @@
6
6
  import React from "react";
7
7
  // eslint-disable-next-line no-restricted-syntax
8
8
  import RNDateTimePicker from "@react-native-community/datetimepicker";
9
- import { View, Text, Pressable, Platform } from "react-native";
9
+ import { View, Pressable, Platform } from "react-native";
10
+ import { Text } from "./bounded-text.native.js";
10
11
  import { makeDateTimePicker } from "./datetimepicker-view.js";
11
12
  import { nowParts, partsToDate, valueToParts } from "./datetimepicker-format.js";
12
13
  import { clampToBounds } from "./datetimepicker-calendar.js";
@@ -2,7 +2,7 @@
2
2
  // ./detail-parts-view.js to react-native-web; see ./primitives.js for why not
3
3
  // the bare specifier.
4
4
 
5
- import * as ReactNative from "react-native-web";
5
+ import { ReactNative } from "./text-scale.js";
6
6
  import Svg, { Circle } from "react-native-svg";
7
7
  import { makeDetailParts } from "./detail-parts-view.js";
8
8
 
@@ -2,7 +2,8 @@
2
2
  // ./detail-parts-view.js to React Native; only the imports differ from
3
3
  // ./detail-parts.js.
4
4
 
5
- import { View, Text, Image } from "react-native";
5
+ import { View, Image } from "react-native";
6
+ import { Text } from "./bounded-text.native.js";
6
7
  import Svg, { Circle } from "react-native-svg";
7
8
  import { makeDetailParts } from "./detail-parts-view.js";
8
9
 
@@ -7,7 +7,7 @@
7
7
  // specifier for the rolldown optional-peer-dep reason at the top of
8
8
  // ./primitives.js.
9
9
 
10
- import * as ReactNative from "react-native-web";
10
+ import { ReactNative } from "./text-scale.js";
11
11
  import { makeEmojiPicker } from "./emoji-picker-view.js";
12
12
  import { Overlay } from "./overlay.js";
13
13
  import { TextInput } from "./primitives.js";
@@ -2,7 +2,8 @@
2
2
  // ./emoji-picker-view.js to React Native's own primitives plus the native
3
3
  // `<Overlay>`. Only the imports differ from the web mirror (./emoji-picker.js).
4
4
 
5
- import { View, Text, Pressable, ScrollView, TextInput } from "react-native";
5
+ import { View, Pressable, ScrollView } from "react-native";
6
+ import { Text, TextInput } from "./bounded-text.native.js";
6
7
  import { makeEmojiPicker } from "./emoji-picker-view.js";
7
8
  import { Overlay } from "./overlay.native.js";
8
9
 
@@ -1,7 +1,7 @@
1
1
  // `<FileImage>` (web). Binds ./file-image-view.js to react-native-web and the
2
2
  // web `<Skeleton>`; see ./primitives.js for why not the bare specifier.
3
3
 
4
- import * as ReactNative from "react-native-web";
4
+ import { ReactNative } from "./text-scale.js";
5
5
  import { Skeleton } from "./loading.js";
6
6
  import { makeFileImage } from "./file-image-view.js";
7
7
 
@@ -4,7 +4,8 @@
4
4
  // button, which is the whole control on native.
5
5
 
6
6
  import React from "react";
7
- import { View, Text, Pressable, Platform } from "react-native-web";
7
+ import { View, Pressable, Platform } from "react-native-web";
8
+ import { Text } from "./text-scale.js";
8
9
  import { makeFileUpload } from "./file-upload-view.js";
9
10
  import { FilePicker } from "./filepicker.js";
10
11
  import { Button } from "./button.js";
@@ -2,7 +2,8 @@
2
2
  // native `<FilePicker>` (the OS document picker). No drop target: React Native
3
3
  // has no events for files dragged onto an app, so the button is the control.
4
4
 
5
- import { View, Text, Pressable, Platform } from "react-native";
5
+ import { View, Pressable, Platform } from "react-native";
6
+ import { Text } from "./bounded-text.native.js";
6
7
  import { makeFileUpload } from "./file-upload-view.js";
7
8
  import { FilePicker } from "./filepicker.native.js";
8
9
  import { Button } from "./button.native.js";
@@ -2,7 +2,7 @@
2
2
  // web `<SearchField>`, `<Select>`, `<Chip>`, `<Button>` and `<Modal>`; see
3
3
  // ./primitives.js for why not the bare specifier.
4
4
 
5
- import * as ReactNative from "react-native-web";
5
+ import { ReactNative } from "./text-scale.js";
6
6
  import { makeFilterBar } from "./filter-bar-view.js";
7
7
  import { SearchField } from "./search-field.js";
8
8
  import { Select } from "./select.js";
package/dist/form.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // (web). Binds ./form-view.js to the web primitives and the web `<Grid>`; see
3
3
  // ./primitives.js for why not the bare specifier.
4
4
 
5
- import * as ReactNative from "react-native-web";
5
+ import { ReactNative } from "./text-scale.js";
6
6
  import { makeForm } from "./form-view.js";
7
7
  import { Grid } from "./grid.js";
8
8
 
@@ -2,7 +2,8 @@
2
2
  // (native). Binds ./form-view.js to React Native's own primitives and the
3
3
  // native `<Grid>`; only the imports differ from ./form.js.
4
4
 
5
- import { View, Text, Pressable, Platform } from "react-native";
5
+ import { View, Pressable, Platform } from "react-native";
6
+ import { Text } from "./bounded-text.native.js";
6
7
  import { makeForm } from "./form-view.js";
7
8
  import { Grid } from "./grid.native.js";
8
9
 
package/dist/gradient.js CHANGED
@@ -17,7 +17,7 @@
17
17
  // `gradientAngleToVector` from the contract. Keep the two in lockstep.
18
18
 
19
19
  import React from "react";
20
- import * as ReactNative from "react-native-web";
20
+ import { ReactNative } from "./text-scale.js";
21
21
  import { gradientStops } from "./gradient-stops.js";
22
22
 
23
23
  const DEFAULT_ANGLE = 180;
package/dist/grid.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // `<Grid>` (web). Binds ./grid-view.js to react-native-web; see
2
2
  // ./primitives.js for why not the bare specifier.
3
3
 
4
- import * as ReactNative from "react-native-web";
4
+ import { ReactNative } from "./text-scale.js";
5
5
  import { makeGrid } from "./grid-view.js";
6
6
 
7
7
  export const Grid = makeGrid({ View: ReactNative.View });