@colixsystems/widget-sdk 0.235.0 → 0.237.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
@@ -84,7 +84,17 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
84
84
 
85
85
  ## Status
86
86
 
87
- `v0.235.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**.
87
+ `v0.237.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
+
89
+ ### What's new in 0.237.0 (contract 1.191.0)
90
+
91
+ **Search boxes have a look of their own.** A new `search` scope on `CONTRACT.themeComponents` (Design > Components > Search fields) restyles every `<SearchField>`: `iconPosition` (`"start"` | `"end"` | `"none"`), `radius` (0–48, so a pill), `background`, `borderColor` and `height`. Each token falls back to the input look (`muted`, Design > Components > Inputs) when unset, so with none set every search box looks exactly as before. `<SearchField iconPosition>` overrides the theme for one field; any `TextField` look prop you pass still wins over the theme. At the end, the clear control takes the icon's place while the field has text, so the two never overlap. Leave the look to the theme: do not pass your own radius, background, border or height, and never add a magnifier icon yourself. `searchScopeDefaults(theme)` on `@colixsystems/widget-sdk/host` gives the Studio the input look each unset token keeps.
92
+
93
+ **Clearer section names.** Design > Components now names the scopes for what they restyle: `muted` is **Inputs**, `popover` **Dropdown menus**, `destructive` **Danger buttons**, `ring` **Button outline** (a permanent border on the Button widget, not a focus style) and `accent` **Labels**. Only the labels changed; the scope keys are the same.
94
+
95
+ ### What's new in 0.236.0 (contract 1.190.0)
96
+
97
+ **Host sign-in screen: the heading and subheading can be hidden.** `resolveLoginTextVisibility(login)` answers whether the built-in sign-in card shows its heading and its subheading: each is `false` only when the author set `login.hideHeading` / `login.hideSubheading` to `true`, so a theme saved before this keeps both. A hidden subheading hides only the sign-in line; the "Create your account" and "Reset your password" step lines still show. It is kept apart from `normaliseLoginDesign` because hidden text is not an authored backdrop. Widgets are unaffected: this is the host surface (`@colixsystems/widget-sdk/host`), and the widget contract version does not change.
88
98
 
89
99
  ### What's new in 0.235.0 (contract 1.190.0)
90
100
 
