@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 +29 -4
- package/dist/checkbox-view.js +28 -16
- package/dist/contract.cjs +57 -24
- package/dist/contract.js +57 -24
- package/dist/file-upload-logic.js +178 -0
- package/dist/file-upload-view.js +321 -0
- package/dist/file-upload.js +62 -0
- package/dist/file-upload.native.js +11 -0
- package/dist/filepicker.js +11 -0
- package/dist/filepicker.native.js +10 -1
- package/dist/form-control-view.js +8 -1
- package/dist/host.d.ts +6 -0
- package/dist/host.js +2 -0
- package/dist/index.d.ts +168 -19
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/list-tokens.js +54 -14
- package/dist/list-view.js +199 -82
- package/dist/list.js +7 -1
- package/dist/list.native.js +3 -2
- package/dist/primitives.js +1 -3
- package/dist/primitives.native.js +2 -4
- package/dist/search-field-tokens.js +85 -0
- package/dist/search-field-view.js +39 -26
- package/dist/select-logic.js +83 -0
- package/dist/select-view.js +165 -32
- package/dist/select.js +6 -1
- package/dist/select.native.js +5 -3
- package/package.json +1 -1
- package/src/contract.js +57 -24
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.
|
|
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
|
|
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
|
|
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,
|
|
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.
|
package/dist/checkbox-view.js
CHANGED
|
@@ -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
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
//
|
|
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: "
|
|
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 >
|
|
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>`
|
|
3131
|
-
//
|
|
3132
|
-
//
|
|
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
|
|
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
|
|
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 (
|
|
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 (
|
|
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 >
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|