@colixsystems/widget-sdk 0.237.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 +21 -2
- package/dist/checkbox-view.js +28 -16
- package/dist/contract.cjs +21 -12
- package/dist/contract.js +21 -12
- 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/index.d.ts +155 -16
- 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/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 +21 -12
package/dist/select-view.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
//
|
|
5
5
|
// The trigger wears the field `<TextField>` does. The options open in
|
|
6
6
|
// `<Overlay>`, at screen level, so no card or scroll container the widget
|
|
7
|
-
// sits in can clip them; a long list scrolls inside the sheet.
|
|
7
|
+
// sits in can clip them; a long list scrolls inside the sheet. With
|
|
8
|
+
// `multiple` the same sheet ticks options into a draft that Done commits.
|
|
8
9
|
|
|
9
10
|
import React from "react";
|
|
10
11
|
import { useHostTheme } from "./hooks.js";
|
|
@@ -29,17 +30,34 @@ import {
|
|
|
29
30
|
spokenLabel,
|
|
30
31
|
} from "./form-control-logic.js";
|
|
31
32
|
import { makeFieldFrame } from "./form-control-view.js";
|
|
33
|
+
import { checkboxBox } from "./checkbox-view.js";
|
|
34
|
+
import {
|
|
35
|
+
committedValues,
|
|
36
|
+
defaultSelectedCountText,
|
|
37
|
+
initialFocusIndex,
|
|
38
|
+
pickedOptions,
|
|
39
|
+
sameValues,
|
|
40
|
+
toggleValue,
|
|
41
|
+
triggerSummary,
|
|
42
|
+
valueList,
|
|
43
|
+
} from "./select-logic.js";
|
|
32
44
|
|
|
33
45
|
const h = React.createElement;
|
|
34
46
|
const NAVIGATION_KEYS = ["ArrowDown", "ArrowUp", "Home", "End"];
|
|
47
|
+
// Wide enough that no picked chip shrinks while it is measured.
|
|
48
|
+
const MEASURE_SPAN = 10000;
|
|
49
|
+
// A compact chip that fits the field's own line, so the trigger keeps its height.
|
|
50
|
+
const TRIGGER_CHIP_STYLE = Object.freeze({ minHeight: 0, paddingVertical: 2 });
|
|
51
|
+
const UNMEASURED = Object.freeze({ available: null, needed: null });
|
|
35
52
|
|
|
36
53
|
/**
|
|
37
54
|
* Bind the shared select to one platform's primitives.
|
|
38
55
|
*
|
|
39
|
-
* @param {{View: any, Text: any, Pressable: any, ScrollView: any, Platform: any,
|
|
56
|
+
* @param {{View: any, Text: any, Pressable: any, ScrollView: any, Platform: any,
|
|
57
|
+
* Overlay: any, Button: any, Chip: any}} rn
|
|
40
58
|
*/
|
|
41
59
|
export function makeSelect(rn) {
|
|
42
|
-
const { View, Text, Pressable, ScrollView, Platform, Overlay } = rn;
|
|
60
|
+
const { View, Text, Pressable, ScrollView, Platform, Overlay, Button, Chip } = rn;
|
|
43
61
|
const FieldFrame = makeFieldFrame(rn);
|
|
44
62
|
|
|
45
63
|
// Keyboard focus is a web concern: a device moves its reader's focus into
|
|
@@ -48,11 +66,11 @@ export function makeSelect(rn) {
|
|
|
48
66
|
if (Platform.OS === "web" && node && typeof node.focus === "function") node.focus();
|
|
49
67
|
}
|
|
50
68
|
|
|
51
|
-
function OptionSheet(
|
|
69
|
+
function OptionSheet(props) {
|
|
70
|
+
const { t, options, isPicked, multiple, title, emptyText, listId, onPick, testID, footer } =
|
|
71
|
+
props;
|
|
52
72
|
const nodes = React.useRef([]);
|
|
53
|
-
const
|
|
54
|
-
const firstEnabled = options.findIndex((option) => !option.disabled);
|
|
55
|
-
const initial = chosen !== -1 && !options[chosen].disabled ? chosen : firstEnabled;
|
|
73
|
+
const initial = initialFocusIndex(options, isPicked);
|
|
56
74
|
// Focus lands on the current choice once, when the sheet mounts it; the
|
|
57
75
|
// browser scrolls a focused option into view in a long list.
|
|
58
76
|
const pendingFocus = React.useRef(true);
|
|
@@ -81,12 +99,13 @@ export function makeSelect(rn) {
|
|
|
81
99
|
nativeID: listId,
|
|
82
100
|
// Web only: native has no listbox, and ignores an unknown `role`.
|
|
83
101
|
role: "listbox",
|
|
102
|
+
...(multiple ? { "aria-multiselectable": true } : null),
|
|
84
103
|
accessibilityLabel: isText(title) ? String(title) : undefined,
|
|
85
104
|
keyboardShouldPersistTaps: "handled",
|
|
86
105
|
testID: testID ? `${testID}-options` : undefined,
|
|
87
106
|
},
|
|
88
107
|
...options.map((option, index) => {
|
|
89
|
-
const selected = option
|
|
108
|
+
const selected = isPicked(option);
|
|
90
109
|
// The web host still delivers keys to a disabled Pressable.
|
|
91
110
|
const pick = () => {
|
|
92
111
|
if (!option.disabled) onPick(option);
|
|
@@ -121,7 +140,7 @@ export function makeSelect(rn) {
|
|
|
121
140
|
{
|
|
122
141
|
flexDirection: "row",
|
|
123
142
|
alignItems: "center",
|
|
124
|
-
justifyContent: "space-between",
|
|
143
|
+
justifyContent: multiple ? "flex-start" : "space-between",
|
|
125
144
|
gap: t.rowGap,
|
|
126
145
|
minHeight: CONTROL_MIN_TARGET,
|
|
127
146
|
paddingHorizontal: t.paddingHorizontal,
|
|
@@ -132,15 +151,93 @@ export function makeSelect(rn) {
|
|
|
132
151
|
option.disabled ? { opacity: t.disabledOpacity } : null,
|
|
133
152
|
],
|
|
134
153
|
},
|
|
154
|
+
multiple ? checkboxBox(View, t, { checked: selected }) : null,
|
|
135
155
|
h(
|
|
136
156
|
Text,
|
|
137
157
|
{ style: [text, { flexShrink: 1 }, selected ? { fontWeight: "600" } : null] },
|
|
138
158
|
option.label,
|
|
139
159
|
),
|
|
140
|
-
selected ? h(Icon, { name: "check", size: 18, color: t.accent }) : null,
|
|
160
|
+
selected && !multiple ? h(Icon, { name: "check", size: 18, color: t.accent }) : null,
|
|
141
161
|
);
|
|
142
162
|
}),
|
|
143
163
|
),
|
|
164
|
+
footer || null,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function DoneFooter({ doneText, onDone, testID }) {
|
|
169
|
+
return h(
|
|
170
|
+
View,
|
|
171
|
+
{ style: { flexDirection: "row", justifyContent: "flex-end" } },
|
|
172
|
+
h(Button, { title: doneText, onPress: onDone, testID: testID ? `${testID}-done` : undefined }),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Chips when they fit the field's one line, else the count; measured on
|
|
177
|
+
// both hosts, so a narrow trigger never grows to hold them.
|
|
178
|
+
function MultiSummary({ t, picked, placeholder, selectedCountText, testID }) {
|
|
179
|
+
const [widths, setWidths] = React.useState(UNMEASURED);
|
|
180
|
+
const countText =
|
|
181
|
+
typeof selectedCountText === "function" ? selectedCountText : defaultSelectedCountText;
|
|
182
|
+
const measure = (key) => (event) => {
|
|
183
|
+
const width = event.nativeEvent.layout.width;
|
|
184
|
+
setWidths((own) => (own[key] === width ? own : { ...own, [key]: width }));
|
|
185
|
+
};
|
|
186
|
+
const mode = triggerSummary({ picked: picked.length, ...widths });
|
|
187
|
+
const text = { fontFamily: t.fontFamily, fontSize: t.fontSize };
|
|
188
|
+
const chips = () =>
|
|
189
|
+
picked.map((option) =>
|
|
190
|
+
h(Chip, {
|
|
191
|
+
key: `${typeof option.value}:${option.value}`,
|
|
192
|
+
label: option.label,
|
|
193
|
+
size: "sm",
|
|
194
|
+
style: TRIGGER_CHIP_STYLE,
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
return h(
|
|
198
|
+
View,
|
|
199
|
+
{
|
|
200
|
+
onLayout: measure("available"),
|
|
201
|
+
style: { flexGrow: 1, flexShrink: 1, minWidth: 0, overflow: "hidden" },
|
|
202
|
+
testID: testID ? `${testID}-summary` : undefined,
|
|
203
|
+
},
|
|
204
|
+
mode === "chips"
|
|
205
|
+
? h(View, { style: { flexDirection: "row", gap: t.gap } }, ...chips())
|
|
206
|
+
: h(
|
|
207
|
+
Text,
|
|
208
|
+
{
|
|
209
|
+
numberOfLines: 1,
|
|
210
|
+
style: { ...text, color: mode === "count" ? t.textColor : t.placeholderColor },
|
|
211
|
+
},
|
|
212
|
+
mode === "count" ? countText(picked.length) : placeholder,
|
|
213
|
+
),
|
|
214
|
+
picked.length > 0
|
|
215
|
+
? h(
|
|
216
|
+
View,
|
|
217
|
+
{
|
|
218
|
+
"aria-hidden": true,
|
|
219
|
+
importantForAccessibility: "no-hide-descendants",
|
|
220
|
+
accessibilityElementsHidden: true,
|
|
221
|
+
style: {
|
|
222
|
+
position: "absolute",
|
|
223
|
+
top: 0,
|
|
224
|
+
left: 0,
|
|
225
|
+
width: MEASURE_SPAN,
|
|
226
|
+
opacity: 0,
|
|
227
|
+
pointerEvents: "none",
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
h(
|
|
231
|
+
View,
|
|
232
|
+
{
|
|
233
|
+
onLayout: measure("needed"),
|
|
234
|
+
style: { flexDirection: "row", alignSelf: "flex-start", gap: t.gap },
|
|
235
|
+
testID: testID ? `${testID}-measure` : undefined,
|
|
236
|
+
},
|
|
237
|
+
...chips(),
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
: null,
|
|
144
241
|
);
|
|
145
242
|
}
|
|
146
243
|
|
|
@@ -148,6 +245,11 @@ export function makeSelect(rn) {
|
|
|
148
245
|
const {
|
|
149
246
|
value,
|
|
150
247
|
onValueChange,
|
|
248
|
+
multiple = false,
|
|
249
|
+
values,
|
|
250
|
+
onValuesChange,
|
|
251
|
+
selectedCountText = defaultSelectedCountText,
|
|
252
|
+
doneText = "Done",
|
|
151
253
|
options,
|
|
152
254
|
placeholder = "Select",
|
|
153
255
|
title,
|
|
@@ -174,6 +276,8 @@ export function makeSelect(rn) {
|
|
|
174
276
|
}
|
|
175
277
|
const t = resolveFormControlTokens(theme, overrides);
|
|
176
278
|
const [open, setOpen] = React.useState(false);
|
|
279
|
+
// Null until the person ticks something: an untouched sheet follows `values`.
|
|
280
|
+
const [ownDraft, setDraft] = React.useState(null);
|
|
177
281
|
const [focused, setFocused] = React.useState(false);
|
|
178
282
|
const triggerRef = React.useRef(null);
|
|
179
283
|
const setRef = React.useMemo(() => shareRef(ref, triggerRef), [ref]);
|
|
@@ -181,14 +285,41 @@ export function makeSelect(rn) {
|
|
|
181
285
|
const noteId = React.useId();
|
|
182
286
|
const list = Array.isArray(options) ? options.filter(Boolean) : [];
|
|
183
287
|
const current = selectedOption(list, value);
|
|
288
|
+
const picked = multiple ? pickedOptions(list, values) : [];
|
|
289
|
+
const draft = ownDraft === null ? valueList(values) : ownDraft;
|
|
184
290
|
const invalid = isShown(error);
|
|
185
291
|
const sheetTitle = isShown(title) ? title : isText(label) ? label : undefined;
|
|
186
292
|
const name = spokenLabel(accessibilityLabel, label);
|
|
187
293
|
const shown = current ? current.label : placeholder;
|
|
294
|
+
const spokenChoice = multiple
|
|
295
|
+
? picked.length > 0
|
|
296
|
+
? picked.map((option) => option.label).filter(isText).join(", ")
|
|
297
|
+
: placeholder
|
|
298
|
+
: shown;
|
|
188
299
|
const show = () => {
|
|
189
|
-
if (
|
|
300
|
+
if (disabled) return;
|
|
301
|
+
if (multiple) setDraft(null);
|
|
302
|
+
setOpen(true);
|
|
190
303
|
};
|
|
304
|
+
// Closing any other way leaves the draft behind: only Done commits it.
|
|
191
305
|
const close = () => setOpen(false);
|
|
306
|
+
const done = () => {
|
|
307
|
+
const next = committedValues(list, draft);
|
|
308
|
+
close();
|
|
309
|
+
if (!sameValues(next, valueList(values)) && typeof onValuesChange === "function") {
|
|
310
|
+
onValuesChange(next, pickedOptions(list, next));
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
const pick = (option) => {
|
|
314
|
+
if (multiple) {
|
|
315
|
+
setDraft((own) => toggleValue(own === null ? valueList(values) : own, option.value));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
close();
|
|
319
|
+
if (option.value !== value && typeof onValueChange === "function") {
|
|
320
|
+
onValueChange(option.value, option);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
192
323
|
// The option that held focus is gone once the sheet closes. After the
|
|
193
324
|
// sheet unmounts, or its focus trap would pull focus straight back in.
|
|
194
325
|
const wasOpen = React.useRef(false);
|
|
@@ -239,7 +370,8 @@ export function makeSelect(rn) {
|
|
|
239
370
|
...(invalid ? { "aria-invalid": true } : null),
|
|
240
371
|
...(hasNote({ helper, error }) ? { "aria-describedby": noteId } : null),
|
|
241
372
|
// The choice is part of the name: a button's own label replaces its text.
|
|
242
|
-
accessibilityLabel:
|
|
373
|
+
accessibilityLabel:
|
|
374
|
+
[name, isText(spokenChoice) ? spokenChoice : null].filter(Boolean).join(", ") || undefined,
|
|
243
375
|
testID,
|
|
244
376
|
style: [
|
|
245
377
|
{
|
|
@@ -260,19 +392,21 @@ export function makeSelect(rn) {
|
|
|
260
392
|
style,
|
|
261
393
|
],
|
|
262
394
|
},
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
395
|
+
multiple
|
|
396
|
+
? h(MultiSummary, { t, picked, placeholder, selectedCountText, testID })
|
|
397
|
+
: h(
|
|
398
|
+
Text,
|
|
399
|
+
{
|
|
400
|
+
numberOfLines: 1,
|
|
401
|
+
style: {
|
|
402
|
+
flexShrink: 1,
|
|
403
|
+
color: current ? t.textColor : t.placeholderColor,
|
|
404
|
+
fontFamily: t.fontFamily,
|
|
405
|
+
fontSize: t.fontSize,
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
shown,
|
|
409
|
+
),
|
|
276
410
|
h(Icon, { name: "chevron-down", size: 18, color: t.placeholderColor }),
|
|
277
411
|
),
|
|
278
412
|
h(
|
|
@@ -293,17 +427,16 @@ export function makeSelect(rn) {
|
|
|
293
427
|
h(OptionSheet, {
|
|
294
428
|
t,
|
|
295
429
|
options: list,
|
|
296
|
-
|
|
430
|
+
multiple,
|
|
431
|
+
isPicked: multiple
|
|
432
|
+
? (option) => draft.includes(option.value)
|
|
433
|
+
: (option) => option.value === value,
|
|
297
434
|
title: sheetTitle,
|
|
298
435
|
emptyText,
|
|
299
436
|
listId,
|
|
300
437
|
testID,
|
|
301
|
-
onPick:
|
|
302
|
-
|
|
303
|
-
if (option.value !== value && typeof onValueChange === "function") {
|
|
304
|
-
onValueChange(option.value, option);
|
|
305
|
-
}
|
|
306
|
-
},
|
|
438
|
+
onPick: pick,
|
|
439
|
+
footer: multiple ? h(DoneFooter, { doneText, onDone: done, testID }) : null,
|
|
307
440
|
}),
|
|
308
441
|
),
|
|
309
442
|
);
|
package/dist/select.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// `<Select>` (web). Binds ./select-view.js to react-native-web and the web
|
|
2
|
-
// `<Overlay>`; see ./primitives.js for why not the
|
|
2
|
+
// `<Overlay>`, `<Button>` and `<Chip>`; see ./primitives.js for why not the
|
|
3
|
+
// bare specifier.
|
|
3
4
|
|
|
4
5
|
import * as ReactNative from "react-native-web";
|
|
5
6
|
import { makeSelect } from "./select-view.js";
|
|
6
7
|
import { Overlay } from "./overlay.js";
|
|
8
|
+
import { Button } from "./button.js";
|
|
9
|
+
import { Chip } from "./chip.js";
|
|
7
10
|
|
|
8
11
|
export const Select = makeSelect({
|
|
9
12
|
View: ReactNative.View,
|
|
@@ -12,4 +15,6 @@ export const Select = makeSelect({
|
|
|
12
15
|
ScrollView: ReactNative.ScrollView,
|
|
13
16
|
Platform: ReactNative.Platform,
|
|
14
17
|
Overlay,
|
|
18
|
+
Button,
|
|
19
|
+
Chip,
|
|
15
20
|
});
|
package/dist/select.native.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// `<Select>` (native). Binds ./select-view.js to React Native's own
|
|
2
|
-
// primitives and the native `<Overlay>`; only the
|
|
3
|
-
// ./select.js.
|
|
2
|
+
// primitives and the native `<Overlay>`, `<Button>` and `<Chip>`; only the
|
|
3
|
+
// imports differ from ./select.js.
|
|
4
4
|
|
|
5
5
|
import { View, Text, Pressable, ScrollView, Platform } from "react-native";
|
|
6
6
|
import { makeSelect } from "./select-view.js";
|
|
7
7
|
import { Overlay } from "./overlay.native.js";
|
|
8
|
+
import { Button } from "./button.native.js";
|
|
9
|
+
import { Chip } from "./chip.native.js";
|
|
8
10
|
|
|
9
|
-
export const Select = makeSelect({ View, Text, Pressable, ScrollView, Platform, Overlay });
|
|
11
|
+
export const Select = makeSelect({ View, Text, Pressable, ScrollView, Platform, Overlay, Button, Chip });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.238.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|
package/src/contract.js
CHANGED
|
@@ -622,7 +622,7 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
622
622
|
muted: Object.freeze({
|
|
623
623
|
label: "Inputs",
|
|
624
624
|
// The SDK's form controls read this scope directly, in every widget.
|
|
625
|
-
sdkComponents: Object.freeze(["TextField", "Checkbox", "Switch", "Select", "SearchField"]),
|
|
625
|
+
sdkComponents: Object.freeze(["TextField", "Checkbox", "Switch", "Select", "SearchField", "FileUpload"]),
|
|
626
626
|
tokens: Object.freeze({
|
|
627
627
|
background: Object.freeze({ type: "color", uiDefault: "colors.surfaceMuted" }),
|
|
628
628
|
textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
|
|
@@ -3145,16 +3145,13 @@ const PRIMITIVES = [
|
|
|
3145
3145
|
rnComponent: "@react-native-community/datetimepicker",
|
|
3146
3146
|
docsUrl: "https://github.com/react-native-datetimepicker/datetimepicker",
|
|
3147
3147
|
},
|
|
3148
|
-
// sc-1378 — `<FilePicker accept onPick>`
|
|
3149
|
-
//
|
|
3150
|
-
//
|
|
3151
|
-
// a hidden DOM `<input type="file">`; native renders a disabled trigger
|
|
3152
|
-
// and exposes `FilePicker.isSupported = false` until a vetted Expo
|
|
3153
|
-
// 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.
|
|
3154
3151
|
{
|
|
3155
3152
|
name: "FilePicker",
|
|
3156
3153
|
description:
|
|
3157
|
-
'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.',
|
|
3158
3155
|
rnComponent: null,
|
|
3159
3156
|
docsUrl: null,
|
|
3160
3157
|
},
|
|
@@ -3255,14 +3252,14 @@ const PRIMITIVES = [
|
|
|
3255
3252
|
{
|
|
3256
3253
|
name: "List",
|
|
3257
3254
|
description:
|
|
3258
|
-
"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.",
|
|
3259
3256
|
rnComponent: null,
|
|
3260
3257
|
docsUrl: null,
|
|
3261
3258
|
},
|
|
3262
3259
|
{
|
|
3263
3260
|
name: "ListRow",
|
|
3264
3261
|
description:
|
|
3265
|
-
'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.',
|
|
3266
3263
|
rnComponent: null,
|
|
3267
3264
|
docsUrl: null,
|
|
3268
3265
|
},
|
|
@@ -3388,7 +3385,7 @@ const PRIMITIVES = [
|
|
|
3388
3385
|
{
|
|
3389
3386
|
name: "Select",
|
|
3390
3387
|
description:
|
|
3391
|
-
'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.',
|
|
3392
3389
|
rnComponent: null,
|
|
3393
3390
|
docsUrl: null,
|
|
3394
3391
|
},
|
|
@@ -3420,6 +3417,14 @@ const PRIMITIVES = [
|
|
|
3420
3417
|
rnComponent: null,
|
|
3421
3418
|
docsUrl: null,
|
|
3422
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
|
+
},
|
|
3423
3428
|
];
|
|
3424
3429
|
|
|
3425
3430
|
const CATEGORIES = [
|
|
@@ -6072,7 +6077,11 @@ const CONTRACT = deepFreeze({
|
|
|
6072
6077
|
// colour, height, each falling back to the `muted` field. The field
|
|
6073
6078
|
// takes an `iconPosition` prop. Scope labels renamed (sc-11763); keys
|
|
6074
6079
|
// unchanged.
|
|
6075
|
-
|
|
6080
|
+
// 1.192.0: additive (sc-11751, sc-11752, sc-11758) — new `<FileUpload>`
|
|
6081
|
+
// on `<FilePicker>` (which gains onFocus/onBlur; `muted` lists it).
|
|
6082
|
+
// `<List variant="card">`, `<ListRow actions>`, `defaults` on both.
|
|
6083
|
+
// `<Select multiple values onValuesChange>`.
|
|
6084
|
+
version: "1.192.0",
|
|
6076
6085
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6077
6086
|
hooks: HOOKS,
|
|
6078
6087
|
primitives: PRIMITIVES,
|