@colixsystems/widget-sdk 0.236.0 → 0.238.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,32 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
84
84
 
85
85
  ## Status
86
86
 
87
- `v0.236.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.238.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.238.0 (contract 1.192.0)
90
+
91
+ **A standard upload field: `<FileUpload>`.** Widgets wrapped the headless `<FilePicker>` round buttons they drew themselves, so every upload control looked different. `<FileUpload>` is one view bound to each host's own `FilePicker`: the app's button inside the input field (Design > Components > Buttons and Inputs), the chosen files listed with their size and a remove control, and the form controls' `label`, `helper`, `error` and `disabled`. A file that `accept` rules out is refused with an error. It never uploads itself: call `upload(file)` from `useFilestoreUpload` in `onPick` and pass `uploading`, which shows an indeterminate spinner and locks the field. With `multiple`, `onPick` receives an array of the new files; `files` + `onFilesChange` let you own the list. On the web a file can also be dropped onto the field; React Native has no file drop, so on a device the button, which opens the system document picker, is the control. `<FilePicker>` gains `onFocus` / `onBlur`, and `FilePicker.isSupported` is documented as true on both hosts.
92
+
93
+ ```jsx
94
+ const { upload, uploading } = useFilestoreUpload({ spaceType: "project" });
95
+ <FileUpload label="Attachment" accept="application/pdf,image/*" uploading={uploading} onPick={(file) => upload(file)} />
96
+ ```
97
+
98
+ **Lists of cards and row actions.** `<List variant="card">` turns every `<ListRow>` into its own card: the surface follows Design > Components > Cards and the inside follows Lists, so a list of cards matches the rest of the app without wrapping rows in `<Card>` by hand. Buttons that act on one record go in `<ListRow actions>`, which stay outside the row's own tap target. `defaults` on `<List>` / `<ListRow>` is a widget's own row look, used only where the theme's Lists settings are unset. Existing rows render unchanged.
99
+
100
+ **Pick several with `<Select multiple>`.** `Select` takes `multiple` with `values` / `onValuesChange(values, options)`: the same field and option sheet, a tick box beside each option, and a Done button that hands you the new array in listed order (only when it changed). Closing the sheet by the backdrop, Escape or Android back keeps the previous choice. The trigger shows the picked labels as compact chips while they fit on its line, otherwise "N selected" (`selectedCountText`; `doneText` translates the button). On the web the list is multiselectable and Space or Enter toggles the focused option. For a handful of options always on show, use `<ChipGroup selection="multiple">` instead.
101
+
102
+ ```jsx
103
+ <Select multiple label={t("tags", "Tags")} values={tags} onValuesChange={setTags}
104
+ doneText={t("done", "Done")} selectedCountText={(n) => `${n} ${t("picked", "picked")}`}
105
+ options={[{ value: "red", label: "Red" }, { value: "blue", label: "Blue" }]} />
106
+ ```
107
+
108
+ ### What's new in 0.237.0 (contract 1.191.0)
109
+
110
+ **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.
111
+
112
+ **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.
88
113
 
89
114
  ### What's new in 0.236.0 (contract 1.190.0)
90
115
 
@@ -1190,7 +1215,7 @@ A binding may now carry an opacity: `"theme:colors.primary/20"` is the primary b
1190
1215
  ```
1191
1216
 
1192
1217
  - **`<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.
1193
- - **`<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.
1218
+ - **`<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.
1194
1219
  - **`<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.
1195
1220
 
1196
1221
  The card and button resolvers (`cardOverlay`, `shadowStyle`, `marginStyle`, `buttonSurfaceOverlay`, …) are exported too. Additive: the built-in widgets render exactly as before.
@@ -2920,11 +2945,11 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
2920
2945
  - `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.
2921
2946
  - `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`.
2922
2947
  - `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`.
2923
- - `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`.
2948
+ - `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`.
2924
2949
  - `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.
2925
2950
  - `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.
2926
2951
  - `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.
2927
- - `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`.
2952
+ - `Checkbox`, `Switch`, `Select`, `SearchField` — the themed tick box, on/off toggle, 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; with `multiple` it takes `values` / `onValuesChange`, ticks options in the same sheet and commits them on Done. Never hand-roll one from `Pressable` and `View`.
2928
2953
  - `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.
2929
2954
  - `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.
