@colixsystems/widget-sdk 0.246.0 → 0.247.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 +24 -2
- package/dist/chart-logic.js +575 -0
- package/dist/chart-tokens.js +131 -0
- package/dist/chart-view.js +262 -0
- package/dist/chart.js +21 -0
- package/dist/chart.native.js +10 -0
- package/dist/contract.cjs +50 -6
- package/dist/contract.js +50 -6
- package/dist/grid-logic.js +222 -0
- package/dist/grid-view.js +109 -0
- package/dist/grid.js +7 -0
- package/dist/grid.native.js +7 -0
- package/dist/host.d.ts +6 -0
- package/dist/host.js +2 -0
- package/dist/index.d.ts +234 -2
- package/dist/index.js +7 -0
- package/dist/index.native.js +7 -0
- package/dist/people-picker-logic.js +203 -0
- package/dist/people-picker-view.js +247 -0
- package/dist/people-picker.js +11 -0
- package/dist/people-picker.native.js +9 -0
- package/dist/select-search-logic.js +150 -0
- package/dist/select-view.js +244 -16
- package/dist/select.js +6 -2
- package/dist/select.native.js +5 -3
- package/package.json +1 -1
- package/src/contract.js +50 -6
package/dist/select-view.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// `<Overlay>`, at screen level, so no card or scroll container the widget
|
|
7
7
|
// sits in can clip them; a long list scrolls inside the sheet. With
|
|
8
8
|
// `multiple` the same sheet ticks options into a draft that Done commits.
|
|
9
|
+
// Searchable, the sheet leads with a `<SearchField>`: the list filters where
|
|
10
|
+
// it is held, or each pause in typing asks `onSearch` / async `options`.
|
|
9
11
|
|
|
10
12
|
import React from "react";
|
|
11
13
|
import { useHostTheme } from "./hooks.js";
|
|
@@ -41,6 +43,18 @@ import {
|
|
|
41
43
|
triggerSummary,
|
|
42
44
|
valueList,
|
|
43
45
|
} from "./select-logic.js";
|
|
46
|
+
import {
|
|
47
|
+
DEFAULT_SEARCH_DEBOUNCE_MS,
|
|
48
|
+
createDebouncer,
|
|
49
|
+
createRequestGate,
|
|
50
|
+
filterOptions,
|
|
51
|
+
knownOption,
|
|
52
|
+
knownOptions,
|
|
53
|
+
normaliseQuery,
|
|
54
|
+
rememberOptions,
|
|
55
|
+
searchRowOverrides,
|
|
56
|
+
sheetBody,
|
|
57
|
+
} from "./select-search-logic.js";
|
|
44
58
|
|
|
45
59
|
const h = React.createElement;
|
|
46
60
|
// Wide enough that no picked chip shrinks while it is measured.
|
|
@@ -53,10 +67,12 @@ const UNMEASURED = Object.freeze({ available: null, needed: null });
|
|
|
53
67
|
* Bind the shared select to one platform's primitives.
|
|
54
68
|
*
|
|
55
69
|
* @param {{View: any, Text: any, Pressable: any, ScrollView: any, Platform: any,
|
|
56
|
-
* Overlay: any, Button: any, Chip: any}} rn
|
|
70
|
+
* Overlay: any, Button: any, Chip: any, SearchField?: any, Spinner?: any}} rn
|
|
71
|
+
* — `SearchField` and `Spinner` are needed only by a searchable select.
|
|
57
72
|
*/
|
|
58
73
|
export function makeSelect(rn) {
|
|
59
|
-
const { View, Text, Pressable, ScrollView, Platform, Overlay, Button, Chip } =
|
|
74
|
+
const { View, Text, Pressable, ScrollView, Platform, Overlay, Button, Chip, SearchField, Spinner } =
|
|
75
|
+
rn;
|
|
60
76
|
const FieldFrame = makeFieldFrame(rn);
|
|
61
77
|
|
|
62
78
|
// Keyboard focus is a web concern: a device moves its reader's focus into
|
|
@@ -66,14 +82,31 @@ export function makeSelect(rn) {
|
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
function OptionSheet(props) {
|
|
69
|
-
const {
|
|
70
|
-
|
|
85
|
+
const {
|
|
86
|
+
t,
|
|
87
|
+
options,
|
|
88
|
+
isPicked,
|
|
89
|
+
multiple,
|
|
90
|
+
title,
|
|
91
|
+
emptyText,
|
|
92
|
+
listId,
|
|
93
|
+
onPick,
|
|
94
|
+
testID,
|
|
95
|
+
footer,
|
|
96
|
+
search,
|
|
97
|
+
body = options.length === 0 ? "empty" : "list",
|
|
98
|
+
bodyText,
|
|
99
|
+
rowStyle,
|
|
100
|
+
} = props;
|
|
71
101
|
const nodes = React.useRef([]);
|
|
72
102
|
const initial = initialFocusIndex(options, isPicked);
|
|
73
103
|
// Focus lands on the current choice once, when the sheet mounts it; the
|
|
74
|
-
// browser scrolls a focused option into view in a long list.
|
|
75
|
-
|
|
104
|
+
// browser scrolls a focused option into view in a long list. A search
|
|
105
|
+
// field takes that first focus instead.
|
|
106
|
+
const pendingFocus = React.useRef(!search);
|
|
76
107
|
const text = { fontFamily: t.fontFamily, fontSize: t.fontSize, color: t.popover.textColor };
|
|
108
|
+
const note = { ...text, color: t.helperColor, paddingHorizontal: t.paddingHorizontal };
|
|
109
|
+
const focusFirst = () => focusNode(nodes.current[initialFocusIndex(options, () => false)]);
|
|
77
110
|
|
|
78
111
|
return h(
|
|
79
112
|
View,
|
|
@@ -85,11 +118,23 @@ export function makeSelect(rn) {
|
|
|
85
118
|
title,
|
|
86
119
|
)
|
|
87
120
|
: null,
|
|
88
|
-
|
|
121
|
+
search ? search(focusFirst) : null,
|
|
122
|
+
body === "loading"
|
|
123
|
+
? h(
|
|
124
|
+
View,
|
|
125
|
+
{ style: { paddingVertical: t.rowGap }, testID: testID ? `${testID}-loading` : undefined },
|
|
126
|
+
h(Spinner, { size: "sm", label: bodyText }),
|
|
127
|
+
)
|
|
128
|
+
: body !== "list"
|
|
89
129
|
? h(
|
|
90
130
|
Text,
|
|
91
|
-
{
|
|
92
|
-
|
|
131
|
+
{
|
|
132
|
+
style: body === "error" ? { ...note, color: t.errorColor } : note,
|
|
133
|
+
// Results arrive after typing; a status line says what came back.
|
|
134
|
+
...(search ? { role: "status", accessibilityLiveRegion: "polite" } : null),
|
|
135
|
+
testID: search && testID ? `${testID}-${body}` : undefined,
|
|
136
|
+
},
|
|
137
|
+
body === "empty" ? emptyText : bodyText,
|
|
93
138
|
)
|
|
94
139
|
: h(
|
|
95
140
|
ScrollView,
|
|
@@ -133,17 +178,28 @@ export function makeSelect(rn) {
|
|
|
133
178
|
"aria-selected": selected,
|
|
134
179
|
"aria-disabled": !!option.disabled,
|
|
135
180
|
testID: option.testID,
|
|
136
|
-
style: ({ pressed }) =>
|
|
137
|
-
optionRowStyle(t, {
|
|
181
|
+
style: ({ pressed }) => [
|
|
182
|
+
...optionRowStyle(t, {
|
|
138
183
|
pressed,
|
|
139
184
|
disabled: option.disabled,
|
|
140
185
|
justifyContent: multiple ? "flex-start" : "space-between",
|
|
141
186
|
}),
|
|
187
|
+
rowStyle,
|
|
188
|
+
],
|
|
142
189
|
},
|
|
143
190
|
multiple ? checkboxBox(View, t, { checked: selected }) : null,
|
|
191
|
+
option.leading ? h(View, { style: { flexShrink: 0 } }, option.leading) : null,
|
|
144
192
|
h(
|
|
145
193
|
Text,
|
|
146
|
-
{
|
|
194
|
+
{
|
|
195
|
+
style: [
|
|
196
|
+
text,
|
|
197
|
+
{ flexShrink: 1 },
|
|
198
|
+
// Keeps the check mark at the end once a leading node is drawn.
|
|
199
|
+
option.leading ? { flexGrow: 1 } : null,
|
|
200
|
+
selected ? { fontWeight: "600" } : null,
|
|
201
|
+
],
|
|
202
|
+
},
|
|
147
203
|
option.label,
|
|
148
204
|
),
|
|
149
205
|
selected && !multiple ? h(Icon, { name: "check", size: 18, color: t.accent }) : null,
|
|
@@ -230,6 +286,50 @@ export function makeSelect(rn) {
|
|
|
230
286
|
);
|
|
231
287
|
}
|
|
232
288
|
|
|
289
|
+
// The sheet's search field and what it shows instead of the list.
|
|
290
|
+
function searchSheetProps({ theme, search, count, loading, failed, texts, testID }) {
|
|
291
|
+
const body = sheetBody({ loading, error: failed, query: search.text, count });
|
|
292
|
+
return {
|
|
293
|
+
body,
|
|
294
|
+
bodyText:
|
|
295
|
+
body === "loading"
|
|
296
|
+
? texts.loadingText
|
|
297
|
+
: body === "error"
|
|
298
|
+
? texts.loadErrorText
|
|
299
|
+
: texts.noResultsText,
|
|
300
|
+
rowStyle: searchRowOverrides(theme),
|
|
301
|
+
search: (focusFirst) =>
|
|
302
|
+
h(SheetSearchField, {
|
|
303
|
+
value: search.text,
|
|
304
|
+
onChangeText: search.type,
|
|
305
|
+
placeholder: texts.searchPlaceholder,
|
|
306
|
+
onArrowDown: focusFirst,
|
|
307
|
+
testID: testID ? `${testID}-search` : undefined,
|
|
308
|
+
}),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function SheetSearchField({ value, onChangeText, placeholder, onArrowDown, testID }) {
|
|
313
|
+
const node = React.useRef(null);
|
|
314
|
+
React.useEffect(() => {
|
|
315
|
+
focusNode(node.current);
|
|
316
|
+
}, []);
|
|
317
|
+
return h(SearchField, {
|
|
318
|
+
ref: node,
|
|
319
|
+
value,
|
|
320
|
+
onChangeText,
|
|
321
|
+
placeholder,
|
|
322
|
+
// Down from the field reaches the first option, as in a combobox.
|
|
323
|
+
onKeyPress: (event) => {
|
|
324
|
+
const key = event && event.nativeEvent ? event.nativeEvent.key : undefined;
|
|
325
|
+
if (key !== "ArrowDown") return;
|
|
326
|
+
if (typeof event.preventDefault === "function") event.preventDefault();
|
|
327
|
+
onArrowDown();
|
|
328
|
+
},
|
|
329
|
+
testID,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
233
333
|
const Select = React.forwardRef(function Select(props, ref) {
|
|
234
334
|
const {
|
|
235
335
|
value,
|
|
@@ -254,6 +354,16 @@ export function makeSelect(rn) {
|
|
|
254
354
|
testID,
|
|
255
355
|
onFocus,
|
|
256
356
|
onBlur,
|
|
357
|
+
searchable: searchableProp = false,
|
|
358
|
+
onSearch,
|
|
359
|
+
searchDebounceMs = DEFAULT_SEARCH_DEBOUNCE_MS,
|
|
360
|
+
loading: loadingProp = false,
|
|
361
|
+
optionsError,
|
|
362
|
+
selectedOptions,
|
|
363
|
+
searchPlaceholder = "Search",
|
|
364
|
+
noResultsText = "No results",
|
|
365
|
+
loadingText = "Loading…",
|
|
366
|
+
loadErrorText = "Could not load the options",
|
|
257
367
|
...others
|
|
258
368
|
} = props;
|
|
259
369
|
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
@@ -272,9 +382,27 @@ export function makeSelect(rn) {
|
|
|
272
382
|
const setRef = React.useMemo(() => shareRef(ref, triggerRef), [ref]);
|
|
273
383
|
const listId = React.useId();
|
|
274
384
|
const noteId = React.useId();
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
385
|
+
const search = useSearchState({
|
|
386
|
+
open,
|
|
387
|
+
options,
|
|
388
|
+
searchable: searchableProp,
|
|
389
|
+
onSearch,
|
|
390
|
+
debounceMs: searchDebounceMs,
|
|
391
|
+
});
|
|
392
|
+
const { searchable } = search;
|
|
393
|
+
const held = search.fetched
|
|
394
|
+
? search.fetched.options
|
|
395
|
+
: Array.isArray(options)
|
|
396
|
+
? options.filter(Boolean)
|
|
397
|
+
: [];
|
|
398
|
+
const list = search.filtersHeld ? filterOptions(held, search.text) : held;
|
|
399
|
+
// Searchable, a picked option keeps its label after a search drops it.
|
|
400
|
+
const known = React.useRef(new Map());
|
|
401
|
+
if (searchable) known.current = rememberOptions(known.current, selectedOptions, held);
|
|
402
|
+
const optionsFor = (own) =>
|
|
403
|
+
searchable ? knownOptions(known.current, own) : pickedOptions(list, own);
|
|
404
|
+
const current = searchable ? knownOption(known.current, value) : selectedOption(list, value);
|
|
405
|
+
const picked = multiple ? optionsFor(values) : [];
|
|
278
406
|
const draft = ownDraft === null ? valueList(values) : ownDraft;
|
|
279
407
|
const invalid = isShown(error);
|
|
280
408
|
const sheetTitle = isShown(title) ? title : isText(label) ? label : undefined;
|
|
@@ -296,7 +424,7 @@ export function makeSelect(rn) {
|
|
|
296
424
|
const next = committedValues(list, draft);
|
|
297
425
|
close();
|
|
298
426
|
if (!sameValues(next, valueList(values)) && typeof onValuesChange === "function") {
|
|
299
|
-
onValuesChange(next,
|
|
427
|
+
onValuesChange(next, optionsFor(next));
|
|
300
428
|
}
|
|
301
429
|
};
|
|
302
430
|
const pick = (option) => {
|
|
@@ -421,6 +549,23 @@ export function makeSelect(rn) {
|
|
|
421
549
|
testID,
|
|
422
550
|
onPick: pick,
|
|
423
551
|
footer: multiple ? h(DoneFooter, { doneText, onDone: done, testID }) : null,
|
|
552
|
+
...(searchable
|
|
553
|
+
? searchSheetProps({
|
|
554
|
+
theme,
|
|
555
|
+
search,
|
|
556
|
+
count: list.length,
|
|
557
|
+
loading:
|
|
558
|
+
search.pending || (search.fetched ? search.fetched.loading : !!loadingProp),
|
|
559
|
+
failed: search.fetched ? search.fetched.error : isShown(optionsError),
|
|
560
|
+
texts: {
|
|
561
|
+
searchPlaceholder,
|
|
562
|
+
noResultsText,
|
|
563
|
+
loadingText,
|
|
564
|
+
loadErrorText: typeof optionsError === "string" ? optionsError : loadErrorText,
|
|
565
|
+
},
|
|
566
|
+
testID,
|
|
567
|
+
})
|
|
568
|
+
: null),
|
|
424
569
|
}),
|
|
425
570
|
),
|
|
426
571
|
);
|
|
@@ -429,3 +574,86 @@ export function makeSelect(rn) {
|
|
|
429
574
|
Select.displayName = "Select";
|
|
430
575
|
return Select;
|
|
431
576
|
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* A searchable select's query: the text typed into the sheet, sent after a
|
|
580
|
+
* pause to `onSearch` or to async `options`, whose latest answer it keeps.
|
|
581
|
+
* Opening the sheet starts from an empty query; a plain select is inert here.
|
|
582
|
+
*/
|
|
583
|
+
function useSearchState({ open, options, searchable, onSearch, debounceMs }) {
|
|
584
|
+
const asyncOptions = typeof options === "function";
|
|
585
|
+
const remote = typeof onSearch === "function" || asyncOptions;
|
|
586
|
+
const enabled = !!searchable || remote;
|
|
587
|
+
const [text, setText] = React.useState("");
|
|
588
|
+
// The query last handed on; typed text that differs is still pending.
|
|
589
|
+
const [sentQuery, setSentQuery] = React.useState("");
|
|
590
|
+
const [fetched, setFetched] = React.useState({ options: [], loading: false, error: false });
|
|
591
|
+
const latest = React.useRef({ onSearch, options });
|
|
592
|
+
latest.current = { onSearch, options };
|
|
593
|
+
const gate = React.useRef(null);
|
|
594
|
+
if (gate.current === null) gate.current = createRequestGate();
|
|
595
|
+
const mounted = React.useRef(false);
|
|
596
|
+
// Set in the body too: StrictMode runs the cleanup, then the effect again.
|
|
597
|
+
React.useEffect(() => {
|
|
598
|
+
mounted.current = true;
|
|
599
|
+
return () => {
|
|
600
|
+
mounted.current = false;
|
|
601
|
+
};
|
|
602
|
+
}, []);
|
|
603
|
+
|
|
604
|
+
const send = React.useCallback((query) => {
|
|
605
|
+
setSentQuery(query);
|
|
606
|
+
const own = latest.current;
|
|
607
|
+
if (typeof own.onSearch === "function") own.onSearch(query);
|
|
608
|
+
if (typeof own.options !== "function") return;
|
|
609
|
+
const ticket = gate.current.begin();
|
|
610
|
+
setFetched((state) => ({ ...state, loading: true, error: false }));
|
|
611
|
+
Promise.resolve()
|
|
612
|
+
.then(() => own.options(query))
|
|
613
|
+
.then(
|
|
614
|
+
(rows) => {
|
|
615
|
+
if (!mounted.current || !gate.current.isCurrent(ticket)) return;
|
|
616
|
+
const list = Array.isArray(rows) ? rows.filter(Boolean) : [];
|
|
617
|
+
setFetched({ options: list, loading: false, error: false });
|
|
618
|
+
},
|
|
619
|
+
() => {
|
|
620
|
+
if (!mounted.current || !gate.current.isCurrent(ticket)) return;
|
|
621
|
+
setFetched({ options: [], loading: false, error: true });
|
|
622
|
+
},
|
|
623
|
+
);
|
|
624
|
+
}, []);
|
|
625
|
+
|
|
626
|
+
const delay = React.useRef(debounceMs);
|
|
627
|
+
delay.current = debounceMs;
|
|
628
|
+
const debouncer = React.useRef(null);
|
|
629
|
+
if (debouncer.current === null) debouncer.current = createDebouncer(() => delay.current, send);
|
|
630
|
+
React.useEffect(() => () => debouncer.current.cancel(), []);
|
|
631
|
+
|
|
632
|
+
// Each opening starts from an empty query, asked for at once; it is
|
|
633
|
+
// cleared on closing so the next opening never draws the old filter.
|
|
634
|
+
React.useEffect(() => {
|
|
635
|
+
if (!enabled) return;
|
|
636
|
+
if (!open) {
|
|
637
|
+
debouncer.current.cancel();
|
|
638
|
+
setText("");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (remote) send("");
|
|
642
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
643
|
+
}, [open, enabled, remote]);
|
|
644
|
+
|
|
645
|
+
const type = (next) => {
|
|
646
|
+
const own = typeof next === "string" ? next : "";
|
|
647
|
+
setText(own);
|
|
648
|
+
if (remote) debouncer.current.push(own);
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
searchable: enabled,
|
|
653
|
+
filtersHeld: enabled && !remote,
|
|
654
|
+
text: enabled ? text : "",
|
|
655
|
+
pending: remote && normaliseQuery(text) !== sentQuery,
|
|
656
|
+
type,
|
|
657
|
+
fetched: asyncOptions ? fetched : null,
|
|
658
|
+
};
|
|
659
|
+
}
|
package/dist/select.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// `<Select>` (web). Binds ./select-view.js to react-native-web and the web
|
|
2
|
-
// `<Overlay>`, `<Button>` and `<
|
|
3
|
-
// bare specifier.
|
|
2
|
+
// `<Overlay>`, `<Button>`, `<Chip>`, `<SearchField>` and `<Spinner>`; see
|
|
3
|
+
// ./primitives.js for why not the bare specifier.
|
|
4
4
|
|
|
5
5
|
import * as ReactNative from "react-native-web";
|
|
6
6
|
import { makeSelect } from "./select-view.js";
|
|
7
7
|
import { Overlay } from "./overlay.js";
|
|
8
8
|
import { Button } from "./button.js";
|
|
9
9
|
import { Chip } from "./chip.js";
|
|
10
|
+
import { SearchField } from "./search-field.js";
|
|
11
|
+
import { Spinner } from "./loading.js";
|
|
10
12
|
|
|
11
13
|
export const Select = makeSelect({
|
|
12
14
|
View: ReactNative.View,
|
|
@@ -17,4 +19,6 @@ export const Select = makeSelect({
|
|
|
17
19
|
Overlay,
|
|
18
20
|
Button,
|
|
19
21
|
Chip,
|
|
22
|
+
SearchField,
|
|
23
|
+
Spinner,
|
|
20
24
|
});
|
package/dist/select.native.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// `<Select>` (native). Binds ./select-view.js to React Native's own
|
|
2
|
-
// primitives and the native `<Overlay>`, `<Button
|
|
3
|
-
// imports differ from ./select.js.
|
|
2
|
+
// primitives and the native `<Overlay>`, `<Button>`, `<Chip>`,
|
|
3
|
+
// `<SearchField>` and `<Spinner>`; only the 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
8
|
import { Button } from "./button.native.js";
|
|
9
9
|
import { Chip } from "./chip.native.js";
|
|
10
|
+
import { SearchField } from "./search-field.native.js";
|
|
11
|
+
import { Spinner } from "./loading.native.js";
|
|
10
12
|
|
|
11
|
-
export const Select = makeSelect({ View, Text, Pressable, ScrollView, Platform, Overlay, Button, Chip });
|
|
13
|
+
export const Select = makeSelect({ View, Text, Pressable, ScrollView, Platform, Overlay, Button, Chip, SearchField, Spinner });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.247.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", "FileUpload", "RadioGroup"]),
|
|
625
|
+
sdkComponents: Object.freeze(["TextField", "Checkbox", "Switch", "Select", "SearchField", "FileUpload", "RadioGroup", "PeoplePicker"]),
|
|
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" }),
|
|
@@ -641,7 +641,7 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
641
641
|
// radius and shadow keep the `<Overlay>` panel's own.
|
|
642
642
|
popover: Object.freeze({
|
|
643
643
|
label: "Dropdown menus",
|
|
644
|
-
sdkComponents: Object.freeze(["Select", "Menu", "Popover", "Tooltip"]),
|
|
644
|
+
sdkComponents: Object.freeze(["Select", "Menu", "Popover", "Tooltip", "PeoplePicker"]),
|
|
645
645
|
tokens: Object.freeze({
|
|
646
646
|
background: Object.freeze({ type: "color", uiDefault: "colors.surface" }),
|
|
647
647
|
textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurface" }),
|
|
@@ -784,6 +784,25 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
784
784
|
}),
|
|
785
785
|
targets: Object.freeze({}),
|
|
786
786
|
}),
|
|
787
|
+
// Charts: `<Chart>` reads this scope directly, so it names the SDK
|
|
788
|
+
// component it styles and binds no widget field. Unset series colours
|
|
789
|
+
// follow the theme's own colours, so their defaults are derived.
|
|
790
|
+
chart: Object.freeze({
|
|
791
|
+
label: "Charts",
|
|
792
|
+
sdkComponents: Object.freeze(["Chart"]),
|
|
793
|
+
tokens: Object.freeze({
|
|
794
|
+
series1: Object.freeze({ type: "color" }),
|
|
795
|
+
series2: Object.freeze({ type: "color" }),
|
|
796
|
+
series3: Object.freeze({ type: "color" }),
|
|
797
|
+
series4: Object.freeze({ type: "color" }),
|
|
798
|
+
series5: Object.freeze({ type: "color" }),
|
|
799
|
+
series6: Object.freeze({ type: "color" }),
|
|
800
|
+
axisColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
|
|
801
|
+
gridColor: Object.freeze({ type: "color", uiDefault: "colors.border" }),
|
|
802
|
+
barRadius: Object.freeze({ type: "size", min: 0, max: 24, uiDefault: "radii.sm" }),
|
|
803
|
+
}),
|
|
804
|
+
targets: Object.freeze({}),
|
|
805
|
+
}),
|
|
787
806
|
});
|
|
788
807
|
|
|
789
808
|
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
@@ -2536,7 +2555,8 @@ const HOOKS = [
|
|
|
2536
2555
|
"Returns { groups, loading, error, refetch, create, remove, addMember, " +
|
|
2537
2556
|
"removeMember }. list returns the { data, meta } envelope verbatim — the " +
|
|
2538
2557
|
"hook unwraps res.data; rows are snake_case. Reads need groups.read:*; " +
|
|
2539
|
-
"mutations need groups.write:*."
|
|
2558
|
+
"mutations need groups.write:*. query { q?, ids?, limit?, offset? }: " +
|
|
2559
|
+
"`ids` (1 to 100) names the groups behind ids you already hold.",
|
|
2540
2560
|
returnShape: {
|
|
2541
2561
|
groups: "Array<{ id, name, member_count }> // snake_case rows; unwrapped from { data, meta }",
|
|
2542
2562
|
loading: "boolean",
|
|
@@ -3377,6 +3397,13 @@ const PRIMITIVES = [
|
|
|
3377
3397
|
rnComponent: null,
|
|
3378
3398
|
docsUrl: null,
|
|
3379
3399
|
},
|
|
3400
|
+
{
|
|
3401
|
+
name: "Chart",
|
|
3402
|
+
description:
|
|
3403
|
+
'Themed chart. `<Chart type="bar" data={groups.map((g) => ({ label: String(g.group), value: Number(g.sum) || 0 }))} loading={loading} valueFormat="compact" />`. Use it by default to plot a table\'s figures: feed it ONE `useDatastoreAggregate` call (`groups` for a breakdown by a column, `buckets` for a series over time) mapped to `[{ label, value }]`. `type` is "bar" (default), "hbar" (horizontal bars, for long category names), "line", "area", "donut" (the parts of a whole) or "sparkline" (a small line inside a tile, with no axes). It measures its own width and redraws when its container resizes, shows a `<Skeleton>` while `loading` and an `<EmptyState>` when there is nothing to plot, draws a legend for a donut and for several series, and gives a screen reader a one-line summary followed by the values. Its series colours, axis and label colour, gridline colour and bar corners come from the app\'s chart look (Design > Components > Charts), falling back to the theme\'s colours, so pass no colours of your own. Props: `data` (`[{ label, value }]`, or one number per series key), `series` (`[{ key, label, color? }]` for several series sharing one `data`: bars stand side by side, lines and areas overlay; a donut and a sparkline draw the first), `type`, `valueFormat` ("number" default | "compact", 1.2k | "percent", which appends % to a value already in percent, 42 reads 42% | a function of the number; used for the axis, the legend and the spoken values, grouped for the app\'s language), `height` (px, default 220; 48 for a sparkline), `loading`, `emptyTitle` (default the shared "No results" text) and `emptyMessage`, `accessibilityLabel` (replaces the spoken summary sentence; pass your translated title), `moreText` (`(count) => string`, the tail of a long spoken value list, default "and N more"; pass it translated), `style`, `testID`. When the request needs a chart it cannot draw (a scatter plot, a gantt or timeline, brushing or tapping a bar to filter), build your own with react-native-svg in the app\'s look (see DRAWING A CHART), painting it with `chartColors(useTheme())`: `series(i)` and `seriesOpacity(i)` for the i-th series, `axis` and `grid`, the colours `<Chart>` uses.',
|
|
3404
|
+
rnComponent: null,
|
|
3405
|
+
docsUrl: null,
|
|
3406
|
+
},
|
|
3380
3407
|
{
|
|
3381
3408
|
name: "Divider",
|
|
3382
3409
|
description:
|
|
@@ -3384,6 +3411,13 @@ const PRIMITIVES = [
|
|
|
3384
3411
|
rnComponent: null,
|
|
3385
3412
|
docsUrl: null,
|
|
3386
3413
|
},
|
|
3414
|
+
{
|
|
3415
|
+
name: "Grid",
|
|
3416
|
+
description:
|
|
3417
|
+
'Balanced lines of cells sized from the grid\'s own measured width. `<Grid minCellWidth={240}>{items.map((item) => <Card key={item.id} style={{ flex: 1 }}>…</Card>)}</Grid>` — use it by default for a card grid, a tile or stat row, and a grid of definite-size cells (calendar days, seats, swatches); when the layout needs what it cannot express (masonry, a bento layout whose cells span lines), lay it out yourself with `useContainerWidth()`. It measures an inner box with no padding, so padding in its `style` is never counted into the width. `balance` "cards" (default): as many cells per line as fit at `minCellWidth` (240 when unset), every line but the last full and the last padded with empty cells so its cards keep the width above (6 that fit 5 are 3 + 3). "tiles": the remainder spreads over every line and each line stretches (5 at 4 per line are 3 + 2, never 4 + 1); the per-line ceiling is `maxColumns`, or what fits at `minCellWidth`, or else 4 / 3 / 2 for the wide / medium / narrow width tier. `columns` fixes the count per line. `cell={{ min, max }}` makes definite-size cells as large as fit up to `max` (never below `min`, 32 when unset) in a block centred in the grid (a block whose `min` cells cannot fit starts at the leading edge, so only its far end is cut off), `square` makes them as tall as wide: `<Grid columns={7} cell={{ min: 32, max: 64 }} square gap="xs">`. Props: `children` (one cell each; each keeps its own look and theme scope, and a child with `flex: 1` fills its cell\'s height so a line of cards ends level), `balance`, `minCellWidth`, `maxColumns`, `columns`, `cell`, `square`, `gap` (a theme spacing name, default "md", or a number), `style` (the outer box), `testID`, `accessibilityLabel`. A layout that depends on the width draws nothing until it is measured. In a row, give it `style={{ flex: 1, minWidth: 0 }}` so it has a width to measure.',
|
|
3418
|
+
rnComponent: null,
|
|
3419
|
+
docsUrl: null,
|
|
3420
|
+
},
|
|
3387
3421
|
{
|
|
3388
3422
|
name: "Banner",
|
|
3389
3423
|
description:
|
|
@@ -3464,7 +3498,7 @@ const PRIMITIVES = [
|
|
|
3464
3498
|
{
|
|
3465
3499
|
name: "Select",
|
|
3466
3500
|
description:
|
|
3467
|
-
'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 default 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. Use it rather than a dropdown of your own; when the request needs options it cannot show (colour swatches, photos with two-line captions), put a small swatch or photo picker in a `<Popover>` at its button, or a long or full-screen option list in a `<Modal>` (an emoji choice is `<EmojiPicker>`) — never an absolutely positioned list, which the widget\'s container clips on both hosts. MULTIPLE: `<Select multiple label="Tags" values={tags} onValuesChange={setTags} options={tagOptions} />` — the default 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.',
|
|
3501
|
+
'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 default 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. Use it rather than a dropdown of your own; when the request needs options it cannot show (colour swatches, photos with two-line captions), put a small swatch or photo picker in a `<Popover>` at its button, or a long or full-screen option list in a `<Modal>` (an emoji choice is `<EmojiPicker>`) — never an absolutely positioned list, which the widget\'s container clips on both hosts. MULTIPLE: `<Select multiple label="Tags" values={tags} onValuesChange={setTags} options={tagOptions} />` — the default 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. SEARCHABLE: a long list the user should type to narrow. `searchable` filters the given `options` by label as they type; `onSearch(query)` hands you the trimmed query when the sheet opens (\"\") and once typing pauses (`searchDebounceMs`, default 250), and you answer with new `options` and `loading` meanwhile (`optionsError` when it failed); or pass `options` as an async function of the query and Select fetches, keeping only the latest answer. The sheet then leads with the app\'s search field (Design > Components > Search fields), shows a spinner while loading, `noResultsText` (default \"No results\") when a query finds nothing and `loadErrorText` on failure, and its rows take the Lists row height and padding when the theme sets them. It remembers the label of every option it has shown; pass `selectedOptions` for a saved value the first page may not hold, or the trigger shows the placeholder. An option may carry `leading` (an `<Avatar>`, an icon) drawn before its label. Picking people, groups or a table\'s records is `<PeoplePicker>`, which does all of this for you.',
|
|
3468
3502
|
rnComponent: null,
|
|
3469
3503
|
docsUrl: null,
|
|
3470
3504
|
},
|
|
@@ -3475,6 +3509,13 @@ const PRIMITIVES = [
|
|
|
3475
3509
|
rnComponent: null,
|
|
3476
3510
|
docsUrl: null,
|
|
3477
3511
|
},
|
|
3512
|
+
{
|
|
3513
|
+
name: "PeoplePicker",
|
|
3514
|
+
description:
|
|
3515
|
+
'Searchable picker of people, groups or records. `<PeoplePicker label="Assignee" value={assignee} onValueChange={setAssignee} />` — use it by default whenever the user picks a workspace user (an assignee, a reviewer, a recipient), a group (`source="groups"`), or a related record of a table they should search (`source="records" table={tableId}`, named by the table\'s identification column or `labelColumn`). It is a searchable `<Select>`: the same field, sheet, `label` / `helper` / `error` / `disabled`, and `multiple` with `values` / `onValuesChange`. Nothing loads until the sheet opens, each pause in typing searches on the server (never one page filtered in the widget), each user or group row leads with an `<Avatar>` (`avatarColumn` names a record\'s photo column; `showAvatars={false}` hides them), `excludeIds` leaves ids out (the current members), `groupId` limits users to one group, `filter` narrows records, and `pageSize` (default 50) sets the page. A saved value outside the first page is looked up once by id, so the trigger shows its name — `loadingText` meanwhile, `unknownText` (default "Unknown") when it is gone — never the raw id. Values are the row ids, exactly what a USER, USER_GROUP or RELATION column stores. Scopes: `directory.read:users` for users, `groups.read:*` for groups, the table\'s `datastore.read` scope for records. When the request needs what it cannot do — a people list that stays on screen with an action per row, a picker of users with presence dots or a role column — build your own with `useDirectory` / `useGroups` and the app\'s look.',
|
|
3516
|
+
rnComponent: null,
|
|
3517
|
+
docsUrl: null,
|
|
3518
|
+
},
|
|
3478
3519
|
{
|
|
3479
3520
|
name: "RadioGroup",
|
|
3480
3521
|
description:
|
|
@@ -4540,7 +4581,7 @@ const VETTED_IMPORTS = [
|
|
|
4540
4581
|
platforms: ["web", "native"],
|
|
4541
4582
|
category: "drawing",
|
|
4542
4583
|
description:
|
|
4543
|
-
"SVG path generators — `line`, `area`, `arc`, `pie`, `curve*`. Each returns a path string you hand to the vetted react-native-svg's `<Path d={…} />`, so a
|
|
4584
|
+
"SVG path generators — `line`, `area`, `arc`, `pie`, `curve*`. Each returns a path string you hand to the vetted react-native-svg's `<Path d={…} />`, so a chart `<Chart>` does not draw renders identically in the Player and the Expo export from ONE source file. Pure JS.",
|
|
4544
4585
|
},
|
|
4545
4586
|
{
|
|
4546
4587
|
specifier: "d3-array",
|
|
@@ -6210,7 +6251,10 @@ const CONTRACT = deepFreeze({
|
|
|
6210
6251
|
// `<Menu>`, `<Popover>`, `<Tooltip>` (popover scope gains radius,
|
|
6211
6252
|
// shadow), `<IconButton>`, Button ghost/link/size/busy/selected and
|
|
6212
6253
|
// trigger pass-through, `<FileImage>`, `Avatar fileId`, `<RadioGroup>`.
|
|
6213
|
-
|
|
6254
|
+
// 1.201.0: additive (sc-12070, sc-12071, sc-12072) — new `<Grid>`,
|
|
6255
|
+
// `<Chart>` + `chart` theme scope + `chartColors`, `<PeoplePicker>`,
|
|
6256
|
+
// searchable `<Select>`; `useGroups` takes `ids`.
|
|
6257
|
+
version: "1.201.0",
|
|
6214
6258
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6215
6259
|
hooks: HOOKS,
|
|
6216
6260
|
primitives: PRIMITIVES,
|