@colixsystems/widget-sdk 0.258.0 → 0.260.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
@@ -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.258.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.260.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.260.0 (contract 1.211.0)
91
+
92
+ **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.
93
+
94
+ ### What's new in 0.259.0 (contract 1.210.0)
95
+
96
+ **`<Avatar url>` falls back to the initials** when its picture fails to load, instead of showing a broken image (as `<Avatar fileId>` already did). The built-in form fields for users, groups and related records now use `<PeoplePicker>`, and the User widget's photo uses `<Avatar>`.
89
97
 
90
98
  ### What's new in 0.258.0 (contract 1.209.0)
91
99
 
@@ -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 });
@@ -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");
@@ -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";
@@ -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";
@@ -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 });
@@ -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 });
@@ -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
@@ -6377,7 +6375,11 @@ const CONTRACT = deepFreeze({
6377
6375
  // 1.209.0: additive (sc-12242) — `<PeoplePicker>` takes `activeOnly`
6378
6376
  // (users), a function `loadErrorText(error)`, and gives each option a
6379
6377
  // `<testID>-option-<id>`.
6380
- version: "1.209.0",
6378
+ // 1.210.0: behaviour (sc-12245) — `<Avatar url>` falls back to the
6379
+ // initials when its picture fails to load. No API change.
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",
6381
6383
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6382
6384
  hooks: HOOKS,
6383
6385
  primitives: PRIMITIVES,
@@ -6406,6 +6408,10 @@ const CONTRACT = deepFreeze({
6406
6408
  bundleExportContract: BUNDLE_EXPORT_CONTRACT,
6407
6409
  // sc-10829 — the hook a `badgeCounter: true` entry exports by name.
6408
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,
6409
6415
  bannedApis: BANNED_APIS,
6410
6416
  vettedImports: VETTED_IMPORTS,
6411
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
@@ -6373,7 +6372,11 @@ const CONTRACT = deepFreeze({
6373
6372
  // 1.209.0: additive (sc-12242) — `<PeoplePicker>` takes `activeOnly`
6374
6373
  // (users), a function `loadErrorText(error)`, and gives each option a
6375
6374
  // `<testID>-option-<id>`.
6376
- version: "1.209.0",
6375
+ // 1.210.0: behaviour (sc-12245) — `<Avatar url>` falls back to the
6376
+ // initials when its picture fails to load. No API change.
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",
6377
6380
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6378
6381
  hooks: HOOKS,
6379
6382
  primitives: PRIMITIVES,
@@ -6402,6 +6405,10 @@ const CONTRACT = deepFreeze({
6402
6405
  bundleExportContract: BUNDLE_EXPORT_CONTRACT,
6403
6406
  // sc-10829 — the hook a `badgeCounter: true` entry exports by name.
6404
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,
6405
6412
  bannedApis: BANNED_APIS,
6406
6413
  vettedImports: VETTED_IMPORTS,
6407
6414
  allowedBareImports: ALLOWED_BARE_IMPORTS,
@@ -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";
@@ -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";
@@ -111,6 +111,13 @@ export function makeDetailParts(rn) {
111
111
  );
112
112
  }
113
113
 
114
+ // A photo that cannot load gives way to the initials.
115
+ function UrlAvatar({ photo, monogram }) {
116
+ const [failed, setFailed] = React.useState(false);
117
+ if (failed) return monogram;
118
+ return h(Image, { ...photo, onError: () => setFailed(true) });
119
+ }
120
+
114
121
  function Avatar({ name, url, fileId, seed, size = "md", shape = "circle", accessibilityLabel, style, testID }) {
115
122
  const { colors, radii, typography } = useDetailTheme();
116
123
  const px = avatarPixelSize(size);
@@ -127,15 +134,6 @@ export function makeDetailParts(rn) {
127
134
  const stored = typeof fileId === "string" && fileId !== "";
128
135
  const source = typeof url === "string" ? url.trim() : "";
129
136
  const label = accessibilityLabel || captionText(name) || "Avatar";
130
- if (source && !stored) {
131
- return h(Image, {
132
- source: { uri: source },
133
- accessibilityLabel: label,
134
- resizeMode: "cover",
135
- style: [box, style],
136
- testID,
137
- });
138
- }
139
137
  const initials = initialsFor(name);
140
138
  const fallback = { alignItems: "center", justifyContent: "center" };
141
139
  const content = initials
@@ -152,11 +150,16 @@ export function makeDetailParts(rn) {
152
150
  initials,
153
151
  )
154
152
  : h(Icon, { name: "user", size: Math.round(px * 0.55), color: ink });
153
+ const monogram = h(View, { style: [box, fallback, style], accessibilityLabel: label, testID }, content);
154
+ if (source && !stored) {
155
+ const photo = { source: { uri: source }, accessibilityLabel: label, resizeMode: "cover", style: [box, style], testID };
156
+ return h(UrlAvatar, { key: source, photo, monogram });
157
+ }
155
158
  if (stored) {
156
159
  const frame = { style: [box, fallback, { overflow: "hidden" }, style], accessibilityLabel: label, testID };
157
160
  return h(FileAvatar, { key: fileId, fileId, px, frame, monogram: content });
158
161
  }
159
- return h(View, { style: [box, fallback, style], accessibilityLabel: label, testID }, content);
162
+ return monogram;
160
163
  }
161
164
  Avatar.displayName = "Avatar";
162
165
 
@@ -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
 
@@ -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
 
@@ -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,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/index.d.ts CHANGED
@@ -4151,7 +4151,7 @@ export const Avatar: (props: {
4151
4151
  name?: string;
4152
4152
  /** A stable id (a user id): tints the monogram with that person's colour. */
4153
4153
  seed?: string;
4154
- /** An absolute image URL. */
4154
+ /** An absolute image URL; the initials show if it cannot load. */
4155
4155
  url?: string;
4156
4156
  /**
4157
4157
  * A filestore file id (a FILE column's value). Takes precedence over `url`;
@@ -5868,6 +5868,8 @@ export interface AiWidgetContract {
5868
5868
  readonly bundleExportContract: ReadonlyArray<ContractBundleShape>;
5869
5869
  /** sc-10829 — the export name of a `badgeCounter: true` entry's hook. */
5870
5870
  readonly badgeCounterExport: "useBadgeCount";
5871
+ /** The most native Text / TextInput grow with the device font-scale setting. */
5872
+ readonly maxFontSizeMultiplier: number;
5871
5873
  readonly bannedApis: ReadonlyArray<ContractBannedApi>;
5872
5874
  readonly allowedBareImports: ReadonlyArray<string>;
5873
5875
  /** Bare specifiers the web host provides by exact name; a widget bundle never inlines them. */
package/dist/linter.cjs CHANGED
@@ -4,7 +4,7 @@
4
4
  // The ESM `linter.js` is the source-of-truth. This file MUST mirror its
5
5
  // rule set byte-for-byte — the contract test in packages/widget-sdk
6
6
  // asserts the two are behaviour-equivalent. When you edit one, edit the
7
- // other in the same PR. (Pattern matches `contract.cjs` / `contract.js`.)
7
+ // other in the same PR.
8
8
  //
9
9
  // The SDK build script copies both into `dist/`.
10
10
 
@@ -1,7 +1,8 @@
1
1
  // `<List>`, `<ListRow>`, `<ListHeader>`, `<EmptyState>` (native). Binds
2
2
  // ./list-view.js to React Native's own primitives; only the imports differ from ./list.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 { Card } from "./card.native.js";
6
7
  import { makeList } from "./list-view.js";
7
8
 
@@ -1,7 +1,8 @@
1
1
  // `<Spinner>` and `<Skeleton>` (native). Binds ./loading-view.js to React
2
2
  // Native's own primitives; only the import differs from ./loading.js.
3
3
 
4
- import { View, Text, Animated, Easing, AccessibilityInfo } from "react-native";
4
+ import { View, Animated, Easing, AccessibilityInfo } from "react-native";
5
+ import { Text } from "./bounded-text.native.js";
5
6
  import { makeLoading } from "./loading-view.js";
6
7
 
7
8
  export const { Spinner, Skeleton } = makeLoading(
@@ -2,7 +2,8 @@
2
2
  // ./markdown-input-view.js to React Native's own primitives and the native
3
3
  // `<RichText>`. Only the imports differ from the web mirror.
4
4
 
5
- import { Text, View, Pressable, TextInput, ScrollView } from "react-native";
5
+ import { View, Pressable, ScrollView } from "react-native";
6
+ import { Text, TextInput } from "./bounded-text.native.js";
6
7
  import { makeMarkdownInput } from "./markdown-input-view.js";
7
8
  import { RichText } from "./richtext.native.js";
8
9
  import { Modal } from "./modal.native.js";
@@ -6,12 +6,12 @@
6
6
  import React from "react";
7
7
  import {
8
8
  View,
9
- Text,
10
9
  Pressable,
11
10
  ScrollView,
12
11
  KeyboardAvoidingView,
13
12
  Platform,
14
13
  } from "react-native";
14
+ import { Text } from "./bounded-text.native.js";
15
15
  // Host-provided: the exported app pins it and mounts its provider at the root.
16
16
  import { SafeAreaInsetsContext } from "react-native-safe-area-context";
17
17
  import { makeModal } from "./modal-view.js";
@@ -3,7 +3,8 @@
3
3
  // differ from ./popover.js, plus the safe-area inset a bottom sheet pads by.
4
4
 
5
5
  import React from "react";
6
- import { View, Text, Pressable, ScrollView, Platform, useWindowDimensions } from "react-native";
6
+ import { View, Pressable, ScrollView, Platform, useWindowDimensions } from "react-native";
7
+ import { Text } from "./bounded-text.native.js";
7
8
  // Host-provided: the exported app pins it and mounts its provider at the root.
8
9
  import { SafeAreaInsetsContext } from "react-native-safe-area-context";
9
10
  import { makePopover } from "./popover-view.js";
@@ -7,12 +7,10 @@
7
7
  // `./primitives.js`.
8
8
 
9
9
  export {
10
- Text,
11
10
  View,
12
11
  Pressable,
13
12
  Image,
14
13
  ScrollView,
15
- TextInput,
16
14
  FlatList,
17
15
  SectionList,
18
16
  ActivityIndicator,
@@ -20,6 +18,8 @@ export {
20
18
  StyleSheet,
21
19
  Linking,
22
20
  } from "react-native";
21
+ // sc-9168 — bounded to the device font-scale cap; no web twin (no such setting).
22
+ export { Text, TextInput } from "./bounded-text.native.js";
23
23
 
24
24
  // REQ-WSDK-PLATFORM §6 — `<Icon>` is implemented in one file that runs on
25
25
  // both platforms (lucide-react-native ships a working build for both).
@@ -1,7 +1,8 @@
1
1
  // `<RadioGroup>` (native). Binds ./radio-group-view.js to React Native's own
2
2
  // primitives; only the import differs from ./radio-group.js.
3
3
 
4
- import { View, Text, Pressable, Platform } from "react-native";
4
+ import { View, Pressable, Platform } from "react-native";
5
+ import { Text } from "./bounded-text.native.js";
5
6
  import { makeRadioGroup } from "./radio-group-view.js";
6
7
 
7
8
  export const RadioGroup = makeRadioGroup({ View, Text, Pressable, Platform });
@@ -2,7 +2,8 @@
2
2
  // ./richtext-view.js to React Native's own primitives. Only the import differs
3
3
  // from the web mirror in ./richtext.js.
4
4
 
5
- import { Text, View, Image } from "react-native";
5
+ import { View, Image } from "react-native";
6
+ import { Text } from "./bounded-text.native.js";
6
7
  import { makeRichText } from "./richtext-view.js";
7
8
 
8
9
  export const RichText = makeRichText({ Text, View, Image });
@@ -2,7 +2,8 @@
2
2
  // primitives and the native `<TextField>`; only the imports differ from
3
3
  // ./search-field.js.
4
4
 
5
- import { View, Text, Pressable } from "react-native";
5
+ import { View, Pressable } from "react-native";
6
+ import { Text } from "./bounded-text.native.js";
6
7
  import { makeSearchField } from "./search-field-view.js";
7
8
  import { TextField } from "./text-field.native.js";
8
9
 
@@ -2,7 +2,8 @@
2
2
  // primitives and the native `<Overlay>`, `<Button>`, `<Chip>`,
3
3
  // `<SearchField>` and `<Spinner>`; only the imports differ from ./select.js.
4
4
 
5
- import { View, Text, Pressable, ScrollView, Platform } from "react-native";
5
+ import { View, Pressable, ScrollView, Platform } from "react-native";
6
+ import { Text } from "./bounded-text.native.js";
6
7
  import { makeSelect } from "./select-view.js";
7
8
  import { Overlay } from "./overlay.native.js";
8
9
  import { Button } from "./button.native.js";
@@ -2,7 +2,8 @@
2
2
  // Native's own primitives; only the imports and the handle style differ from
3
3
  // ./sortable.js.
4
4
 
5
- import { View, Text, ScrollView, Animated, PanResponder, Platform, AccessibilityInfo } from "react-native";
5
+ import { View, ScrollView, Animated, PanResponder, Platform, AccessibilityInfo } from "react-native";
6
+ import { Text } from "./bounded-text.native.js";
6
7
  import { List, ListRow } from "./list.native.js";
7
8
  import { Card } from "./card.native.js";
8
9
  import { Menu } from "./popover.native.js";
@@ -26,8 +26,8 @@ import {
26
26
  * derived from an image, so its subtree keeps what it inherited and the scrim
27
27
  * plus an explicit light text colour carries the hero.
28
28
  *
29
- * The web Player calls this; the export's generated layout renderer mirrors
30
- * it — the two hosts must not differ here (CLAUDE.md §8).
29
+ * The web Player and the export's generated layout renderer both call this,
30
+ * so the two hosts cannot differ here (CLAUDE.md §8).
31
31
  */
32
32
  export function surfaceAwareTheme(tenantTheme, backgroundColor) {
33
33
  const surfaceTokens = deriveSurfaceTokens(backgroundColor);
@@ -1,7 +1,8 @@
1
1
  // `<Switch>` (native). Binds ./switch-view.js to React Native's own
2
2
  // primitives; only the import differs from ./switch.js.
3
3
 
4
- import { View, Text, Pressable, Switch as PlatformSwitch } from "react-native";
4
+ import { View, Pressable, Switch as PlatformSwitch } from "react-native";
5
+ import { Text } from "./bounded-text.native.js";
5
6
  import { makeSwitch } from "./switch-view.js";
6
7
 
7
8
  export const Switch = makeSwitch({ View, Text, Pressable, Switch: PlatformSwitch });
@@ -1,7 +1,8 @@
1
1
  // `<Tabs>` (native). Binds ./tabs-view.js to React Native's own primitives;
2
2
  // only the import differs from ./tabs.js.
3
3
 
4
- import { View, Text, Pressable, ScrollView } from "react-native";
4
+ import { View, Pressable, ScrollView } from "react-native";
5
+ import { Text } from "./bounded-text.native.js";
5
6
  import { makeTabs } from "./tabs-view.js";
6
7
 
7
8
  export const Tabs = makeTabs({ View, Text, Pressable, ScrollView });
@@ -1,7 +1,8 @@
1
1
  // `<TextField>` (native). Binds ./text-field-view.js to React Native's own
2
2
  // primitives; only the import differs from ./text-field.js.
3
3
 
4
- import { TextInput, View, Text, Pressable } from "react-native";
4
+ import { View, Pressable } from "react-native";
5
+ import { TextInput, Text } from "./bounded-text.native.js";
5
6
  import { makeTextField } from "./text-field-view.js";
6
7
 
7
8
  export const TextField = makeTextField({ TextInput, View, Text, Pressable });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.258.0",
3
+ "version": "0.260.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",
@@ -56,6 +56,7 @@
56
56
  ],
57
57
  "scripts": {
58
58
  "build": "node scripts/build.js",
59
+ "generate:contract": "node scripts/contract-cjs.js",
59
60
  "test": "node --test \"src/__tests__/*.test.js\""
60
61
  },
61
62
  "engines": {
package/src/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
@@ -6373,7 +6372,11 @@ const CONTRACT = deepFreeze({
6373
6372
  // 1.209.0: additive (sc-12242) — `<PeoplePicker>` takes `activeOnly`
6374
6373
  // (users), a function `loadErrorText(error)`, and gives each option a
6375
6374
  // `<testID>-option-<id>`.
6376
- version: "1.209.0",
6375
+ // 1.210.0: behaviour (sc-12245) — `<Avatar url>` falls back to the
6376
+ // initials when its picture fails to load. No API change.
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",
6377
6380
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6378
6381
  hooks: HOOKS,
6379
6382
  primitives: PRIMITIVES,
@@ -6402,6 +6405,10 @@ const CONTRACT = deepFreeze({
6402
6405
  bundleExportContract: BUNDLE_EXPORT_CONTRACT,
6403
6406
  // sc-10829 — the hook a `badgeCounter: true` entry exports by name.
6404
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,
6405
6412
  bannedApis: BANNED_APIS,
6406
6413
  vettedImports: VETTED_IMPORTS,
6407
6414
  allowedBareImports: ALLOWED_BARE_IMPORTS,