2930
2955
  - `textRoleStyle(role, typography, colors)`, `TEXT_ROLES` — the style of a semantic text tier, shared with the built-in widgets.
@@ -28,6 +28,33 @@ import { makeFieldFrame } from "./form-control-view.js";
28
28
 
29
29
  const h = React.createElement;
30
30
 
31
+ /**
32
+ * The tick box itself, drawn by `<Checkbox>` and by each option of a
33
+ * `<Select multiple>`, so the two always look alike.
34
+ *
35
+ * @param {any} View — the host's View primitive.
36
+ * @param {object} t — resolved form-control tokens.
37
+ * @param {{checked: boolean, invalid?: boolean}} state
38
+ */
39
+ export function checkboxBox(View, t, { checked, invalid = false }) {
40
+ return h(
41
+ View,
42
+ {
43
+ style: {
44
+ width: CHECKBOX_SIZE,
45
+ height: CHECKBOX_SIZE,
46
+ alignItems: "center",
47
+ justifyContent: "center",
48
+ borderWidth: 1,
49
+ borderRadius: Math.min(t.radius, CHECKBOX_MAX_RADIUS),
50
+ borderColor: checked ? t.accent : invalid ? t.errorColor : t.borderColor,
51
+ backgroundColor: checked ? t.accent : t.background,
52
+ },
53
+ },
54
+ checked ? h(Icon, { name: "check", size: 16, color: t.onAccent }) : null,
55
+ );
56
+ }
57
+
31
58
  /**
32
59
  * Bind the shared checkbox to one platform's primitives.
33
60
  *
@@ -103,22 +130,7 @@ export function makeCheckbox(rn) {
103
130
  h(
104
131
  View,
105
132
  { style: focusHaloStyle(t, focused, radius) },
106
- h(
107
- View,
108
- {
109
- style: {
110
- width: CHECKBOX_SIZE,
111
- height: CHECKBOX_SIZE,
112
- alignItems: "center",
113
- justifyContent: "center",
114
- borderWidth: 1,
115
- borderRadius: radius,
116
- borderColor: checked ? t.accent : invalid ? t.errorColor : t.borderColor,
117
- backgroundColor: checked ? t.accent : t.background,
118
- },
119
- },
120
- checked ? h(Icon, { name: "check", size: 16, color: t.onAccent }) : null,
121
- ),
133
+ checkboxBox(View, t, { checked, invalid }),
122
134
  ),
123
135
  isText(label)
124
136
  ? h(
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,9 +620,9 @@ 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
- sdkComponents: Object.freeze(["TextField", "Checkbox", "Switch", "Select", "SearchField"]),
625
+ sdkComponents: Object.freeze(["TextField", "Checkbox", "Switch", "Select", "SearchField", "FileUpload"]),
623
626
  tokens: Object.freeze({
624
627
  background: Object.freeze({ type: "color", uiDefault: "colors.surfaceMuted" }),
625
628
  textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
@@ -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
  },
@@ -3127,16 +3145,13 @@ const PRIMITIVES = [
3127
3145
  rnComponent: "@react-native-community/datetimepicker",
3128
3146
  docsUrl: "https://github.com/react-native-datetimepicker/datetimepicker",
3129
3147
  },
3130
- // sc-1378 — `<FilePicker accept onPick>` lets a widget surface a native
3131
- // file-picker on the web Player so end users can upload files to the
3132
- // backend (paired with useFilestoreUpload). Web wraps
3133
- // a hidden DOM `<input type="file">`; native renders a disabled trigger
3134
- // and exposes `FilePicker.isSupported = false` until a vetted Expo
3135
- // picker pin lands.
3148
+ // sc-1378 — `<FilePicker accept onPick>` opens the system file picker so
3149
+ // end users can upload files (paired with useFilestoreUpload). Web wraps a
3150
+ // hidden DOM `<input type="file">`; native wraps expo-document-picker.
3136
3151
  {
3137
3152
  name: "FilePicker",
3138
3153
  description:
3139
- 'End-user file picker primitive. `<FilePicker accept="image/*" multiple={false} disabled={false} onPick={(file) => …}>…trigger…</FilePicker>`. Children render as the visible click target (a Pressable + Icon + Text is the common shape) and are wrapped in a hidden-input <label> so the click reaches the file dialog without any ref plumbing. Pair with useFilestoreUpload to upload the picked File to the end-user filestore. `FilePicker.isSupported` is `true` on the web Player and `false` on the native Expo export — widgets should branch on it to hide the trigger when uploads are not yet wired natively.',
3154
+ 'End-user file picker primitive. `<FilePicker accept="image/*" multiple={false} disabled={false} onPick={(file) => …}>…trigger…</FilePicker>`. Children render as the visible click target (a Pressable + Icon + Text is the common shape) and are wrapped in a hidden-input <label> so the click reaches the file dialog without any ref plumbing. Pair with useFilestoreUpload to upload the picked File to the end-user filestore. `onFocus` / `onBlur` report the picker\'s keyboard focus, so a trigger you draw can show it. `FilePicker.isSupported` is `true` on the web Player and on the native Expo export, which opens the system document picker; a host without a picker sets it `false`, so disable the trigger and say why. For an upload field use `<FileUpload>`, which draws the button, the chosen files and the uploading and error states around this picker; the bare FilePicker is for a trigger that is not a field, such as a photo the user taps to replace.',
3140
3155
  rnComponent: null,
3141
3156
  docsUrl: null,
3142
3157
  },
@@ -3202,7 +3217,7 @@ const PRIMITIVES = [
3202
3217
  {
3203
3218
  name: "ConfirmDialog",
3204
3219
  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.',
3220
+ '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
3221
  rnComponent: null,
3207
3222
  docsUrl: null,
3208
3223
  },
@@ -3237,14 +3252,14 @@ const PRIMITIVES = [
3237
3252
  {
3238
3253
  name: "List",
3239
3254
  description:
3240
- "Themed list container. `<List>{rows.map((r) => <ListRow key={r.id} title={r.name} />)}</List>`. Spaces and insets its `<ListRow>` children the way the app's look says (flush rows with hairlines, or separated rounded rows), and hands any row override you pass (`rowPadding`, `rowMinHeight`, `gap`, `radius`, `background`, `dividerColor`, `dividerWidth`) down to every row. Props: those overrides, `style`, `testID`. Put it inside a `ScrollView` (or bound its height) for long lists.",
3255
+ "Themed list container. `<List>{rows.map((r) => <ListRow key={r.id} title={r.name} />)}</List>`. Spaces and insets its `<ListRow>` children the way the app's look says (flush rows with hairlines, or separated rounded rows), and hands any row override you pass (`rowPadding`, `rowMinHeight`, `gap`, `radius`, `background`, `dividerColor`, `dividerWidth`) down to every row. `<List variant=\"card\">` draws EVERY row as its own card instead: THE way to build a list of cards (one record per card, stacked in one column). Each card's surface (fill, border, corner radius, padding, shadow, gradient) is the app's card look (Design > Components > Cards), what is inside it (height, the leading / body / trailing arrangement) is its Lists look (Design > Components > Lists), and the cards are spaced by the Lists gap, or a small step when that is unset; never wrap hand-made rows in `<Card>`s for this. `defaults` takes the same keys as the overrides and is used only where the app's Lists settings leave a value unset (a built-in widget keeps its own row look with it); leave it out in a new widget so the app decides. Props: those overrides, `variant` (\"plain\" default | \"card\"), `defaults`, `style`, `testID`. Put it inside a `ScrollView` (or bound its height) for long lists.",
3241
3256
  rnComponent: null,
3242
3257
  docsUrl: null,
3243
3258
  },
3244
3259
  {
3245
3260
  name: "ListRow",
3246
3261
  description:
3247
- 'Themed list row. `<ListRow title={item.name} subtitle={item.email} leading={<Icon name="user" size={20} />} trailing={<Text>…</Text>} onPress={() => open(item)} selected={item.id === selectedId} />`. THE way to draw one row of records: its padding, minimum height, divider, corner radius and background come from the theme\'s Lists settings, so every list in the app matches — never hand-roll a row from `View` + `Pressable`. Props: `title`, `subtitle` (rendered in the subtitle / caption text tiers), `leading` / `trailing` nodes, `children` (replaces the title block: a single `View` child — your own row of cells — spans the full width between `leading` and `trailing`; anything else, such as several cells, sits in the row as given, which is the shape a table row takes under a `<ListHeader>`), `onPress` (makes it a button with a press state), `selected`, `disabled`, `divider` (default true; pass false on the last row), `accessibilityLabel`, `style`, `testID`, plus the same per-row overrides `<List>` takes. Declare `listRowPadding` / `listRowRadius` / … style fields and pass them through to let an author override one widget.',
3262
+ 'Themed list row. `<ListRow title={item.name} subtitle={item.email} leading={<Icon name="user" size={20} />} trailing={<Text>…</Text>} onPress={() => open(item)} selected={item.id === selectedId} />`. THE way to draw one row of records: its padding, minimum height, divider, corner radius and background come from the theme\'s Lists settings, so every list in the app matches — never hand-roll a row from `View` + `Pressable`. Props: `title`, `subtitle` (rendered in the subtitle / caption text tiers), `leading` / `trailing` nodes, `children` (replaces the title block: a single `View` child — your own row of cells — spans the full width between `leading` and `trailing`; anything else, such as several cells, sits in the row as given, which is the shape a table row takes under a `<ListHeader>`), `onPress` (makes it a button with a press state), `actions` (the row\'s OWN buttons, such as remove, move up or sign: drawn after `trailing` and, on a row with `onPress`, kept OUTSIDE its tap target so each stays a separate button the keyboard and a screen reader reach; never put a `Pressable` in `trailing` of a pressable row), `selected`, `disabled`, `divider` (default true; pass false on the last row), `accessibilityLabel`, `style`, `testID`, `defaults`, plus the same per-row overrides `<List>` takes. Inside a `<List variant="card">` the row is drawn as one card. Declare `listRowPadding` / `listRowRadius` / … style fields and pass them through to let an author override one widget.',
3248
3263
  rnComponent: null,
3249
3264
  docsUrl: null,
3250
3265
  },
@@ -3349,35 +3364,35 @@ const PRIMITIVES = [
3349
3364
  {
3350
3365
  name: "Button",
3351
3366
  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.',
3367
+ '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
3368
  rnComponent: null,
3354
3369
  docsUrl: null,
3355
3370
  },
3356
3371
  {
3357
3372
  name: "TextField",
3358
3373
  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`.',
3374
+ '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
3375
  rnComponent: null,
3361
3376
  docsUrl: null,
3362
3377
  },
3363
3378
  {
3364
3379
  name: "Checkbox",
3365
3380
  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 "✓".',
3381
+ '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
3382
  rnComponent: null,
3368
3383
  docsUrl: null,
3369
3384
  },
3370
3385
  {
3371
3386
  name: "Select",
3372
3387
  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.',
3388
+ 'Themed picker for ONE value of a known list, or several with `multiple`. `<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. MULTIPLE: `<Select multiple label="Tags" values={tags} onValuesChange={setTags} options={tagOptions} />` — THE way to let the user pick SEVERAL of a known list. The same trigger and sheet; each option shows a tick box and toggles, the listbox is multiselectable on the web, and the sheet\'s Done button commits the choice through `onValuesChange(values, options)` (values in listed order, called only when they changed); closing the sheet any other way (backdrop, Escape, Android back) keeps the previous choice. Space toggles the focused option on the web. The trigger shows the picked labels as small chips while they fit on its one line, otherwise "N selected" (`selectedCountText(count)` to translate it), and `placeholder` while nothing is picked; `doneText` (default "Done") names the button. In this mode `value` / `onValueChange` are ignored. A handful of options always shown inline is a `<ChipGroup selection="multiple">` of `<Chip>`s instead.',
3374
3389
  rnComponent: null,
3375
3390
  docsUrl: null,
3376
3391
  },
3377
3392
  {
3378
3393
  name: "SearchField",
3379
3394
  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.',
3395
+ '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
3396
  rnComponent: null,
3382
3397
  docsUrl: null,
3383
3398
  },
@@ -3402,6 +3417,14 @@ const PRIMITIVES = [
3402
3417
  rnComponent: null,
3403
3418
  docsUrl: null,
3404
3419
  },
3420
+ // sc-11751 — the standard upload field, built on `<FilePicker>`.
3421
+ {
3422
+ name: "FileUpload",
3423
+ description:
3424
+ 'Themed file chooser. `<FileUpload label="Attachments" accept="image/*,.pdf" multiple files={files} onFilesChange={setFiles} />` — THE way to let the user pick files to upload, whether they upload at once or with a form. It is built on `<FilePicker>`: a button (Design > Components > Buttons) inside the app\'s input field (Design > Components > Inputs), the chosen files listed under it with their name, size and a remove control, and the same `label` / `helper` / `error` / `disabled` as every form control. The button opens the system file picker on both hosts and is the only way in on a device; on the web a file can also be dropped onto the field. A picked or dropped file that `accept` rules out is not added and the field shows an error. It uploads nothing itself: call `upload(file)` from `useFilestoreUpload` in `onPick` (or when the form is submitted) and pass `uploading` while it runs; the field then shows a `<Spinner>` with "Uploading…" and locks (no percentage is reported). Props: `accept` (an HTML accept value: "image/*", ".pdf", "application/pdf"), `multiple` (default false: a new pick replaces the file), `files` + `onFilesChange(files)` to own the list (omit both and it keeps its own; pass `files={[]}` to list nothing), `onPick(file)` (an array when `multiple`) once per pick or drop with only the new files, `onRemove(file, index)`, `onReject(files, reason)` (reason "type" for a file `accept` rules out, "count" for the files beyond the first when not `multiple`), `uploading`, `label`, `helper`, `error` (a message, or `true`), `disabled`, `buttonLabel` (default "Choose file" / "Choose files"), `buttonVariant` ("primary" | "secondary" | "outlined", the default), `uploadingLabel`, `emptyLabel`, `dropLabel`, `rejectedMessage`, `singleFileMessage`, `unsupportedMessage`, `removeAccessibilityLabel`, `accessibilityLabel`, `style` (the field), `containerStyle`, `testID` (also tags `<testID>-button`, `<testID>-file-<i>` and `<testID>-remove-<i>`). Pass translated copy for every label. Never hand-build an upload control from `<FilePicker>` plus your own button, file list or "Uploading…" text.',
3425
+ rnComponent: null,
3426
+ docsUrl: null,
3427
+ },
3405
3428
  ];
3406
3429
 
3407
3430
  const CATEGORIES = [
@@ -6053,7 +6076,16 @@ const CONTRACT = deepFreeze({
6053
6076
  // calendar sheet instead of the browser's date popup; both hosts show
6054
6077
  // the value in the app's language with a clear button. New optional
6055
6078
  // props: placeholder, clearLabel, doneLabel, closeAccessibilityLabel, testID.
6056
- version: "1.190.0",
6079
+ // 1.191.0: additive (sc-11759) — `themeComponents.search` styles every
6080
+ // `<SearchField>`: icon position, corner radius, background, border
6081
+ // colour, height, each falling back to the `muted` field. The field
6082
+ // takes an `iconPosition` prop. Scope labels renamed (sc-11763); keys
6083
+ // unchanged.
6084
+ // 1.192.0: additive (sc-11751, sc-11752, sc-11758) — new `<FileUpload>`
6085
+ // on `<FilePicker>` (which gains onFocus/onBlur; `muted` lists it).
6086
+ // `<List variant="card">`, `<ListRow actions>`, `defaults` on both.
6087
+ // `<Select multiple values onValuesChange>`.
6088
+ version: "1.192.0",
6057
6089
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6058
6090
  hooks: HOOKS,
6059
6091
  primitives: PRIMITIVES,
@@ -6069,6 +6101,7 @@ const CONTRACT = deepFreeze({
6069
6101
  themeColorTokens: THEME_COLOR_TOKENS,
6070
6102
  themeComponents: THEME_COMPONENTS,
6071
6103
  themeComponentTabStyles: THEME_TAB_STYLES,
6104
+ themeComponentSearchIconPositions: THEME_SEARCH_ICON_POSITIONS,
6072
6105
  themeComponentShadows: THEME_COMPONENT_SHADOWS,
6073
6106
  themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
6074
6107
  themeComponentGradient: THEME_COMPONENT_GRADIENT,