@@ -1186,7 +1196,7 @@ A binding may now carry an opacity: `"theme:colors.primary/20"` is the primary b
1186
1196
  ```
1187
1197
 
1188
1198
  - **`<Card>`** takes the app's card look (Theme Settings > Cards) with no props. Override one card with `background`, `borderColor`, `radius`, `padding`, `shadow` or `gradient`, or pass `style={props.style}` when your widget declares the `card*` style fields. `as={Pressable}` makes the whole card pressable.
1189
- - **`<Button>`** follows Theme Settings > Buttons (and Destructive / Focus ring). `variant` is `primary` | `secondary` | `outlined` | `danger`; `icon` / `iconPosition` add a lucide glyph; omit `onPress` for a visual inside something pressable; `fields` overrides one button's surface.
1199
+ - **`<Button>`** follows Design > Components > Buttons (and Danger buttons / Button outline). `variant` is `primary` | `secondary` | `outlined` | `danger`; `icon` / `iconPosition` add a lucide glyph; omit `onPress` for a visual inside something pressable; `fields` overrides one button's surface.
1190
1200
  - **`<TextField>`** is a `TextInput` in the app's input look (Theme Settings > Muted, which gains a **`height`** token); `multiline` grows to a ceiling, then scrolls.
1191
1201
 
1192
1202
  The card and button resolvers (`cardOverlay`, `shadowStyle`, `marginStyle`, `buttonSurfaceOverlay`, …) are exported too. Additive: the built-in widgets render exactly as before.
@@ -2916,11 +2926,11 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
2916
2926
  - `isSafeMarkdownImageSrc`, `isHttpImageSrc`, `normaliseMarkdownImageSize`, `readMarkdownAlt` — the image half of that grammar, for a widget rendering images itself via `renderImage`. `isSafeMarkdownImageSrc` is the same allowlist the parser applies (an `http(s)` URL or a filestore id; a `javascript:` scheme is refused), so the widget and the parser cannot disagree about which src is renderable.
2917
2927
  - `List`, `ListRow`, `ListHeader`, `EmptyState` — the themed list building blocks. `<ListRow title subtitle leading trailing onPress selected divider>` draws one record row from the theme's `list` scope (padding, height, gap, radius, background, divider); `<List>` spaces its rows and hands overrides down; `<ListHeader>` labels a table's columns on the rows' own inset; `<EmptyState icon title message>` is the "nothing here" panel. Never hand-roll a row or a header row from `View` + `Pressable`.
2918
2928
  - `Badge`, `Avatar`, `Progress`, `Stat`, `Divider` — the themed status pill, person picture, completion bar or ring, headline figure and rule the built-in widgets of the same names render through. Reach for them inside a record row instead of hand-rolling a pill or a circle from `View`.
2919
- - `Card`, `Button`, `TextField` — the themed card surface, button and text field the built-in widgets render through. With no props each takes the app's look from Theme Settings (Cards, Buttons, Muted); never hand-roll one from `View` / `Pressable` / a bare `TextInput`.
2929
+ - `Card`, `Button`, `TextField` — the themed card surface, button and text field the built-in widgets render through. With no props each takes the app's look from Design > Components (Cards, Buttons, Inputs); never hand-roll one from `View` / `Pressable` / a bare `TextInput`.
2920
2930
  - `Banner` — the themed inline message. `<Banner tone message>` shows an error, warning, success or notice in place, styled by the theme's `alert` scope; `action` adds one button and `onDismiss` a close control. Use `useToast()` for a brief confirmation instead. Never hand-roll a tinted error box.
2921
2931
  - `Accordion`, `AccordionItem` — themed collapsible sections. `<Accordion><AccordionItem title subtitle trailing>…</AccordionItem></Accordion>` draws its surface from the theme's `card` scope and its headers and dividers from the `list` scope; one section open at a time, or `multiple`. Never hand-roll a collapsible from `Pressable` + a chevron.
2922
2932
  - `Spinner`, `Skeleton` — the themed loading states. `<Spinner size label>` for a pending action; `<Skeleton shape="text" | "block" | "circle" | "listRow">` (or a group of bones) for content whose layout you know. The spinner wears `colors.loader`, the skeleton follows the theme's `loading` scope; both stop under reduced motion and mark one busy region. Never use `ActivityIndicator` or hand-build placeholder bars.
2923
- - `Checkbox`, `Switch`, `Select`, `SearchField` — the themed tick box, on/off toggle, single-choice picker and search box. Like `TextField` they follow the theme's `muted` scope (and `popover` for the Select's option sheet) and take `label`, `helper`, `error` and `disabled`. `Select` opens its options at screen level. Never hand-roll one from `Pressable` and `View`.
2933
+ - `Checkbox`, `Switch`, `Select`, `SearchField` — the themed tick box, on/off toggle, single-choice picker and search box. Like `TextField` they follow the theme's `muted` scope, Design > Components > Inputs (and `popover`, Dropdown menus, for the Select's option sheet; `SearchField` also reads `search`, Search fields, for its glyph position, corner radius, background, border colour and height, each falling back to Inputs) and take `label`, `helper`, `error` and `disabled`. `Select` opens its options at screen level. Never hand-roll one from `Pressable` and `View`.
2924
2934
  - `Tabs` — the themed tab strip. `<Tabs items value onChange>` draws a segmented control, pills or an underline as the theme's `tabs` scope says; render the selected panel yourself. Never hand-roll tabs from `Pressable` chips.
2925
2935
  - `Chip`, `ChipGroup` — the themed filter chip, tag or pill. `<Chip label selected onPress onRemove disabled>` follows the theme's `chip` scope; `<ChipGroup>` spaces and wraps its chips and, with `selection="single"`, announces them as one choice. Never hand-roll a chip from `Pressable` + a pill radius.
2926
2936
  - `textRoleStyle(role, typography, colors)`, `TEXT_ROLES` — the style of a semantic text tier, shared with the built-in widgets.
package/dist/contract.cjs CHANGED
@@ -275,6 +275,9 @@ function formatThemeColorTokenRef(path, percent) {
275
275
  // The looks a `tabs.style` token may name; `<Tabs>` draws each.
276
276
  const THEME_TAB_STYLES = Object.freeze(["segmented", "pill", "underline"]);
277
277
 
278
+ // Where `<SearchField>` draws its magnifier; "start" is the look it always had.
279
+ const THEME_SEARCH_ICON_POSITIONS = Object.freeze(["start", "end", "none"]);
280
+
278
281
  const THEME_COMPONENT_SHADOWS = Object.freeze(["none", "sm", "md", "lg"]);
279
282
 
280
283
  // sc-3857 — the `textTransform` token's closed enum, matching the per-instance
@@ -585,7 +588,7 @@ const THEME_COMPONENTS = Object.freeze({
585
588
  // than its `color` field, which the `text` scope already owns -- two
586
589
  // scopes writing the same field on the same widget would race.
587
590
  accent: Object.freeze({
588
- label: "Accent",
591
+ label: "Labels",
589
592
  tokens: Object.freeze({
590
593
  background: Object.freeze({ type: "color", uiDefault: "colors.primarySoft" }),
591
594
  borderColor: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
@@ -602,7 +605,7 @@ const THEME_COMPONENTS = Object.freeze({
602
605
  // sc-5392 -- the Button widget's danger/delete variant. Separate token set
603
606
  // from `button` above so "make the buttons coral" never recolours Delete.
604
607
  destructive: Object.freeze({
605
- label: "Destructive",
608
+ label: "Danger buttons",
606
609
  tokens: Object.freeze({
607
610
  background: Object.freeze({ type: "color", uiDefault: "colors.danger" }),
608
611
  textColor: Object.freeze({ type: "color", uiDefault: "colors.onPrimary" }),
@@ -617,7 +620,7 @@ const THEME_COMPONENTS = Object.freeze({
617
620
  // scope) rather than to Text/Label/Data Value, whose `color` field the
618
621
  // `text` scope already owns.
619
622
  muted: Object.freeze({
620
- label: "Muted",
623
+ label: "Inputs",
621
624
  // The SDK's form controls read this scope directly, in every widget.
622
625
  sdkComponents: Object.freeze(["TextField", "Checkbox", "Switch", "Select", "SearchField"]),
623
626
  tokens: Object.freeze({
@@ -636,7 +639,7 @@ const THEME_COMPONENTS = Object.freeze({
636
639
  // sc-5392 -- dropdown/menu surfaces. The form widgets' choice-field option
637
640
  // list is the closest thing the built-in catalog has to a popover panel.
638
641
  popover: Object.freeze({
639
- label: "Popover",
642
+ label: "Dropdown menus",
640
643
  sdkComponents: Object.freeze(["Select"]),
641
644
  tokens: Object.freeze({
642
645
  background: Object.freeze({ type: "color", uiDefault: "colors.surface" }),
@@ -648,11 +651,26 @@ const THEME_COMPONENTS = Object.freeze({
648
651
  "appstudio.form-builder": POPOVER_FIELDS,
649
652
  }),
650
653
  }),
651
- // sc-5392 -- the app-wide focus-visible outline. A themed emphasis border
654
+ // Search boxes: `<SearchField>` reads this scope directly. Every token is
655
+ // unset by default and falls back to the `muted` field it draws.
656
+ search: Object.freeze({
657
+ label: "Search fields",
658
+ sdkComponents: Object.freeze(["SearchField"]),
659
+ tokens: Object.freeze({
660
+ iconPosition: Object.freeze({ type: "enum", values: THEME_SEARCH_ICON_POSITIONS }),
661
+ // 48 is past half the tallest field, so its ceiling draws a pill.
662
+ radius: Object.freeze({ type: "size", min: 0, max: 48 }),
663
+ background: Object.freeze({ type: "color" }),
664
+ borderColor: Object.freeze({ type: "color" }),
665
+ height: Object.freeze({ type: "size", min: 32, max: 72 }),
666
+ }),
667
+ targets: Object.freeze({}),
668
+ }),
669
+ // sc-5392 -- not a keyboard focus style: a permanent emphasis border
652
670
  // on the Button widget, applied after its variant colours and beaten by the
653
671
  // widget's own per-instance `borderColor` (REQ-THEME-13 stays final).
654
672
  ring: Object.freeze({
655
- label: "Focus ring",
673
+ label: "Button outline",
656
674
  tokens: Object.freeze({
657
675
  color: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
658
676
  width: Object.freeze({ type: "size", min: 0, max: 8 }),
@@ -3093,7 +3111,7 @@ const PRIMITIVES = [
3093
3111
  {
3094
3112
  name: "Switch",
3095
3113
  description:
3096
- 'Themed on/off toggle. `<Switch label="Email me updates" value={on} onValueChange={setOn} />` — THE way to draw a setting that takes effect at once. It is the platform\'s own switch, so each host gives it its switch role, state and keys; its "on" track is the app\'s primary colour and its "off" track the input border (Design > Components > Muted). `label` draws the text beside it and makes the text tappable too, `helper` a hint below, `error` a message in its place (or `true` to mark it without one), `disabled` stops it (a device greys it; on the web it is drawn at half strength unless you passed your own colours). Always pass `label`, or `accessibilityLabel` when a visible label already sits next to it. Controlled — `value` + `onValueChange(next)`. Every React Native `Switch` prop still works: a `trackColor` you pass overrides the theme for the states it names, and `thumbColor` colours the thumb. Never hand-roll a toggle from `Pressable` + a sliding `View`.',
3114
+ 'Themed on/off toggle. `<Switch label="Email me updates" value={on} onValueChange={setOn} />` — THE way to draw a setting that takes effect at once. It is the platform\'s own switch, so each host gives it its switch role, state and keys; its "on" track is the app\'s primary colour and its "off" track the input border (Design > Components > Inputs). `label` draws the text beside it and makes the text tappable too, `helper` a hint below, `error` a message in its place (or `true` to mark it without one), `disabled` stops it (a device greys it; on the web it is drawn at half strength unless you passed your own colours). Always pass `label`, or `accessibilityLabel` when a visible label already sits next to it. Controlled — `value` + `onValueChange(next)`. Every React Native `Switch` prop still works: a `trackColor` you pass overrides the theme for the states it names, and `thumbColor` colours the thumb. Never hand-roll a toggle from `Pressable` + a sliding `View`.',
3097
3115
  rnComponent: "Switch",
3098
3116
  docsUrl: "https://reactnative.dev/docs/switch",
3099
3117
  },
@@ -3202,7 +3220,7 @@ const PRIMITIVES = [
3202
3220
  {
3203
3221
  name: "ConfirmDialog",
3204
3222
  description:
3205
- 'Standard confirmation prompt, a preset of `<Modal>`. `<ConfirmDialog visible={!!pending} tone="destructive" title="Delete this order?" message="This cannot be undone." confirmLabel={t("delete", "Delete")} onCancel={() => setPending(null)} onConfirm={async () => { await remove(pending.id); setPending(null); }} />`. THE way to ask "are you sure?" before a delete or any other action that cannot be undone, so every confirmation in the app has the same wording slots, button order and colours. Never hand-build one from a `<Modal>` or an `<Overlay>` with your own two buttons, never arm a button to be pressed twice, and never use `Alert.alert` or `window.confirm`. Props: `visible` (boolean), `title` (string, the question), `message` (what confirming does; text or a node), `confirmLabel` and `cancelLabel` (default the app\'s translated "Confirm" and "Cancel" — name the action: "Delete", "Remove", "Send"), `tone` ("default" | "destructive"; destructive draws the confirm button from the theme\'s Destructive colours and opens with focus on Cancel), `busy` (boolean; disables both buttons, shows a `<Spinner>` and keeps the dialog open), `onConfirm` (return the promise of the action and the dialog stays busy until it settles, with a second press ignored; a rejection is NOT caught, so catch a failure yourself and report it), `onCancel` (fired by the Cancel button, the close button, the backdrop press, the Escape key on web and the Android back button — cancelling is always the safe default), `closeAccessibilityLabel` and `testID`. The dialog never closes itself: set `visible` false from `onCancel` and after the action. Its panel follows the theme (Design > Components > Dialogs) and its buttons the Buttons and Destructive settings — do not re-style it.',
3223
+ 'Standard confirmation prompt, a preset of `<Modal>`. `<ConfirmDialog visible={!!pending} tone="destructive" title="Delete this order?" message="This cannot be undone." confirmLabel={t("delete", "Delete")} onCancel={() => setPending(null)} onConfirm={async () => { await remove(pending.id); setPending(null); }} />`. THE way to ask "are you sure?" before a delete or any other action that cannot be undone, so every confirmation in the app has the same wording slots, button order and colours. Never hand-build one from a `<Modal>` or an `<Overlay>` with your own two buttons, never arm a button to be pressed twice, and never use `Alert.alert` or `window.confirm`. Props: `visible` (boolean), `title` (string, the question), `message` (what confirming does; text or a node), `confirmLabel` and `cancelLabel` (default the app\'s translated "Confirm" and "Cancel" — name the action: "Delete", "Remove", "Send"), `tone` ("default" | "destructive"; destructive draws the confirm button from the theme\'s Danger buttons colours and opens with focus on Cancel), `busy` (boolean; disables both buttons, shows a `<Spinner>` and keeps the dialog open), `onConfirm` (return the promise of the action and the dialog stays busy until it settles, with a second press ignored; a rejection is NOT caught, so catch a failure yourself and report it), `onCancel` (fired by the Cancel button, the close button, the backdrop press, the Escape key on web and the Android back button — cancelling is always the safe default), `closeAccessibilityLabel` and `testID`. The dialog never closes itself: set `visible` false from `onCancel` and after the action. Its panel follows the theme (Design > Components > Dialogs) and its buttons the Buttons and Destructive settings — do not re-style it.',
3206
3224
  rnComponent: null,
3207
3225
  docsUrl: null,
3208
3226
  },
@@ -3349,35 +3367,35 @@ const PRIMITIVES = [
3349
3367
  {
3350
3368
  name: "Button",
3351
3369
  description:
3352
- 'Themed button. `<Button title="Save" onPress={save} />` — THE way to draw a button: its fill, label colour, border, corner radius, padding, weight, shadow and gradient come from the app\'s button look (Theme Settings > Buttons, and Destructive / Focus ring for the danger variant and emphasis border), so it matches the built-in Button widget. Props: `title`, `onPress` (omit it for a purely visual button inside something else that is pressable), `variant` ("primary" default | "secondary" | "outlined" | "danger"), `icon` (a lucide name) with `iconPosition` ("leading" | "trailing"), `disabled`, `accessibilityLabel`, `testID`, `style` (extra container style, e.g. `alignSelf`) and `labelStyle`. Pass `fields` — `{ background, textColor, borderColor, borderWidth, paddingX, paddingY, radius, fontWeight, fontSize, shadow, gradient }` — to override one button, e.g. from style fields your widget declares. On a section that paints its own fill (`theme.colors.backdrop` there), a variant colour that the section makes unreadable takes that section\'s text colour, as its text does: an outlined label and border, a secondary whose fill disappears becomes outlined, and a primary or danger fill turns inverse (that text colour as its fill, its own colour as its label). A white or lightly tinted panel changes nothing, and a button inside a `<Card>` is measured against that card. A colour you pass in `fields`, `style` or `labelStyle`, or that the app\'s button look sets, is never changed. Never hand-roll a button from `Pressable` + background + radius.',
3370
+ 'Themed button. `<Button title="Save" onPress={save} />` — THE way to draw a button: its fill, label colour, border, corner radius, padding, weight, shadow and gradient come from the app\'s button look (Design > Components > Buttons, and Danger buttons / Button outline for the danger variant and emphasis border), so it matches the built-in Button widget. Props: `title`, `onPress` (omit it for a purely visual button inside something else that is pressable), `variant` ("primary" default | "secondary" | "outlined" | "danger"), `icon` (a lucide name) with `iconPosition` ("leading" | "trailing"), `disabled`, `accessibilityLabel`, `testID`, `style` (extra container style, e.g. `alignSelf`) and `labelStyle`. Pass `fields` — `{ background, textColor, borderColor, borderWidth, paddingX, paddingY, radius, fontWeight, fontSize, shadow, gradient }` — to override one button, e.g. from style fields your widget declares. On a section that paints its own fill (`theme.colors.backdrop` there), a variant colour that the section makes unreadable takes that section\'s text colour, as its text does: an outlined label and border, a secondary whose fill disappears becomes outlined, and a primary or danger fill turns inverse (that text colour as its fill, its own colour as its label). A white or lightly tinted panel changes nothing, and a button inside a `<Card>` is measured against that card. A colour you pass in `fields`, `style` or `labelStyle`, or that the app\'s button look sets, is never changed. Never hand-roll a button from `Pressable` + background + radius.',
3353
3371
  rnComponent: null,
3354
3372
  docsUrl: null,
3355
3373
  },
3356
3374
  {
3357
3375
  name: "TextField",
3358
3376
  description:
3359
- 'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works, and its background, text colour, border, corner radius and height come from the app\'s input look (Theme Settings > Muted), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. `label` draws a label above the field and names it for a screen reader, `helper` a hint below, `error` a message in the helper\'s place with a danger border (`true` marks it without a message), `disabled` makes it read-only and dimmed; `containerStyle` styles the box around a labelled field. Use it instead of styling a bare `TextInput`.',
3377
+ 'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works, and its background, text colour, border, corner radius and height come from the app\'s input look (Design > Components > Inputs), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. `label` draws a label above the field and names it for a screen reader, `helper` a hint below, `error` a message in the helper\'s place with a danger border (`true` marks it without a message), `disabled` makes it read-only and dimmed; `containerStyle` styles the box around a labelled field. Use it instead of styling a bare `TextInput`.',
3360
3378
  rnComponent: null,
3361
3379
  docsUrl: null,
3362
3380
  },
3363
3381
  {
3364
3382
  name: "Checkbox",
3365
3383
  description:
3366
- 'Themed tick box. `<Checkbox label="Remember me" value={checked} onValueChange={setChecked} />` — THE way to draw a yes/no choice that is submitted with a form, or one row of a multi-select list. The box is the app\'s input surface (Design > Components > Muted) and fills with the primary colour when checked; focus draws a ring in the primary colour around it. The whole row is one 44px target with the checkbox role and checked state on both hosts, and Space toggles it on the web. Props: `value` (boolean), `onValueChange(next)`, `label` (text, or an element), `helper`, `error` (a message, or `true`), `disabled`, `accessibilityLabel` (required when there is no text `label`), `style` (the row), `containerStyle` (the box around a row that shows a helper or error), `testID`; any other prop lands on the row. Never hand-roll a tick box from `Pressable` + a bordered `View` + a "✓".',
3384
+ 'Themed tick box. `<Checkbox label="Remember me" value={checked} onValueChange={setChecked} />` — THE way to draw a yes/no choice that is submitted with a form, or one row of a multi-select list. The box is the app\'s input surface (Design > Components > Inputs) and fills with the primary colour when checked; focus draws a ring in the primary colour around it. The whole row is one 44px target with the checkbox role and checked state on both hosts, and Space toggles it on the web. Props: `value` (boolean), `onValueChange(next)`, `label` (text, or an element), `helper`, `error` (a message, or `true`), `disabled`, `accessibilityLabel` (required when there is no text `label`), `style` (the row), `containerStyle` (the box around a row that shows a helper or error), `testID`; any other prop lands on the row. Never hand-roll a tick box from `Pressable` + a bordered `View` + a "✓".',
3367
3385
  rnComponent: null,
3368
3386
  docsUrl: null,
3369
3387
  },
3370
3388
  {
3371
3389
  name: "Select",
3372
3390
  description:
3373
- 'Themed single-choice picker. `<Select label="Status" value={status} onValueChange={setStatus} options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} placeholder="Choose a status" />` — THE way to let the user pick ONE of a known list. The trigger is the app\'s input field (Design > Components > Muted) showing the chosen label; pressing it opens the options at SCREEN level, in a sheet painted from Design > Components > Popover, so no card or scroll container can clip them, and a long list scrolls inside the sheet. The trigger is a button that opens a dialog (expanded state included) and each option carries the option role and its selected state on both hosts; the list around them is a listbox on the web only. On the web Enter, Space or an arrow key opens it, the arrow keys, Home and End move between options, Enter or Space chooses, Escape closes, and focus returns to the trigger; on Android the back button closes it. Props: `options` (`[{ value, label, disabled?, testID? }]`, `value` a string or number, unique), `value`, `onValueChange(value, option)`, `placeholder` (default "Select"), `title` (the sheet\'s heading; defaults to a text `label`), `emptyText` (default "No options"), `label`, `helper`, `error`, `disabled`, `accessibilityLabel`, `closeAccessibilityLabel`, `background` / `textColor` / `borderColor` / `radius` / `height` to override one field, `style` (the trigger), `containerStyle`, `testID`; any other prop lands on the trigger. Never hand-roll a dropdown from `Pressable` + an absolutely positioned list: it is clipped by the widget\'s container on both hosts.',
3391
+ 'Themed single-choice picker. `<Select label="Status" value={status} onValueChange={setStatus} options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} placeholder="Choose a status" />` — THE way to let the user pick ONE of a known list. The trigger is the app\'s input field (Design > Components > Inputs) showing the chosen label; pressing it opens the options at SCREEN level, in a sheet painted from Design > Components > Dropdown menus, so no card or scroll container can clip them, and a long list scrolls inside the sheet. The trigger is a button that opens a dialog (expanded state included) and each option carries the option role and its selected state on both hosts; the list around them is a listbox on the web only. On the web Enter, Space or an arrow key opens it, the arrow keys, Home and End move between options, Enter or Space chooses, Escape closes, and focus returns to the trigger; on Android the back button closes it. Props: `options` (`[{ value, label, disabled?, testID? }]`, `value` a string or number, unique), `value`, `onValueChange(value, option)`, `placeholder` (default "Select"), `title` (the sheet\'s heading; defaults to a text `label`), `emptyText` (default "No options"), `label`, `helper`, `error`, `disabled`, `accessibilityLabel`, `closeAccessibilityLabel`, `background` / `textColor` / `borderColor` / `radius` / `height` to override one field, `style` (the trigger), `containerStyle`, `testID`; any other prop lands on the trigger. Never hand-roll a dropdown from `Pressable` + an absolutely positioned list: it is clipped by the widget\'s container on both hosts.',
3374
3392
  rnComponent: null,
3375
3393
  docsUrl: null,
3376
3394
  },
3377
3395
  {
3378
3396
  name: "SearchField",
3379
3397
  description:
3380
- 'Themed search box. `<SearchField value={query} onChangeText={setQuery} placeholder="Search orders" />` — THE way to draw a field that filters or searches. It IS a `TextField` (same look, same props, same `label` / `helper` / `error` / `disabled`) with a search glyph, the search key on the on-screen keyboard and a clear control that appears once there is text; clearing calls `onChangeText("")`, then `onClear` if you passed one, and keeps focus in the field. `placeholder` defaults to "Search" and `clearAccessibilityLabel` to "Clear search" — pass translated ones. `style` styles the input and `containerStyle` the box around it (use it for `flex: 1` in a toolbar). Debounce the query yourself before you fetch. Never hand-roll a search box from a `TextInput` + an icon.',
3398
+ 'Themed search box. `<SearchField value={query} onChangeText={setQuery} placeholder="Search orders" />` — THE way to draw a field that filters or searches. It IS a `TextField` (same props, same `label` / `helper` / `error` / `disabled`, the input look restyled by Design > Components > Search fields) with a search glyph, the search key on the on-screen keyboard and a clear control that appears once there is text; clearing calls `onChangeText("")`, then `onClear` if you passed one, and keeps focus in the field. `placeholder` defaults to "Search" and `clearAccessibilityLabel` to "Clear search" — pass translated ones. `style` styles the input and `containerStyle` the box around it (use it for `flex: 1` in a toolbar). Where the glyph sits (start, end or hidden), the corner radius, background, border colour and height come from the app; `iconPosition` ("start" | "end" | "none") overrides the glyph for one field, and at the end the clear control takes the glyph\'s place while there is text. Debounce the query yourself before you fetch. Never hand-roll a search box from a `TextInput` + an icon, and never add a magnifier of your own.',
3381
3399
  rnComponent: null,
3382
3400
  docsUrl: null,
3383
3401
  },
@@ -6053,7 +6071,12 @@ const CONTRACT = deepFreeze({
6053
6071
  // calendar sheet instead of the browser's date popup; both hosts show
6054
6072
  // the value in the app's language with a clear button. New optional
6055
6073
  // props: placeholder, clearLabel, doneLabel, closeAccessibilityLabel, testID.
6056
- version: "1.190.0",
6074
+ // 1.191.0: additive (sc-11759) — `themeComponents.search` styles every
6075
+ // `<SearchField>`: icon position, corner radius, background, border
6076
+ // colour, height, each falling back to the `muted` field. The field
6077
+ // takes an `iconPosition` prop. Scope labels renamed (sc-11763); keys
6078
+ // unchanged.
6079
+ version: "1.191.0",
6057
6080
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6058
6081
  hooks: HOOKS,
6059
6082
  primitives: PRIMITIVES,
@@ -6069,6 +6092,7 @@ const CONTRACT = deepFreeze({
6069
6092
  themeColorTokens: THEME_COLOR_TOKENS,
6070
6093
  themeComponents: THEME_COMPONENTS,
6071
6094
  themeComponentTabStyles: THEME_TAB_STYLES,
6095
+ themeComponentSearchIconPositions: THEME_SEARCH_ICON_POSITIONS,
6072
6096
  themeComponentShadows: THEME_COMPONENT_SHADOWS,
6073
6097
  themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
6074
6098
  themeComponentGradient: THEME_COMPONENT_GRADIENT,
package/dist/contract.js CHANGED
@@ -275,6 +275,9 @@ function formatThemeColorTokenRef(path, percent) {
275
275
  // The looks a `tabs.style` token may name; `<Tabs>` draws each.
276
276
  const THEME_TAB_STYLES = Object.freeze(["segmented", "pill", "underline"]);
277
277
 
278
+ // Where `<SearchField>` draws its magnifier; "start" is the look it always had.
279
+ const THEME_SEARCH_ICON_POSITIONS = Object.freeze(["start", "end", "none"]);
280
+
278
281
  const THEME_COMPONENT_SHADOWS = Object.freeze(["none", "sm", "md", "lg"]);
279
282
 
280
283
  // sc-3857 — the `textTransform` token's closed enum, matching the per-instance
@@ -585,7 +588,7 @@ const THEME_COMPONENTS = Object.freeze({
585
588
  // than its `color` field, which the `text` scope already owns -- two
586
589
  // scopes writing the same field on the same widget would race.
587
590
  accent: Object.freeze({
588
- label: "Accent",
591
+ label: "Labels",
589
592
  tokens: Object.freeze({
590
593
  background: Object.freeze({ type: "color", uiDefault: "colors.primarySoft" }),
591
594
  borderColor: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
@@ -602,7 +605,7 @@ const THEME_COMPONENTS = Object.freeze({
602
605
  // sc-5392 -- the Button widget's danger/delete variant. Separate token set
603
606
  // from `button` above so "make the buttons coral" never recolours Delete.
604
607
  destructive: Object.freeze({
605
- label: "Destructive",
608
+ label: "Danger buttons",
606
609
  tokens: Object.freeze({
607
610
  background: Object.freeze({ type: "color", uiDefault: "colors.danger" }),
608
611
  textColor: Object.freeze({ type: "color", uiDefault: "colors.onPrimary" }),
@@ -617,7 +620,7 @@ const THEME_COMPONENTS = Object.freeze({
617
620
  // scope) rather than to Text/Label/Data Value, whose `color` field the
618
621
  // `text` scope already owns.
619
622
  muted: Object.freeze({
620
- label: "Muted",
623
+ label: "Inputs",
621
624
  // The SDK's form controls read this scope directly, in every widget.
622
625
  sdkComponents: Object.freeze(["TextField", "Checkbox", "Switch", "Select", "SearchField"]),
623
626
  tokens: Object.freeze({
@@ -636,7 +639,7 @@ const THEME_COMPONENTS = Object.freeze({
636
639
  // sc-5392 -- dropdown/menu surfaces. The form widgets' choice-field option
637
640
  // list is the closest thing the built-in catalog has to a popover panel.
638
641
  popover: Object.freeze({
639
- label: "Popover",
642
+ label: "Dropdown menus",
640
643
  sdkComponents: Object.freeze(["Select"]),
641
644
  tokens: Object.freeze({
642
645
  background: Object.freeze({ type: "color", uiDefault: "colors.surface" }),
@@ -648,11 +651,26 @@ const THEME_COMPONENTS = Object.freeze({
648
651
  "appstudio.form-builder": POPOVER_FIELDS,
649
652
  }),
650
653
  }),
651
- // sc-5392 -- the app-wide focus-visible outline. A themed emphasis border
654
+ // Search boxes: `<SearchField>` reads this scope directly. Every token is
655
+ // unset by default and falls back to the `muted` field it draws.
656
+ search: Object.freeze({
657
+ label: "Search fields",
658
+ sdkComponents: Object.freeze(["SearchField"]),
659
+ tokens: Object.freeze({
660
+ iconPosition: Object.freeze({ type: "enum", values: THEME_SEARCH_ICON_POSITIONS }),
661
+ // 48 is past half the tallest field, so its ceiling draws a pill.
662
+ radius: Object.freeze({ type: "size", min: 0, max: 48 }),
663
+ background: Object.freeze({ type: "color" }),
664
+ borderColor: Object.freeze({ type: "color" }),
665
+ height: Object.freeze({ type: "size", min: 32, max: 72 }),
666
+ }),
667
+ targets: Object.freeze({}),
668
+ }),
669
+ // sc-5392 -- not a keyboard focus style: a permanent emphasis border
652
670
  // on the Button widget, applied after its variant colours and beaten by the
653
671
  // widget's own per-instance `borderColor` (REQ-THEME-13 stays final).
654
672
  ring: Object.freeze({
655
- label: "Focus ring",
673
+ label: "Button outline",
656
674
  tokens: Object.freeze({
657
675
  color: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
658
676
  width: Object.freeze({ type: "size", min: 0, max: 8 }),
@@ -3093,7 +3111,7 @@ const PRIMITIVES = [
3093
3111
  {
3094
3112
  name: "Switch",
3095
3113
  description:
3096
- 'Themed on/off toggle. `<Switch label="Email me updates" value={on} onValueChange={setOn} />` — THE way to draw a setting that takes effect at once. It is the platform\'s own switch, so each host gives it its switch role, state and keys; its "on" track is the app\'s primary colour and its "off" track the input border (Design > Components > Muted). `label` draws the text beside it and makes the text tappable too, `helper` a hint below, `error` a message in its place (or `true` to mark it without one), `disabled` stops it (a device greys it; on the web it is drawn at half strength unless you passed your own colours). Always pass `label`, or `accessibilityLabel` when a visible label already sits next to it. Controlled — `value` + `onValueChange(next)`. Every React Native `Switch` prop still works: a `trackColor` you pass overrides the theme for the states it names, and `thumbColor` colours the thumb. Never hand-roll a toggle from `Pressable` + a sliding `View`.',
3114
+ 'Themed on/off toggle. `<Switch label="Email me updates" value={on} onValueChange={setOn} />` — THE way to draw a setting that takes effect at once. It is the platform\'s own switch, so each host gives it its switch role, state and keys; its "on" track is the app\'s primary colour and its "off" track the input border (Design > Components > Inputs). `label` draws the text beside it and makes the text tappable too, `helper` a hint below, `error` a message in its place (or `true` to mark it without one), `disabled` stops it (a device greys it; on the web it is drawn at half strength unless you passed your own colours). Always pass `label`, or `accessibilityLabel` when a visible label already sits next to it. Controlled — `value` + `onValueChange(next)`. Every React Native `Switch` prop still works: a `trackColor` you pass overrides the theme for the states it names, and `thumbColor` colours the thumb. Never hand-roll a toggle from `Pressable` + a sliding `View`.',
3097
3115
  rnComponent: "Switch",
3098
3116
  docsUrl: "https://reactnative.dev/docs/switch",
3099
3117
  },
@@ -3202,7 +3220,7 @@ const PRIMITIVES = [
3202
3220
  {
3203
3221
  name: "ConfirmDialog",
3204
3222
  description:
3205
- 'Standard confirmation prompt, a preset of `<Modal>`. `<ConfirmDialog visible={!!pending} tone="destructive" title="Delete this order?" message="This cannot be undone." confirmLabel={t("delete", "Delete")} onCancel={() => setPending(null)} onConfirm={async () => { await remove(pending.id); setPending(null); }} />`. THE way to ask "are you sure?" before a delete or any other action that cannot be undone, so every confirmation in the app has the same wording slots, button order and colours. Never hand-build one from a `<Modal>` or an `<Overlay>` with your own two buttons, never arm a button to be pressed twice, and never use `Alert.alert` or `window.confirm`. Props: `visible` (boolean), `title` (string, the question), `message` (what confirming does; text or a node), `confirmLabel` and `cancelLabel` (default the app\'s translated "Confirm" and "Cancel" — name the action: "Delete", "Remove", "Send"), `tone` ("default" | "destructive"; destructive draws the confirm button from the theme\'s Destructive colours and opens with focus on Cancel), `busy` (boolean; disables both buttons, shows a `<Spinner>` and keeps the dialog open), `onConfirm` (return the promise of the action and the dialog stays busy until it settles, with a second press ignored; a rejection is NOT caught, so catch a failure yourself and report it), `onCancel` (fired by the Cancel button, the close button, the backdrop press, the Escape key on web and the Android back button — cancelling is always the safe default), `closeAccessibilityLabel` and `testID`. The dialog never closes itself: set `visible` false from `onCancel` and after the action. Its panel follows the theme (Design > Components > Dialogs) and its buttons the Buttons and Destructive settings — do not re-style it.',
3223
+ 'Standard confirmation prompt, a preset of `<Modal>`. `<ConfirmDialog visible={!!pending} tone="destructive" title="Delete this order?" message="This cannot be undone." confirmLabel={t("delete", "Delete")} onCancel={() => setPending(null)} onConfirm={async () => { await remove(pending.id); setPending(null); }} />`. THE way to ask "are you sure?" before a delete or any other action that cannot be undone, so every confirmation in the app has the same wording slots, button order and colours. Never hand-build one from a `<Modal>` or an `<Overlay>` with your own two buttons, never arm a button to be pressed twice, and never use `Alert.alert` or `window.confirm`. Props: `visible` (boolean), `title` (string, the question), `message` (what confirming does; text or a node), `confirmLabel` and `cancelLabel` (default the app\'s translated "Confirm" and "Cancel" — name the action: "Delete", "Remove", "Send"), `tone` ("default" | "destructive"; destructive draws the confirm button from the theme\'s Danger buttons colours and opens with focus on Cancel), `busy` (boolean; disables both buttons, shows a `<Spinner>` and keeps the dialog open), `onConfirm` (return the promise of the action and the dialog stays busy until it settles, with a second press ignored; a rejection is NOT caught, so catch a failure yourself and report it), `onCancel` (fired by the Cancel button, the close button, the backdrop press, the Escape key on web and the Android back button — cancelling is always the safe default), `closeAccessibilityLabel` and `testID`. The dialog never closes itself: set `visible` false from `onCancel` and after the action. Its panel follows the theme (Design > Components > Dialogs) and its buttons the Buttons and Destructive settings — do not re-style it.',
3206
3224
  rnComponent: null,
3207
3225
  docsUrl: null,
3208
3226
  },
@@ -3349,35 +3367,35 @@ const PRIMITIVES = [
3349
3367
  {
3350
3368
  name: "Button",
3351
3369
  description:
3352
- 'Themed button. `<Button title="Save" onPress={save} />` — THE way to draw a button: its fill, label colour, border, corner radius, padding, weight, shadow and gradient come from the app\'s button look (Theme Settings > Buttons, and Destructive / Focus ring for the danger variant and emphasis border), so it matches the built-in Button widget. Props: `title`, `onPress` (omit it for a purely visual button inside something else that is pressable), `variant` ("primary" default | "secondary" | "outlined" | "danger"), `icon` (a lucide name) with `iconPosition` ("leading" | "trailing"), `disabled`, `accessibilityLabel`, `testID`, `style` (extra container style, e.g. `alignSelf`) and `labelStyle`. Pass `fields` — `{ background, textColor, borderColor, borderWidth, paddingX, paddingY, radius, fontWeight, fontSize, shadow, gradient }` — to override one button, e.g. from style fields your widget declares. On a section that paints its own fill (`theme.colors.backdrop` there), a variant colour that the section makes unreadable takes that section\'s text colour, as its text does: an outlined label and border, a secondary whose fill disappears becomes outlined, and a primary or danger fill turns inverse (that text colour as its fill, its own colour as its label). A white or lightly tinted panel changes nothing, and a button inside a `<Card>` is measured against that card. A colour you pass in `fields`, `style` or `labelStyle`, or that the app\'s button look sets, is never changed. Never hand-roll a button from `Pressable` + background + radius.',
3370
+ 'Themed button. `<Button title="Save" onPress={save} />` — THE way to draw a button: its fill, label colour, border, corner radius, padding, weight, shadow and gradient come from the app\'s button look (Design > Components > Buttons, and Danger buttons / Button outline for the danger variant and emphasis border), so it matches the built-in Button widget. Props: `title`, `onPress` (omit it for a purely visual button inside something else that is pressable), `variant` ("primary" default | "secondary" | "outlined" | "danger"), `icon` (a lucide name) with `iconPosition` ("leading" | "trailing"), `disabled`, `accessibilityLabel`, `testID`, `style` (extra container style, e.g. `alignSelf`) and `labelStyle`. Pass `fields` — `{ background, textColor, borderColor, borderWidth, paddingX, paddingY, radius, fontWeight, fontSize, shadow, gradient }` — to override one button, e.g. from style fields your widget declares. On a section that paints its own fill (`theme.colors.backdrop` there), a variant colour that the section makes unreadable takes that section\'s text colour, as its text does: an outlined label and border, a secondary whose fill disappears becomes outlined, and a primary or danger fill turns inverse (that text colour as its fill, its own colour as its label). A white or lightly tinted panel changes nothing, and a button inside a `<Card>` is measured against that card. A colour you pass in `fields`, `style` or `labelStyle`, or that the app\'s button look sets, is never changed. Never hand-roll a button from `Pressable` + background + radius.',
3353
3371
  rnComponent: null,
3354
3372
  docsUrl: null,
3355
3373
  },
3356
3374
  {
3357
3375
  name: "TextField",
3358
3376
  description:
3359
- 'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works, and its background, text colour, border, corner radius and height come from the app\'s input look (Theme Settings > Muted), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. `label` draws a label above the field and names it for a screen reader, `helper` a hint below, `error` a message in the helper\'s place with a danger border (`true` marks it without a message), `disabled` makes it read-only and dimmed; `containerStyle` styles the box around a labelled field. Use it instead of styling a bare `TextInput`.',
3377
+ 'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works, and its background, text colour, border, corner radius and height come from the app\'s input look (Design > Components > Inputs), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. `label` draws a label above the field and names it for a screen reader, `helper` a hint below, `error` a message in the helper\'s place with a danger border (`true` marks it without a message), `disabled` makes it read-only and dimmed; `containerStyle` styles the box around a labelled field. Use it instead of styling a bare `TextInput`.',
3360
3378
  rnComponent: null,
3361
3379
  docsUrl: null,
3362
3380
  },
3363
3381
  {
3364
3382
  name: "Checkbox",
3365
3383
  description:
3366
- 'Themed tick box. `<Checkbox label="Remember me" value={checked} onValueChange={setChecked} />` — THE way to draw a yes/no choice that is submitted with a form, or one row of a multi-select list. The box is the app\'s input surface (Design > Components > Muted) and fills with the primary colour when checked; focus draws a ring in the primary colour around it. The whole row is one 44px target with the checkbox role and checked state on both hosts, and Space toggles it on the web. Props: `value` (boolean), `onValueChange(next)`, `label` (text, or an element), `helper`, `error` (a message, or `true`), `disabled`, `accessibilityLabel` (required when there is no text `label`), `style` (the row), `containerStyle` (the box around a row that shows a helper or error), `testID`; any other prop lands on the row. Never hand-roll a tick box from `Pressable` + a bordered `View` + a "✓".',
3384
+ 'Themed tick box. `<Checkbox label="Remember me" value={checked} onValueChange={setChecked} />` — THE way to draw a yes/no choice that is submitted with a form, or one row of a multi-select list. The box is the app\'s input surface (Design > Components > Inputs) and fills with the primary colour when checked; focus draws a ring in the primary colour around it. The whole row is one 44px target with the checkbox role and checked state on both hosts, and Space toggles it on the web. Props: `value` (boolean), `onValueChange(next)`, `label` (text, or an element), `helper`, `error` (a message, or `true`), `disabled`, `accessibilityLabel` (required when there is no text `label`), `style` (the row), `containerStyle` (the box around a row that shows a helper or error), `testID`; any other prop lands on the row. Never hand-roll a tick box from `Pressable` + a bordered `View` + a "✓".',
3367
3385
  rnComponent: null,
3368
3386
  docsUrl: null,
3369
3387
  },
3370
3388
  {
3371
3389
  name: "Select",
3372
3390
  description:
3373
- 'Themed single-choice picker. `<Select label="Status" value={status} onValueChange={setStatus} options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} placeholder="Choose a status" />` — THE way to let the user pick ONE of a known list. The trigger is the app\'s input field (Design > Components > Muted) showing the chosen label; pressing it opens the options at SCREEN level, in a sheet painted from Design > Components > Popover, so no card or scroll container can clip them, and a long list scrolls inside the sheet. The trigger is a button that opens a dialog (expanded state included) and each option carries the option role and its selected state on both hosts; the list around them is a listbox on the web only. On the web Enter, Space or an arrow key opens it, the arrow keys, Home and End move between options, Enter or Space chooses, Escape closes, and focus returns to the trigger; on Android the back button closes it. Props: `options` (`[{ value, label, disabled?, testID? }]`, `value` a string or number, unique), `value`, `onValueChange(value, option)`, `placeholder` (default "Select"), `title` (the sheet\'s heading; defaults to a text `label`), `emptyText` (default "No options"), `label`, `helper`, `error`, `disabled`, `accessibilityLabel`, `closeAccessibilityLabel`, `background` / `textColor` / `borderColor` / `radius` / `height` to override one field, `style` (the trigger), `containerStyle`, `testID`; any other prop lands on the trigger. Never hand-roll a dropdown from `Pressable` + an absolutely positioned list: it is clipped by the widget\'s container on both hosts.',
3391
+ 'Themed single-choice picker. `<Select label="Status" value={status} onValueChange={setStatus} options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} placeholder="Choose a status" />` — THE way to let the user pick ONE of a known list. The trigger is the app\'s input field (Design > Components > Inputs) showing the chosen label; pressing it opens the options at SCREEN level, in a sheet painted from Design > Components > Dropdown menus, so no card or scroll container can clip them, and a long list scrolls inside the sheet. The trigger is a button that opens a dialog (expanded state included) and each option carries the option role and its selected state on both hosts; the list around them is a listbox on the web only. On the web Enter, Space or an arrow key opens it, the arrow keys, Home and End move between options, Enter or Space chooses, Escape closes, and focus returns to the trigger; on Android the back button closes it. Props: `options` (`[{ value, label, disabled?, testID? }]`, `value` a string or number, unique), `value`, `onValueChange(value, option)`, `placeholder` (default "Select"), `title` (the sheet\'s heading; defaults to a text `label`), `emptyText` (default "No options"), `label`, `helper`, `error`, `disabled`, `accessibilityLabel`, `closeAccessibilityLabel`, `background` / `textColor` / `borderColor` / `radius` / `height` to override one field, `style` (the trigger), `containerStyle`, `testID`; any other prop lands on the trigger. Never hand-roll a dropdown from `Pressable` + an absolutely positioned list: it is clipped by the widget\'s container on both hosts.',
3374
3392
  rnComponent: null,
3375
3393
  docsUrl: null,
3376
3394
  },
3377
3395
  {
3378
3396
  name: "SearchField",
3379
3397
  description:
3380
- 'Themed search box. `<SearchField value={query} onChangeText={setQuery} placeholder="Search orders" />` — THE way to draw a field that filters or searches. It IS a `TextField` (same look, same props, same `label` / `helper` / `error` / `disabled`) with a search glyph, the search key on the on-screen keyboard and a clear control that appears once there is text; clearing calls `onChangeText("")`, then `onClear` if you passed one, and keeps focus in the field. `placeholder` defaults to "Search" and `clearAccessibilityLabel` to "Clear search" — pass translated ones. `style` styles the input and `containerStyle` the box around it (use it for `flex: 1` in a toolbar). Debounce the query yourself before you fetch. Never hand-roll a search box from a `TextInput` + an icon.',
3398
+ 'Themed search box. `<SearchField value={query} onChangeText={setQuery} placeholder="Search orders" />` — THE way to draw a field that filters or searches. It IS a `TextField` (same props, same `label` / `helper` / `error` / `disabled`, the input look restyled by Design > Components > Search fields) with a search glyph, the search key on the on-screen keyboard and a clear control that appears once there is text; clearing calls `onChangeText("")`, then `onClear` if you passed one, and keeps focus in the field. `placeholder` defaults to "Search" and `clearAccessibilityLabel` to "Clear search" — pass translated ones. `style` styles the input and `containerStyle` the box around it (use it for `flex: 1` in a toolbar). Where the glyph sits (start, end or hidden), the corner radius, background, border colour and height come from the app; `iconPosition` ("start" | "end" | "none") overrides the glyph for one field, and at the end the clear control takes the glyph\'s place while there is text. Debounce the query yourself before you fetch. Never hand-roll a search box from a `TextInput` + an icon, and never add a magnifier of your own.',
3381
3399
  rnComponent: null,
3382
3400
  docsUrl: null,
3383
3401
  },
@@ -6049,7 +6067,12 @@ const CONTRACT = deepFreeze({
6049
6067
  // calendar sheet instead of the browser's date popup; both hosts show
6050
6068
  // the value in the app's language with a clear button. New optional
6051
6069
  // props: placeholder, clearLabel, doneLabel, closeAccessibilityLabel, testID.
6052
- version: "1.190.0",
6070
+ // 1.191.0: additive (sc-11759) — `themeComponents.search` styles every
6071
+ // `<SearchField>`: icon position, corner radius, background, border
6072
+ // colour, height, each falling back to the `muted` field. The field
6073
+ // takes an `iconPosition` prop. Scope labels renamed (sc-11763); keys
6074
+ // unchanged.
6075
+ version: "1.191.0",
6053
6076
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6054
6077
  hooks: HOOKS,
6055
6078
  primitives: PRIMITIVES,
@@ -6065,6 +6088,7 @@ const CONTRACT = deepFreeze({
6065
6088
  themeColorTokens: THEME_COLOR_TOKENS,
6066
6089
  themeComponents: THEME_COMPONENTS,
6067
6090
  themeComponentTabStyles: THEME_TAB_STYLES,
6091
+ themeComponentSearchIconPositions: THEME_SEARCH_ICON_POSITIONS,
6068
6092
  themeComponentShadows: THEME_COMPONENT_SHADOWS,
6069
6093
  themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
6070
6094
  themeComponentGradient: THEME_COMPONENT_GRADIENT,
package/dist/host.d.ts CHANGED
@@ -51,6 +51,12 @@ export function bannerScopeDefaults(theme: unknown): Record<string, string | num
51
51
  */
52
52
  export function chipScopeDefaults(theme: unknown): Record<string, string | number>;
53
53
 
54
+ /**
55
+ * What each token of the `search` component scope resolves to while unset,
56
+ * keyed by token name: the input look `<SearchField>` falls back to.
57
+ */
58
+ export function searchScopeDefaults(theme: unknown): Record<string, string | number>;
59
+
54
60
  /**
55
61
  * The theme a surface painting `backgroundColor` hands its subtree: surface
56
62
  * tokens derived for that fill, and any type tier whose own ink cannot be read
@@ -578,6 +584,12 @@ export function resolveLoginLogoUrl(
578
584
  appLogoUrl: string | null | undefined,
579
585
  ): string | null;
580
586
 
587
+ /** sc-11754 — whether the sign-in card shows its heading and subheading. */
588
+ export function resolveLoginTextVisibility(login: unknown): {
589
+ heading: boolean;
590
+ subheading: boolean;
591
+ };
592
+
581
593
  /** sc-7800 — true when a stored value is a `theme:<path>` colour binding. */
582
594
  export function isThemeTokenRef(value: unknown): boolean;
583
595
 
package/dist/host.js CHANGED
@@ -54,6 +54,8 @@ export { resolveModalTokens, modalScopeDefaults } from "./modal-tokens.js";
54
54
  // What each `chip` scope token resolves to while unset, for the Studio's
55
55
  // Design page to preview beside its controls.
56
56
  export { chipScopeDefaults } from "./chip-tokens.js";
57
+ // The input look each unset `search` token falls back to, for the same page.
58
+ export { searchScopeDefaults } from "./search-field-tokens.js";
57
59
 
58
60
  export {
59
61
  createToastController,
@@ -145,6 +147,7 @@ export {
145
147
  normaliseThemeGradient,
146
148
  normaliseLoginDesign,
147
149
  resolveLoginLogoUrl,
150
+ resolveLoginTextVisibility,
148
151
  } from "./theme-background.js";
149
152
 
150
153
  // sc-8448: one DATETIME local-clock conversion for the picker and the Studio.
package/dist/index.d.ts CHANGED
@@ -3939,7 +3939,7 @@ export type ButtonVariant = "primary" | "secondary" | "outlined" | "danger";
3939
3939
  export const BUTTON_VARIANTS: readonly ButtonVariant[];
3940
3940
 
3941
3941
  /**
3942
- * Themed button (Theme Settings > Buttons, Destructive, Focus ring). Omit
3942
+ * Themed button (Design > Components > Buttons, Danger buttons, Button outline). Omit
3943
3943
  * `onPress` for a purely visual button inside something pressable.
3944
3944
  *
3945
3945
  * @example
@@ -4000,7 +4000,7 @@ export interface FormFieldOverrides {
4000
4000
  }
4001
4001
 
4002
4002
  /**
4003
- * Themed text-entry field (Design > Components > Muted). Every TextInput prop
4003
+ * Themed text-entry field (Design > Components > Inputs). Every TextInput prop
4004
4004
  * works.
4005
4005
  *
4006
4006
  * @example
@@ -4100,9 +4100,14 @@ export function spaceActivates(
4100
4100
  onPress: () => void,
4101
4101
  ): (event: { key: string; repeat?: boolean; preventDefault: () => void }) => void;
4102
4102
 
4103
+ /** Where `<SearchField>` draws its search glyph. */
4104
+ export type SearchIconPosition = "start" | "end" | "none";
4105
+
4103
4106
  /**
4104
4107
  * Themed search box: a `TextField` with a search glyph, the search key and a
4105
- * clear control. Every `TextField` prop works.
4108
+ * clear control. Every `TextField` prop works. It wears the input look,
4109
+ * restyled by the theme's `search` scope (glyph position, corner radius,
4110
+ * background, border colour, height); each unset token keeps the input look.
4106
4111
  *
4107
4112
  * @example
4108
4113
  * <SearchField value={query} onChangeText={setQuery} placeholder="Search orders" />
@@ -4110,6 +4115,11 @@ export function spaceActivates(
4110
4115
  export const SearchField: (props: FormControlProps & {
4111
4116
  value?: string;
4112
4117
  onChangeText?: (text: string) => void;
4118
+ /**
4119
+ * Overrides the theme's glyph position for this field. At "end" the clear
4120
+ * control takes the glyph's place while the field has text.
4121
+ */
4122
+ iconPosition?: SearchIconPosition;
4113
4123
  /** Called after the clear control empties the field. */
4114
4124
  onClear?: () => void;
4115
4125
  /** Default "Search". */
@@ -0,0 +1,85 @@
1
+ // What `<SearchField>` draws from the theme's `search` scope: where its
2
+ // magnifier sits, and the field look it lays over `<TextField>`'s `muted` one.
3
+ // Platform-free, so both hosts lay the field out from the same answer.
4
+
5
+ import { CONTRACT } from "./contract.js";
6
+ import { normaliseThemeComponents } from "./theme-components.js";
7
+ import { resolveTextFieldTokens } from "./text-field-tokens.js";
8
+
9
+ export const SEARCH_ICON_POSITIONS = CONTRACT.themeComponentSearchIconPositions;
10
+ export const DEFAULT_SEARCH_ICON_POSITION = "start";
11
+
12
+ // The `search` tokens that restyle the field; each is a `<TextField>` override.
13
+ export const SEARCH_FIELD_LOOK_TOKENS = Object.freeze([
14
+ "background",
15
+ "borderColor",
16
+ "radius",
17
+ "height",
18
+ ]);
19
+
20
+ function searchScope(theme) {
21
+ const components = theme && typeof theme === "object" ? theme.components : undefined;
22
+ return normaliseThemeComponents({ search: components && components.search }).search || {};
23
+ }
24
+
25
+ /**
26
+ * The magnifier's position: the field's own prop, then the theme, then start.
27
+ *
28
+ * @param {unknown} own — the `iconPosition` prop.
29
+ * @param {unknown} themed — the theme's `search.iconPosition`.
30
+ */
31
+ export function searchIconPosition(own, themed) {
32
+ if (SEARCH_ICON_POSITIONS.includes(own)) return own;
33
+ if (SEARCH_ICON_POSITIONS.includes(themed)) return themed;
34
+ return DEFAULT_SEARCH_ICON_POSITION;
35
+ }
36
+
37
+ /**
38
+ * Resolve one search field. `field` holds only the tokens the theme set, so
39
+ * an unset one leaves `<TextField>` on the `muted` look it always drew.
40
+ *
41
+ * @param {object} theme — the resolved workspace theme (`useTheme()` shape).
42
+ * @param {{ iconPosition?: string }} [own] — the field's own props.
43
+ * @returns {{ iconPosition: string, field: Record<string, string|number> }}
44
+ */
45
+ export function resolveSearchFieldTokens(theme, own) {
46
+ const scope = searchScope(theme);
47
+ const field = {};
48
+ for (const name of SEARCH_FIELD_LOOK_TOKENS) {
49
+ if (scope[name] !== undefined) field[name] = scope[name];
50
+ }
51
+ return {
52
+ iconPosition: searchIconPosition(own && own.iconPosition, scope.iconPosition),
53
+ field,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Where the glyph and the clear control go. At the end, the clear control
59
+ * takes the glyph's slot while there is text, so the two never overlap.
60
+ *
61
+ * @param {string} iconPosition — a resolved SEARCH_ICON_POSITIONS value.
62
+ * @param {boolean} clearable — the field has text and is enabled.
63
+ * @returns {{ glyph: "start"|"end"|null, padStart: boolean, padEnd: boolean }}
64
+ */
65
+ export function searchFieldLayout(iconPosition, clearable) {
66
+ const glyphAtEnd = iconPosition === "end" && !clearable;
67
+ return {
68
+ glyph: iconPosition === "start" ? "start" : glyphAtEnd ? "end" : null,
69
+ padStart: iconPosition === "start",
70
+ padEnd: clearable || iconPosition === "end",
71
+ };
72
+ }
73
+
74
+ /**
75
+ * What each `search` token resolves to while unset, keyed by token name, for
76
+ * the Studio to preview: the `muted` field the search box falls back to.
77
+ *
78
+ * @param {object} theme — the resolved workspace theme.
79
+ */
80
+ export function searchScopeDefaults(theme) {
81
+ const field = resolveTextFieldTokens(theme);
82
+ const out = { iconPosition: DEFAULT_SEARCH_ICON_POSITION };
83
+ for (const name of SEARCH_FIELD_LOOK_TOKENS) out[name] = field[name];
84
+ return out;
85
+ }
@@ -1,13 +1,15 @@
1
1
  // The ONE implementation behind the `<SearchField>` SDK primitive. It is
2
2
  // `<TextField>` with a search glyph and a clear control, so it wears the same
3
- // field and takes the same props; the platform bindings (`search-field.js` /
4
- // `search-field.native.js`) differ only in where the primitives come from.
3
+ // field and takes the same props, plus the theme's `search` look; the platform
4
+ // bindings (`search-field.js` / `search-field.native.js`) differ only in where
5
+ // the primitives come from.
5
6
 
6
7
  import React from "react";
7
8
  import { useHostTheme } from "./hooks.js";
8
9
  import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
9
10
  import { Icon } from "./icon.js";
10
11
  import { resolveFormControlTokens } from "./form-control-tokens.js";
12
+ import { resolveSearchFieldTokens, searchFieldLayout } from "./search-field-tokens.js";
11
13
  import {
12
14
  errorMessage,
13
15
  hasNote,
@@ -24,6 +26,17 @@ const ADORNMENT_WIDTH = 40;
24
26
  // A real target: the web host ignores hitSlop, so size carries it.
25
27
  const CLEAR_MIN_SIZE = 36;
26
28
 
29
+ function adornmentSlot(side) {
30
+ return {
31
+ position: "absolute",
32
+ top: 0,
33
+ bottom: 0,
34
+ alignItems: "center",
35
+ justifyContent: "center",
36
+ ...(side === "start" ? { left: 0 } : { right: 0 }),
37
+ };
38
+ }
39
+
27
40
  /**
28
41
  * Bind the shared search field to one platform's primitives.
29
42
  *
@@ -45,6 +58,7 @@ export function makeSearchField(rn) {
45
58
  error,
46
59
  disabled = false,
47
60
  accessibilityLabel,
61
+ iconPosition,
48
62
  style,
49
63
  containerStyle,
50
64
  testID,
@@ -52,6 +66,7 @@ export function makeSearchField(rn) {
52
66
  } = props;
53
67
  const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
54
68
  const t = resolveFormControlTokens(theme);
69
+ const search = resolveSearchFieldTokens(theme, { iconPosition });
55
70
  const inputRef = React.useRef(null);
56
71
  const setRef = React.useMemo(() => shareRef(ref, inputRef), [ref]);
57
72
  const noteId = React.useId();
@@ -60,6 +75,8 @@ export function makeSearchField(rn) {
60
75
  if (input && typeof input.focus === "function") input.focus();
61
76
  };
62
77
  const hasText = typeof value === "string" && value.length > 0;
78
+ const clearable = hasText && !disabled;
79
+ const layout = searchFieldLayout(search.iconPosition, clearable);
63
80
  const clear = () => {
64
81
  if (typeof onChangeText === "function") onChangeText("");
65
82
  if (typeof onClear === "function") onClear();
@@ -98,30 +115,30 @@ export function makeSearchField(rn) {
98
115
  autoCapitalize: "none",
99
116
  autoCorrect: false,
100
117
  testID,
118
+ // The field's own props beat the theme's search look.
119
+ ...search.field,
101
120
  ...rest,
102
121
  style: [
103
- { paddingLeft: ADORNMENT_WIDTH },
104
- hasText && !disabled ? { paddingRight: ADORNMENT_WIDTH } : null,
122
+ layout.padStart ? { paddingLeft: ADORNMENT_WIDTH } : null,
123
+ layout.padEnd ? { paddingRight: ADORNMENT_WIDTH } : null,
105
124
  style,
106
125
  ],
107
126
  }),
108
- h(
109
- View,
110
- {
111
- style: {
112
- position: "absolute",
113
- left: 0,
114
- top: 0,
115
- bottom: 0,
116
- width: ADORNMENT_WIDTH,
117
- alignItems: "center",
118
- justifyContent: "center",
119
- pointerEvents: "none",
120
- },
121
- },
122
- h(Icon, { name: "search", size: GLYPH_SIZE, color: t.placeholderColor }),
123
- ),
124
- hasText && !disabled
127
+ layout.glyph
128
+ ? h(
129
+ View,
130
+ {
131
+ testID: testID ? `${testID}-icon` : undefined,
132
+ style: {
133
+ ...adornmentSlot(layout.glyph),
134
+ width: ADORNMENT_WIDTH,
135
+ pointerEvents: "none",
136
+ },
137
+ },
138
+ h(Icon, { name: "search", size: GLYPH_SIZE, color: t.placeholderColor }),
139
+ )
140
+ : null,
141
+ clearable
125
142
  ? h(
126
143
  Pressable,
127
144
  {
@@ -131,14 +148,10 @@ export function makeSearchField(rn) {
131
148
  testID: testID ? `${testID}-clear` : undefined,
132
149
  style: ({ pressed }) => [
133
150
  {
134
- position: "absolute",
151
+ ...adornmentSlot("end"),
135
152
  right: 2,
136
- top: 0,
137
- bottom: 0,
138
153
  minWidth: CLEAR_MIN_SIZE,
139
154
  minHeight: CLEAR_MIN_SIZE,
140
- alignItems: "center",
141
- justifyContent: "center",
142
155
  },
143
156
  pressed ? { opacity: 0.6 } : null,
144
157
  ],
@@ -153,9 +153,27 @@ function resolveLoginLogoUrl(login, appLogoUrl) {
153
153
  return own || normaliseImageUrl(appLogoUrl);
154
154
  }
155
155
 
156
+ /**
157
+ * sc-11754 — which of the card's two text lines the author left showing.
158
+ * Only a literal `true` hides, so every app saved before this keeps both.
159
+ * Not in `normaliseLoginDesign`: hidden text is not a backdrop, so it must
160
+ * not lift the card or drop the shell background.
161
+ *
162
+ * @param {object|null|undefined} login - `themeConfig.login`.
163
+ * @returns {{ heading: boolean, subheading: boolean }}
164
+ */
165
+ function resolveLoginTextVisibility(login) {
166
+ const own = login && typeof login === "object" ? login : {};
167
+ return {
168
+ heading: own.hideHeading !== true,
169
+ subheading: own.hideSubheading !== true,
170
+ };
171
+ }
172
+
156
173
  module.exports = {
157
174
  LOGIN_TEXT_MAX_LENGTH,
158
175
  normaliseThemeGradient,
159
176
  normaliseLoginDesign,
160
177
  resolveLoginLogoUrl,
178
+ resolveLoginTextVisibility,
161
179
  };
@@ -144,3 +144,20 @@ export function resolveLoginLogoUrl(login, appLogoUrl) {
144
144
  : null;
145
145
  return own || normaliseImageUrl(appLogoUrl);
146
146
  }
147
+
148
+ /**
149
+ * sc-11754 — which of the card's two text lines the author left showing.
150
+ * Only a literal `true` hides, so every app saved before this keeps both.
151
+ * Not in `normaliseLoginDesign`: hidden text is not a backdrop, so it must
152
+ * not lift the card or drop the shell background.
153
+ *
154
+ * @param {object|null|undefined} login - `themeConfig.login`.
155
+ * @returns {{ heading: boolean, subheading: boolean }}
156
+ */
157
+ export function resolveLoginTextVisibility(login) {
158
+ const own = login && typeof login === "object" ? login : {};
159
+ return {
160
+ heading: own.hideHeading !== true,
161
+ subheading: own.hideSubheading !== true,
162
+ };
163
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.235.0",
3
+ "version": "0.237.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",
package/src/contract.js CHANGED
@@ -275,6 +275,9 @@ function formatThemeColorTokenRef(path, percent) {
275
275
  // The looks a `tabs.style` token may name; `<Tabs>` draws each.
276
276
  const THEME_TAB_STYLES = Object.freeze(["segmented", "pill", "underline"]);
277
277
 
278
+ // Where `<SearchField>` draws its magnifier; "start" is the look it always had.
279
+ const THEME_SEARCH_ICON_POSITIONS = Object.freeze(["start", "end", "none"]);
280
+
278
281
  const THEME_COMPONENT_SHADOWS = Object.freeze(["none", "sm", "md", "lg"]);
279
282
 
280
283
  // sc-3857 — the `textTransform` token's closed enum, matching the per-instance
@@ -585,7 +588,7 @@ const THEME_COMPONENTS = Object.freeze({
585
588
  // than its `color` field, which the `text` scope already owns -- two
586
589
  // scopes writing the same field on the same widget would race.
587
590
  accent: Object.freeze({
588
- label: "Accent",
591
+ label: "Labels",
589
592
  tokens: Object.freeze({
590
593
  background: Object.freeze({ type: "color", uiDefault: "colors.primarySoft" }),
591
594
  borderColor: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
@@ -602,7 +605,7 @@ const THEME_COMPONENTS = Object.freeze({
602
605
  // sc-5392 -- the Button widget's danger/delete variant. Separate token set
603
606
  // from `button` above so "make the buttons coral" never recolours Delete.
604
607
  destructive: Object.freeze({
605
- label: "Destructive",
608
+ label: "Danger buttons",
606
609
  tokens: Object.freeze({
607
610
  background: Object.freeze({ type: "color", uiDefault: "colors.danger" }),
608
611
  textColor: Object.freeze({ type: "color", uiDefault: "colors.onPrimary" }),
@@ -617,7 +620,7 @@ const THEME_COMPONENTS = Object.freeze({
617
620
  // scope) rather than to Text/Label/Data Value, whose `color` field the
618
621
  // `text` scope already owns.
619
622
  muted: Object.freeze({
620
- label: "Muted",
623
+ label: "Inputs",
621
624
  // The SDK's form controls read this scope directly, in every widget.
622
625
  sdkComponents: Object.freeze(["TextField", "Checkbox", "Switch", "Select", "SearchField"]),
623
626
  tokens: Object.freeze({
@@ -636,7 +639,7 @@ const THEME_COMPONENTS = Object.freeze({
636
639
  // sc-5392 -- dropdown/menu surfaces. The form widgets' choice-field option
637
640
  // list is the closest thing the built-in catalog has to a popover panel.
638
641
  popover: Object.freeze({
639
- label: "Popover",
642
+ label: "Dropdown menus",
640
643
  sdkComponents: Object.freeze(["Select"]),
641
644
  tokens: Object.freeze({
642
645
  background: Object.freeze({ type: "color", uiDefault: "colors.surface" }),
@@ -648,11 +651,26 @@ const THEME_COMPONENTS = Object.freeze({
648
651
  "appstudio.form-builder": POPOVER_FIELDS,
649
652
  }),
650
653
  }),
651
- // sc-5392 -- the app-wide focus-visible outline. A themed emphasis border
654
+ // Search boxes: `<SearchField>` reads this scope directly. Every token is
655
+ // unset by default and falls back to the `muted` field it draws.
656
+ search: Object.freeze({
657
+ label: "Search fields",
658
+ sdkComponents: Object.freeze(["SearchField"]),
659
+ tokens: Object.freeze({
660
+ iconPosition: Object.freeze({ type: "enum", values: THEME_SEARCH_ICON_POSITIONS }),
661
+ // 48 is past half the tallest field, so its ceiling draws a pill.
662
+ radius: Object.freeze({ type: "size", min: 0, max: 48 }),
663
+ background: Object.freeze({ type: "color" }),
664
+ borderColor: Object.freeze({ type: "color" }),
665
+ height: Object.freeze({ type: "size", min: 32, max: 72 }),
666
+ }),
667
+ targets: Object.freeze({}),
668
+ }),
669
+ // sc-5392 -- not a keyboard focus style: a permanent emphasis border
652
670
  // on the Button widget, applied after its variant colours and beaten by the
653
671
  // widget's own per-instance `borderColor` (REQ-THEME-13 stays final).
654
672
  ring: Object.freeze({
655
- label: "Focus ring",
673
+ label: "Button outline",
656
674
  tokens: Object.freeze({
657
675
  color: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
658
676
  width: Object.freeze({ type: "size", min: 0, max: 8 }),
@@ -3093,7 +3111,7 @@ const PRIMITIVES = [
3093
3111
  {
3094
3112
  name: "Switch",
3095
3113
  description:
3096
- 'Themed on/off toggle. `<Switch label="Email me updates" value={on} onValueChange={setOn} />` — THE way to draw a setting that takes effect at once. It is the platform\'s own switch, so each host gives it its switch role, state and keys; its "on" track is the app\'s primary colour and its "off" track the input border (Design > Components > Muted). `label` draws the text beside it and makes the text tappable too, `helper` a hint below, `error` a message in its place (or `true` to mark it without one), `disabled` stops it (a device greys it; on the web it is drawn at half strength unless you passed your own colours). Always pass `label`, or `accessibilityLabel` when a visible label already sits next to it. Controlled — `value` + `onValueChange(next)`. Every React Native `Switch` prop still works: a `trackColor` you pass overrides the theme for the states it names, and `thumbColor` colours the thumb. Never hand-roll a toggle from `Pressable` + a sliding `View`.',
3114
+ 'Themed on/off toggle. `<Switch label="Email me updates" value={on} onValueChange={setOn} />` — THE way to draw a setting that takes effect at once. It is the platform\'s own switch, so each host gives it its switch role, state and keys; its "on" track is the app\'s primary colour and its "off" track the input border (Design > Components > Inputs). `label` draws the text beside it and makes the text tappable too, `helper` a hint below, `error` a message in its place (or `true` to mark it without one), `disabled` stops it (a device greys it; on the web it is drawn at half strength unless you passed your own colours). Always pass `label`, or `accessibilityLabel` when a visible label already sits next to it. Controlled — `value` + `onValueChange(next)`. Every React Native `Switch` prop still works: a `trackColor` you pass overrides the theme for the states it names, and `thumbColor` colours the thumb. Never hand-roll a toggle from `Pressable` + a sliding `View`.',
3097
3115
  rnComponent: "Switch",
3098
3116
  docsUrl: "https://reactnative.dev/docs/switch",
3099
3117
  },
@@ -3202,7 +3220,7 @@ const PRIMITIVES = [
3202
3220
  {
3203
3221
  name: "ConfirmDialog",
3204
3222
  description:
3205
- 'Standard confirmation prompt, a preset of `<Modal>`. `<ConfirmDialog visible={!!pending} tone="destructive" title="Delete this order?" message="This cannot be undone." confirmLabel={t("delete", "Delete")} onCancel={() => setPending(null)} onConfirm={async () => { await remove(pending.id); setPending(null); }} />`. THE way to ask "are you sure?" before a delete or any other action that cannot be undone, so every confirmation in the app has the same wording slots, button order and colours. Never hand-build one from a `<Modal>` or an `<Overlay>` with your own two buttons, never arm a button to be pressed twice, and never use `Alert.alert` or `window.confirm`. Props: `visible` (boolean), `title` (string, the question), `message` (what confirming does; text or a node), `confirmLabel` and `cancelLabel` (default the app\'s translated "Confirm" and "Cancel" — name the action: "Delete", "Remove", "Send"), `tone` ("default" | "destructive"; destructive draws the confirm button from the theme\'s Destructive colours and opens with focus on Cancel), `busy` (boolean; disables both buttons, shows a `<Spinner>` and keeps the dialog open), `onConfirm` (return the promise of the action and the dialog stays busy until it settles, with a second press ignored; a rejection is NOT caught, so catch a failure yourself and report it), `onCancel` (fired by the Cancel button, the close button, the backdrop press, the Escape key on web and the Android back button — cancelling is always the safe default), `closeAccessibilityLabel` and `testID`. The dialog never closes itself: set `visible` false from `onCancel` and after the action. Its panel follows the theme (Design > Components > Dialogs) and its buttons the Buttons and Destructive settings — do not re-style it.',
3223
+ 'Standard confirmation prompt, a preset of `<Modal>`. `<ConfirmDialog visible={!!pending} tone="destructive" title="Delete this order?" message="This cannot be undone." confirmLabel={t("delete", "Delete")} onCancel={() => setPending(null)} onConfirm={async () => { await remove(pending.id); setPending(null); }} />`. THE way to ask "are you sure?" before a delete or any other action that cannot be undone, so every confirmation in the app has the same wording slots, button order and colours. Never hand-build one from a `<Modal>` or an `<Overlay>` with your own two buttons, never arm a button to be pressed twice, and never use `Alert.alert` or `window.confirm`. Props: `visible` (boolean), `title` (string, the question), `message` (what confirming does; text or a node), `confirmLabel` and `cancelLabel` (default the app\'s translated "Confirm" and "Cancel" — name the action: "Delete", "Remove", "Send"), `tone` ("default" | "destructive"; destructive draws the confirm button from the theme\'s Danger buttons colours and opens with focus on Cancel), `busy` (boolean; disables both buttons, shows a `<Spinner>` and keeps the dialog open), `onConfirm` (return the promise of the action and the dialog stays busy until it settles, with a second press ignored; a rejection is NOT caught, so catch a failure yourself and report it), `onCancel` (fired by the Cancel button, the close button, the backdrop press, the Escape key on web and the Android back button — cancelling is always the safe default), `closeAccessibilityLabel` and `testID`. The dialog never closes itself: set `visible` false from `onCancel` and after the action. Its panel follows the theme (Design > Components > Dialogs) and its buttons the Buttons and Destructive settings — do not re-style it.',
3206
3224
  rnComponent: null,
3207
3225
  docsUrl: null,
3208
3226
  },
@@ -3349,35 +3367,35 @@ const PRIMITIVES = [
3349
3367
  {
3350
3368
  name: "Button",
3351
3369
  description:
3352
- 'Themed button. `<Button title="Save" onPress={save} />` — THE way to draw a button: its fill, label colour, border, corner radius, padding, weight, shadow and gradient come from the app\'s button look (Theme Settings > Buttons, and Destructive / Focus ring for the danger variant and emphasis border), so it matches the built-in Button widget. Props: `title`, `onPress` (omit it for a purely visual button inside something else that is pressable), `variant` ("primary" default | "secondary" | "outlined" | "danger"), `icon` (a lucide name) with `iconPosition` ("leading" | "trailing"), `disabled`, `accessibilityLabel`, `testID`, `style` (extra container style, e.g. `alignSelf`) and `labelStyle`. Pass `fields` — `{ background, textColor, borderColor, borderWidth, paddingX, paddingY, radius, fontWeight, fontSize, shadow, gradient }` — to override one button, e.g. from style fields your widget declares. On a section that paints its own fill (`theme.colors.backdrop` there), a variant colour that the section makes unreadable takes that section\'s text colour, as its text does: an outlined label and border, a secondary whose fill disappears becomes outlined, and a primary or danger fill turns inverse (that text colour as its fill, its own colour as its label). A white or lightly tinted panel changes nothing, and a button inside a `<Card>` is measured against that card. A colour you pass in `fields`, `style` or `labelStyle`, or that the app\'s button look sets, is never changed. Never hand-roll a button from `Pressable` + background + radius.',
3370
+ 'Themed button. `<Button title="Save" onPress={save} />` — THE way to draw a button: its fill, label colour, border, corner radius, padding, weight, shadow and gradient come from the app\'s button look (Design > Components > Buttons, and Danger buttons / Button outline for the danger variant and emphasis border), so it matches the built-in Button widget. Props: `title`, `onPress` (omit it for a purely visual button inside something else that is pressable), `variant` ("primary" default | "secondary" | "outlined" | "danger"), `icon` (a lucide name) with `iconPosition` ("leading" | "trailing"), `disabled`, `accessibilityLabel`, `testID`, `style` (extra container style, e.g. `alignSelf`) and `labelStyle`. Pass `fields` — `{ background, textColor, borderColor, borderWidth, paddingX, paddingY, radius, fontWeight, fontSize, shadow, gradient }` — to override one button, e.g. from style fields your widget declares. On a section that paints its own fill (`theme.colors.backdrop` there), a variant colour that the section makes unreadable takes that section\'s text colour, as its text does: an outlined label and border, a secondary whose fill disappears becomes outlined, and a primary or danger fill turns inverse (that text colour as its fill, its own colour as its label). A white or lightly tinted panel changes nothing, and a button inside a `<Card>` is measured against that card. A colour you pass in `fields`, `style` or `labelStyle`, or that the app\'s button look sets, is never changed. Never hand-roll a button from `Pressable` + background + radius.',
3353
3371
  rnComponent: null,
3354
3372
  docsUrl: null,
3355
3373
  },
3356
3374
  {
3357
3375
  name: "TextField",
3358
3376
  description:
3359
- 'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works, and its background, text colour, border, corner radius and height come from the app\'s input look (Theme Settings > Muted), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. `label` draws a label above the field and names it for a screen reader, `helper` a hint below, `error` a message in the helper\'s place with a danger border (`true` marks it without a message), `disabled` makes it read-only and dimmed; `containerStyle` styles the box around a labelled field. Use it instead of styling a bare `TextInput`.',
3377
+ 'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works, and its background, text colour, border, corner radius and height come from the app\'s input look (Design > Components > Inputs), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. `label` draws a label above the field and names it for a screen reader, `helper` a hint below, `error` a message in the helper\'s place with a danger border (`true` marks it without a message), `disabled` makes it read-only and dimmed; `containerStyle` styles the box around a labelled field. Use it instead of styling a bare `TextInput`.',
3360
3378
  rnComponent: null,
3361
3379
  docsUrl: null,
3362
3380
  },
3363
3381
  {
3364
3382
  name: "Checkbox",
3365
3383
  description:
3366
- 'Themed tick box. `<Checkbox label="Remember me" value={checked} onValueChange={setChecked} />` — THE way to draw a yes/no choice that is submitted with a form, or one row of a multi-select list. The box is the app\'s input surface (Design > Components > Muted) and fills with the primary colour when checked; focus draws a ring in the primary colour around it. The whole row is one 44px target with the checkbox role and checked state on both hosts, and Space toggles it on the web. Props: `value` (boolean), `onValueChange(next)`, `label` (text, or an element), `helper`, `error` (a message, or `true`), `disabled`, `accessibilityLabel` (required when there is no text `label`), `style` (the row), `containerStyle` (the box around a row that shows a helper or error), `testID`; any other prop lands on the row. Never hand-roll a tick box from `Pressable` + a bordered `View` + a "✓".',
3384
+ 'Themed tick box. `<Checkbox label="Remember me" value={checked} onValueChange={setChecked} />` — THE way to draw a yes/no choice that is submitted with a form, or one row of a multi-select list. The box is the app\'s input surface (Design > Components > Inputs) and fills with the primary colour when checked; focus draws a ring in the primary colour around it. The whole row is one 44px target with the checkbox role and checked state on both hosts, and Space toggles it on the web. Props: `value` (boolean), `onValueChange(next)`, `label` (text, or an element), `helper`, `error` (a message, or `true`), `disabled`, `accessibilityLabel` (required when there is no text `label`), `style` (the row), `containerStyle` (the box around a row that shows a helper or error), `testID`; any other prop lands on the row. Never hand-roll a tick box from `Pressable` + a bordered `View` + a "✓".',
3367
3385
  rnComponent: null,
3368
3386
  docsUrl: null,
3369
3387
  },
3370
3388
  {
3371
3389
  name: "Select",
3372
3390
  description:
3373
- 'Themed single-choice picker. `<Select label="Status" value={status} onValueChange={setStatus} options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} placeholder="Choose a status" />` — THE way to let the user pick ONE of a known list. The trigger is the app\'s input field (Design > Components > Muted) showing the chosen label; pressing it opens the options at SCREEN level, in a sheet painted from Design > Components > Popover, so no card or scroll container can clip them, and a long list scrolls inside the sheet. The trigger is a button that opens a dialog (expanded state included) and each option carries the option role and its selected state on both hosts; the list around them is a listbox on the web only. On the web Enter, Space or an arrow key opens it, the arrow keys, Home and End move between options, Enter or Space chooses, Escape closes, and focus returns to the trigger; on Android the back button closes it. Props: `options` (`[{ value, label, disabled?, testID? }]`, `value` a string or number, unique), `value`, `onValueChange(value, option)`, `placeholder` (default "Select"), `title` (the sheet\'s heading; defaults to a text `label`), `emptyText` (default "No options"), `label`, `helper`, `error`, `disabled`, `accessibilityLabel`, `closeAccessibilityLabel`, `background` / `textColor` / `borderColor` / `radius` / `height` to override one field, `style` (the trigger), `containerStyle`, `testID`; any other prop lands on the trigger. Never hand-roll a dropdown from `Pressable` + an absolutely positioned list: it is clipped by the widget\'s container on both hosts.',
3391
+ 'Themed single-choice picker. `<Select label="Status" value={status} onValueChange={setStatus} options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} placeholder="Choose a status" />` — THE way to let the user pick ONE of a known list. The trigger is the app\'s input field (Design > Components > Inputs) showing the chosen label; pressing it opens the options at SCREEN level, in a sheet painted from Design > Components > Dropdown menus, so no card or scroll container can clip them, and a long list scrolls inside the sheet. The trigger is a button that opens a dialog (expanded state included) and each option carries the option role and its selected state on both hosts; the list around them is a listbox on the web only. On the web Enter, Space or an arrow key opens it, the arrow keys, Home and End move between options, Enter or Space chooses, Escape closes, and focus returns to the trigger; on Android the back button closes it. Props: `options` (`[{ value, label, disabled?, testID? }]`, `value` a string or number, unique), `value`, `onValueChange(value, option)`, `placeholder` (default "Select"), `title` (the sheet\'s heading; defaults to a text `label`), `emptyText` (default "No options"), `label`, `helper`, `error`, `disabled`, `accessibilityLabel`, `closeAccessibilityLabel`, `background` / `textColor` / `borderColor` / `radius` / `height` to override one field, `style` (the trigger), `containerStyle`, `testID`; any other prop lands on the trigger. Never hand-roll a dropdown from `Pressable` + an absolutely positioned list: it is clipped by the widget\'s container on both hosts.',
3374
3392
  rnComponent: null,
3375
3393
  docsUrl: null,
3376
3394
  },
3377
3395
  {
3378
3396
  name: "SearchField",
3379
3397
  description:
3380
- 'Themed search box. `<SearchField value={query} onChangeText={setQuery} placeholder="Search orders" />` — THE way to draw a field that filters or searches. It IS a `TextField` (same look, same props, same `label` / `helper` / `error` / `disabled`) with a search glyph, the search key on the on-screen keyboard and a clear control that appears once there is text; clearing calls `onChangeText("")`, then `onClear` if you passed one, and keeps focus in the field. `placeholder` defaults to "Search" and `clearAccessibilityLabel` to "Clear search" — pass translated ones. `style` styles the input and `containerStyle` the box around it (use it for `flex: 1` in a toolbar). Debounce the query yourself before you fetch. Never hand-roll a search box from a `TextInput` + an icon.',
3398
+ 'Themed search box. `<SearchField value={query} onChangeText={setQuery} placeholder="Search orders" />` — THE way to draw a field that filters or searches. It IS a `TextField` (same props, same `label` / `helper` / `error` / `disabled`, the input look restyled by Design > Components > Search fields) with a search glyph, the search key on the on-screen keyboard and a clear control that appears once there is text; clearing calls `onChangeText("")`, then `onClear` if you passed one, and keeps focus in the field. `placeholder` defaults to "Search" and `clearAccessibilityLabel` to "Clear search" — pass translated ones. `style` styles the input and `containerStyle` the box around it (use it for `flex: 1` in a toolbar). Where the glyph sits (start, end or hidden), the corner radius, background, border colour and height come from the app; `iconPosition` ("start" | "end" | "none") overrides the glyph for one field, and at the end the clear control takes the glyph\'s place while there is text. Debounce the query yourself before you fetch. Never hand-roll a search box from a `TextInput` + an icon, and never add a magnifier of your own.',
3381
3399
  rnComponent: null,
3382
3400
  docsUrl: null,
3383
3401
  },
@@ -6049,7 +6067,12 @@ const CONTRACT = deepFreeze({
6049
6067
  // calendar sheet instead of the browser's date popup; both hosts show
6050
6068
  // the value in the app's language with a clear button. New optional
6051
6069
  // props: placeholder, clearLabel, doneLabel, closeAccessibilityLabel, testID.
6052
- version: "1.190.0",
6070
+ // 1.191.0: additive (sc-11759) — `themeComponents.search` styles every
6071
+ // `<SearchField>`: icon position, corner radius, background, border
6072
+ // colour, height, each falling back to the `muted` field. The field
6073
+ // takes an `iconPosition` prop. Scope labels renamed (sc-11763); keys
6074
+ // unchanged.
6075
+ version: "1.191.0",
6053
6076
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6054
6077
  hooks: HOOKS,
6055
6078
  primitives: PRIMITIVES,
@@ -6065,6 +6088,7 @@ const CONTRACT = deepFreeze({
6065
6088
  themeColorTokens: THEME_COLOR_TOKENS,
6066
6089
  themeComponents: THEME_COMPONENTS,
6067
6090
  themeComponentTabStyles: THEME_TAB_STYLES,
6091
+ themeComponentSearchIconPositions: THEME_SEARCH_ICON_POSITIONS,
6068
6092
  themeComponentShadows: THEME_COMPONENT_SHADOWS,
6069
6093
  themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
6070
6094
  themeComponentGradient: THEME_COMPONENT_GRADIENT,