@colixsystems/widget-sdk 0.237.0 → 0.239.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 +39 -4
- package/dist/checkbox-view.js +28 -16
- package/dist/contract.cjs +26 -12
- package/dist/contract.js +26 -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 +162 -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/property-schema.js +6 -0
- 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 +26 -12
package/dist/list-view.js
CHANGED
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
resolveListTokens,
|
|
15
15
|
listRowFrame,
|
|
16
16
|
listSideEdgeWidth,
|
|
17
|
+
listCardRowFrame,
|
|
18
|
+
listCardGap,
|
|
19
|
+
listVariant,
|
|
17
20
|
LIST_TOKEN_NAMES,
|
|
18
21
|
LIST_SELECTED_BAR_WIDTH,
|
|
19
22
|
} from "./list-tokens.js";
|
|
@@ -33,6 +36,10 @@ function listOverrides(props) {
|
|
|
33
36
|
return out;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
function hasNode(node) {
|
|
40
|
+
return node !== undefined && node !== null && node !== false;
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
function soleElement(children) {
|
|
37
44
|
const parts = React.Children.toArray(children);
|
|
38
45
|
return parts.length === 1 && React.isValidElement(parts[0]) ? parts[0] : null;
|
|
@@ -50,16 +57,31 @@ const HORIZONTAL_PADDING = [
|
|
|
50
57
|
/**
|
|
51
58
|
* Bind the shared list components to one platform's primitives.
|
|
52
59
|
*
|
|
53
|
-
* @param {{View: any, Text: any, Pressable: any}} rn
|
|
60
|
+
* @param {{View: any, Text: any, Pressable: any, Card: any}} rn — `Card` is
|
|
61
|
+
* the platform's bound `<Card>`, the surface of a card-style list's rows.
|
|
54
62
|
*/
|
|
55
63
|
export function makeList(rn) {
|
|
56
|
-
const { View, Text, Pressable } = rn;
|
|
64
|
+
const { View, Text, Pressable, Card } = rn;
|
|
57
65
|
const ListContext = React.createContext(null);
|
|
58
66
|
|
|
67
|
+
// What a `<List>` hands its rows: overrides, the widget's defaults, its variant.
|
|
68
|
+
// Tokens carry into a nested list; the variant is each list's own.
|
|
69
|
+
function listContextValue(props, inherited) {
|
|
70
|
+
const { variant, defaults } = props;
|
|
71
|
+
return {
|
|
72
|
+
overrides: { ...(inherited && inherited.overrides), ...listOverrides(props) },
|
|
73
|
+
defaults: { ...(inherited && inherited.defaults), ...defaults },
|
|
74
|
+
variant: listVariant(variant),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
function useRowTokens(props) {
|
|
60
79
|
const theme = useListTheme();
|
|
61
80
|
const inherited = React.useContext(ListContext);
|
|
62
|
-
|
|
81
|
+
const list = listContextValue(props, inherited);
|
|
82
|
+
// Only a `<List>` chooses the variant; a row follows it.
|
|
83
|
+
const variant = inherited ? inherited.variant : "plain";
|
|
84
|
+
return [theme, resolveListTokens(theme, list.overrides, list.defaults), { ...list, variant }];
|
|
63
85
|
}
|
|
64
86
|
|
|
65
87
|
// A lone row View spans the row, as the title block does. Anything
|
|
@@ -71,52 +93,67 @@ export function makeList(rn) {
|
|
|
71
93
|
: children;
|
|
72
94
|
}
|
|
73
95
|
|
|
96
|
+
function titleBlock(title, subtitle, typography, colors) {
|
|
97
|
+
return React.createElement(
|
|
98
|
+
View,
|
|
99
|
+
{ style: { flex: 1, gap: 2 } },
|
|
100
|
+
title !== undefined && title !== null
|
|
101
|
+
? React.createElement(
|
|
102
|
+
Text,
|
|
103
|
+
{ style: textRoleStyle("subtitle", typography, colors), numberOfLines: 1 },
|
|
104
|
+
title,
|
|
105
|
+
)
|
|
106
|
+
: null,
|
|
107
|
+
subtitle !== undefined && subtitle !== null
|
|
108
|
+
? React.createElement(
|
|
109
|
+
Text,
|
|
110
|
+
{ style: textRoleStyle("caption", typography, colors), numberOfLines: 2 },
|
|
111
|
+
subtitle,
|
|
112
|
+
)
|
|
113
|
+
: null,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function rowBody(props, typography, colors) {
|
|
118
|
+
return props.children !== undefined
|
|
119
|
+
? customBody(props.children)
|
|
120
|
+
: titleBlock(props.title, props.subtitle, typography, colors);
|
|
121
|
+
}
|
|
122
|
+
|
|
74
123
|
function setsOwnInset(style) {
|
|
75
124
|
const flat = flattenStyle(style);
|
|
76
125
|
return HORIZONTAL_PADDING.some((key) => flat[key] !== undefined);
|
|
77
126
|
}
|
|
78
127
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const inherited = React.useContext(ListContext);
|
|
83
|
-
const overrides = { ...inherited, ...listOverrides(props) };
|
|
84
|
-
const tokens = resolveListTokens(theme, overrides);
|
|
85
|
-
// No divider under the last row: the list's own edge ends it.
|
|
86
|
-
const rows = React.Children.toArray(children);
|
|
87
|
-
const last = rows.length - 1;
|
|
88
|
-
const content = rows.map((row, i) =>
|
|
89
|
-
i === last && React.isValidElement(row) && row.props.divider === undefined
|
|
90
|
-
? React.cloneElement(row, { divider: false })
|
|
91
|
-
: row,
|
|
92
|
-
);
|
|
93
|
-
return React.createElement(
|
|
94
|
-
ListContext.Provider,
|
|
95
|
-
{ value: overrides },
|
|
96
|
-
React.createElement(View, { style: [{ gap: tokens.gap }, style], testID }, content),
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
List.displayName = "List";
|
|
100
|
-
|
|
101
|
-
function ListRow(props) {
|
|
102
|
-
const {
|
|
103
|
-
title,
|
|
104
|
-
subtitle,
|
|
105
|
-
leading,
|
|
106
|
-
trailing,
|
|
128
|
+
// accessibilityRole + aria-*: the pair that announces on both hosts.
|
|
129
|
+
function pressProps({ onPress, disabled = false, selected = false, accessibilityLabel }) {
|
|
130
|
+
return {
|
|
107
131
|
onPress,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
divider = true,
|
|
132
|
+
disabled,
|
|
133
|
+
accessibilityRole: "button",
|
|
111
134
|
accessibilityLabel,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const [theme, tokens] = useRowTokens(props);
|
|
117
|
-
const { colors, spacing, typography } = theme;
|
|
135
|
+
"aria-selected": selected,
|
|
136
|
+
"aria-disabled": disabled,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
118
139
|
|
|
119
|
-
|
|
140
|
+
// Beside `actions`, the tap target is the row minus its buttons,
|
|
141
|
+
// laid out as the row is so nothing moves.
|
|
142
|
+
function pressRegionStyle(style, spacing) {
|
|
143
|
+
const flat = flattenStyle(style);
|
|
144
|
+
return {
|
|
145
|
+
flex: 1,
|
|
146
|
+
minWidth: 0,
|
|
147
|
+
alignSelf: "stretch",
|
|
148
|
+
flexDirection: "row",
|
|
149
|
+
alignItems: flat.alignItems ?? "center",
|
|
150
|
+
gap: flat.gap ?? spacing.md,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function plainRowStyle(props, tokens, spacing) {
|
|
155
|
+
const { selected = false, divider = true, style } = props;
|
|
156
|
+
const pressable = typeof props.onPress === "function";
|
|
120
157
|
// A tile is outlined; a flush band is separated by its bottom divider.
|
|
121
158
|
const edge = tokens.separated
|
|
122
159
|
? tokens.dividerWidth > 0
|
|
@@ -125,7 +162,7 @@ export function makeList(rn) {
|
|
|
125
162
|
: divider && tokens.dividerWidth > 0
|
|
126
163
|
? { borderBottomWidth: tokens.dividerWidth, borderColor: tokens.dividerColor }
|
|
127
164
|
: null;
|
|
128
|
-
|
|
165
|
+
return {
|
|
129
166
|
// An inset the widget sets itself is left exactly as it asked.
|
|
130
167
|
...listRowFrame(tokens, spacing, selected && !setsOwnInset(style)),
|
|
131
168
|
paddingVertical: tokens.rowPadding,
|
|
@@ -143,44 +180,44 @@ export function makeList(rn) {
|
|
|
143
180
|
}
|
|
144
181
|
: null),
|
|
145
182
|
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The row's slots, with its own buttons kept outside the tap target.
|
|
186
|
+
function rowSlots(props, row) {
|
|
187
|
+
const { leading, trailing, actions, disabled = false, style } = props;
|
|
188
|
+
const body = rowBody(props, row.theme.typography, row.colors);
|
|
189
|
+
if (!row.splitPress) return [leading || null, body, trailing || null, actions || null];
|
|
190
|
+
return [
|
|
191
|
+
React.createElement(
|
|
192
|
+
Pressable,
|
|
193
|
+
{
|
|
194
|
+
...pressProps(props),
|
|
195
|
+
onPressIn: () => row.setPressed(true),
|
|
196
|
+
onPressOut: () => row.setPressed(false),
|
|
197
|
+
style: [pressRegionStyle(style, row.theme.spacing), disabled ? { opacity: 0.5 } : null],
|
|
198
|
+
},
|
|
199
|
+
leading || null,
|
|
200
|
+
body,
|
|
201
|
+
trailing || null,
|
|
202
|
+
),
|
|
203
|
+
actions,
|
|
204
|
+
];
|
|
205
|
+
}
|
|
146
206
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
{ style: { flex: 1, gap: 2 } },
|
|
153
|
-
title !== undefined && title !== null
|
|
154
|
-
? React.createElement(
|
|
155
|
-
Text,
|
|
156
|
-
{
|
|
157
|
-
style: textRoleStyle("subtitle", typography, colors),
|
|
158
|
-
numberOfLines: 1,
|
|
159
|
-
},
|
|
160
|
-
title,
|
|
161
|
-
)
|
|
162
|
-
: null,
|
|
163
|
-
subtitle !== undefined && subtitle !== null
|
|
164
|
-
? React.createElement(
|
|
165
|
-
Text,
|
|
166
|
-
{
|
|
167
|
-
style: textRoleStyle("caption", typography, colors),
|
|
168
|
-
numberOfLines: 2,
|
|
169
|
-
},
|
|
170
|
-
subtitle,
|
|
171
|
-
)
|
|
172
|
-
: null,
|
|
173
|
-
);
|
|
174
|
-
const content = [leading || null, body, trailing || null];
|
|
207
|
+
function PlainRow({ props, row }) {
|
|
208
|
+
const { disabled = false, accessibilityLabel, style, testID } = props;
|
|
209
|
+
const rowStyle = plainRowStyle(props, row.tokens, row.theme.spacing);
|
|
210
|
+
const pressedFill = { backgroundColor: row.tokens.pressedBackground };
|
|
211
|
+
const content = rowSlots(props, { ...row, colors: row.theme.colors });
|
|
175
212
|
|
|
176
|
-
if (!pressable) {
|
|
213
|
+
if (!row.pressable || row.splitPress) {
|
|
177
214
|
return React.createElement(
|
|
178
215
|
View,
|
|
179
216
|
{
|
|
180
|
-
style: [rowStyle, style],
|
|
217
|
+
style: [rowStyle, row.pressed ? pressedFill : null, style],
|
|
181
218
|
testID,
|
|
182
|
-
accessibilityLabel,
|
|
183
|
-
accessible: accessibilityLabel ? true : undefined,
|
|
219
|
+
accessibilityLabel: row.splitPress ? undefined : accessibilityLabel,
|
|
220
|
+
accessible: accessibilityLabel && !row.splitPress ? true : undefined,
|
|
184
221
|
},
|
|
185
222
|
...content,
|
|
186
223
|
);
|
|
@@ -188,17 +225,11 @@ export function makeList(rn) {
|
|
|
188
225
|
return React.createElement(
|
|
189
226
|
Pressable,
|
|
190
227
|
{
|
|
191
|
-
|
|
192
|
-
disabled,
|
|
193
|
-
// accessibilityRole + aria-*: the pair that announces on both hosts.
|
|
194
|
-
accessibilityRole: "button",
|
|
195
|
-
accessibilityLabel,
|
|
196
|
-
"aria-selected": selected,
|
|
197
|
-
"aria-disabled": disabled,
|
|
228
|
+
...pressProps(props),
|
|
198
229
|
testID,
|
|
199
230
|
style: ({ pressed }) => [
|
|
200
231
|
rowStyle,
|
|
201
|
-
pressed ?
|
|
232
|
+
pressed ? pressedFill : null,
|
|
202
233
|
disabled ? { opacity: 0.5 } : null,
|
|
203
234
|
style,
|
|
204
235
|
],
|
|
@@ -206,6 +237,92 @@ export function makeList(rn) {
|
|
|
206
237
|
...content,
|
|
207
238
|
);
|
|
208
239
|
}
|
|
240
|
+
|
|
241
|
+
// Each row is the app's `<Card>`; the card scope paints it, the list
|
|
242
|
+
// scope arranges what is inside.
|
|
243
|
+
function CardRow({ props, row }) {
|
|
244
|
+
const { selected = false, disabled = false, accessibilityLabel, style, testID } = props;
|
|
245
|
+
const { tokens, theme } = row;
|
|
246
|
+
const frame = listCardRowFrame(tokens, theme.spacing, row.pressable);
|
|
247
|
+
// The card's own border carries the accent, so nothing shifts.
|
|
248
|
+
const mark = selected
|
|
249
|
+
? { backgroundColor: tokens.selectedBackground, borderColor: tokens.selectedAccent }
|
|
250
|
+
: null;
|
|
251
|
+
const pressedFill = { backgroundColor: tokens.pressedBackground };
|
|
252
|
+
// A function child reads the inks of the card's own fill.
|
|
253
|
+
const content = ({ colors }) =>
|
|
254
|
+
React.createElement(React.Fragment, null, ...rowSlots(props, { ...row, colors }));
|
|
255
|
+
|
|
256
|
+
if (!row.pressable || row.splitPress) {
|
|
257
|
+
return React.createElement(
|
|
258
|
+
Card,
|
|
259
|
+
{
|
|
260
|
+
testID,
|
|
261
|
+
accessibilityLabel: row.splitPress ? undefined : accessibilityLabel,
|
|
262
|
+
accessible: accessibilityLabel && !row.splitPress ? true : undefined,
|
|
263
|
+
containerStyle: [frame, mark, row.pressed ? pressedFill : null, style],
|
|
264
|
+
},
|
|
265
|
+
content,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return React.createElement(
|
|
269
|
+
Card,
|
|
270
|
+
{
|
|
271
|
+
as: Pressable,
|
|
272
|
+
fillInGradient: true,
|
|
273
|
+
...pressProps(props),
|
|
274
|
+
testID,
|
|
275
|
+
containerStyle: ({ pressed }) => [
|
|
276
|
+
frame,
|
|
277
|
+
mark,
|
|
278
|
+
pressed ? pressedFill : null,
|
|
279
|
+
disabled ? { opacity: 0.5 } : null,
|
|
280
|
+
style,
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
content,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function List(props) {
|
|
288
|
+
const { children, style, testID } = props;
|
|
289
|
+
const theme = useListTheme();
|
|
290
|
+
const value = listContextValue(props, React.useContext(ListContext));
|
|
291
|
+
const tokens = resolveListTokens(theme, value.overrides, value.defaults);
|
|
292
|
+
const gap = value.variant === "card" ? listCardGap(tokens, theme.spacing) : tokens.gap;
|
|
293
|
+
// No divider under the last row: the list's own edge ends it.
|
|
294
|
+
const rows = React.Children.toArray(children);
|
|
295
|
+
const last = rows.length - 1;
|
|
296
|
+
const content = rows.map((row, i) =>
|
|
297
|
+
i === last && React.isValidElement(row) && row.props.divider === undefined
|
|
298
|
+
? React.cloneElement(row, { divider: false })
|
|
299
|
+
: row,
|
|
300
|
+
);
|
|
301
|
+
return React.createElement(
|
|
302
|
+
ListContext.Provider,
|
|
303
|
+
{ value },
|
|
304
|
+
React.createElement(View, { style: [{ gap }, style], testID }, content),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
List.displayName = "List";
|
|
308
|
+
|
|
309
|
+
function ListRow(props) {
|
|
310
|
+
const [theme, tokens, list] = useRowTokens(props);
|
|
311
|
+
const [pressed, setPressed] = React.useState(false);
|
|
312
|
+
const pressable = typeof props.onPress === "function";
|
|
313
|
+
const row = {
|
|
314
|
+
theme,
|
|
315
|
+
tokens,
|
|
316
|
+
pressable,
|
|
317
|
+
// Buttons inside a button are unreachable; `actions` sit beside it.
|
|
318
|
+
splitPress: pressable && hasNode(props.actions),
|
|
319
|
+
pressed,
|
|
320
|
+
setPressed,
|
|
321
|
+
};
|
|
322
|
+
return list.variant === "card"
|
|
323
|
+
? React.createElement(CardRow, { props, row })
|
|
324
|
+
: React.createElement(PlainRow, { props, row });
|
|
325
|
+
}
|
|
209
326
|
ListRow.displayName = "ListRow";
|
|
210
327
|
|
|
211
328
|
// A table's column labels: a ListRow's frame and body slot with none of its
|
package/dist/list.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
// ./list-view.js to react-native-web; see ./primitives.js for why not the bare specifier.
|
|
3
3
|
|
|
4
4
|
import * as ReactNative from "react-native-web";
|
|
5
|
+
import { Card } from "./card.js";
|
|
5
6
|
import { makeList } from "./list-view.js";
|
|
6
7
|
|
|
7
|
-
export const { List, ListRow, ListHeader, EmptyState } = makeList(
|
|
8
|
+
export const { List, ListRow, ListHeader, EmptyState } = makeList({
|
|
9
|
+
View: ReactNative.View,
|
|
10
|
+
Text: ReactNative.Text,
|
|
11
|
+
Pressable: ReactNative.Pressable,
|
|
12
|
+
Card,
|
|
13
|
+
});
|
package/dist/list.native.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// `<List>`, `<ListRow>`, `<ListHeader>`, `<EmptyState>` (native). Binds
|
|
2
|
-
// ./list-view.js to React Native's own primitives; only the
|
|
2
|
+
// ./list-view.js to React Native's own primitives; only the imports differ from ./list.js.
|
|
3
3
|
|
|
4
4
|
import { View, Text, Pressable } from "react-native";
|
|
5
|
+
import { Card } from "./card.native.js";
|
|
5
6
|
import { makeList } from "./list-view.js";
|
|
6
7
|
|
|
7
|
-
export const { List, ListRow, ListHeader, EmptyState } = makeList({ View, Text, Pressable });
|
|
8
|
+
export const { List, ListRow, ListHeader, EmptyState } = makeList({ View, Text, Pressable, Card });
|
package/dist/primitives.js
CHANGED
|
@@ -73,9 +73,7 @@ export { Icon } from "./icon.js";
|
|
|
73
73
|
export { DateTimePicker } from "./datetimepicker.js";
|
|
74
74
|
// sc-1378 — `<FilePicker>` (web). Wraps a hidden DOM `<input type="file">`
|
|
75
75
|
// because react-native-web has no native equivalent. The native build's
|
|
76
|
-
// counterpart (./filepicker.native.js)
|
|
77
|
-
// a vetted expo-document-picker pin lands. `isSupported` is a static
|
|
78
|
-
// boolean widgets branch on to hide the trigger on native.
|
|
76
|
+
// counterpart (./filepicker.native.js) opens the OS document picker.
|
|
79
77
|
export { FilePicker } from "./filepicker.js";
|
|
80
78
|
// REQ-AI-AGENT-DESIGN (sc-3696) — `<Gradient>` (web). Paints a CSS
|
|
81
79
|
// `linear-gradient` on a View; the native build (./gradient.native.js) delegates
|
|
@@ -31,10 +31,8 @@ export { Icon } from "./icon.js";
|
|
|
31
31
|
// `./datetimepicker.js`. Both files honour the same ISO 8601 value /
|
|
32
32
|
// onChange contract.
|
|
33
33
|
export { DateTimePicker } from "./datetimepicker.native.js";
|
|
34
|
-
// sc-1378 — `<FilePicker>` (native).
|
|
35
|
-
// wraps a hidden
|
|
36
|
-
// equivalent yet, so this build renders a disabled trigger and exposes
|
|
37
|
-
// `FilePicker.isSupported = false` for widgets to branch on.
|
|
34
|
+
// sc-1378 — `<FilePicker>` (native). Opens the OS document picker
|
|
35
|
+
// (expo-document-picker); the web counterpart wraps a hidden file input.
|
|
38
36
|
export { FilePicker } from "./filepicker.native.js";
|
|
39
37
|
// REQ-AI-AGENT-DESIGN (sc-3696) — `<Gradient>` (native). Delegates to
|
|
40
38
|
// expo-linear-gradient (pinned unconditionally in every export); the web build
|
package/dist/property-schema.js
CHANGED
|
@@ -34,6 +34,11 @@ const VALID_TYPES = new Set([
|
|
|
34
34
|
// `useNavigation().goTo(pageId, params)`. Bare uuid string — tenant-copy
|
|
35
35
|
// remaps it through the layout walk's pageId map (see tenant-copy.service.js).
|
|
36
36
|
"pageRef",
|
|
37
|
+
// REQ-BLD-19: `sectionRef` is a section picker for the widget's OWN page,
|
|
38
|
+
// emitting a bare layout node id; a widget scrolls to it via
|
|
39
|
+
// `useNavigation().scrollToSection(id)`. Node ids survive Copy Workspace
|
|
40
|
+
// unchanged, and page duplication remaps them, so no per-prop hook.
|
|
41
|
+
"sectionRef",
|
|
37
42
|
// REQ-WDG-RICHTEXT: `richText` is a rich-text (HTML) string. Renders a
|
|
38
43
|
// Tiptap editor in the Studio Properties Panel; the widget renders the
|
|
39
44
|
// sanitised HTML. Stored as a string, so no tenant-copy remap is needed.
|
|
@@ -153,6 +158,7 @@ function coerceLeaf(def, value, path, errors) {
|
|
|
153
158
|
case "recordBinding":
|
|
154
159
|
case "groupRef":
|
|
155
160
|
case "pageRef":
|
|
161
|
+
case "sectionRef":
|
|
156
162
|
case "agentRef":
|
|
157
163
|
case "richText":
|
|
158
164
|
case "asset":
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The `<Select>` rules that need no renderer: where focus lands when the
|
|
2
|
+
// sheet opens, the draft a `multiple` select edits until Done, and what its
|
|
3
|
+
// trigger shows. Both hosts share them, and the node suite tests them.
|
|
4
|
+
|
|
5
|
+
/** The default trigger text once the picked labels no longer fit. */
|
|
6
|
+
export function defaultSelectedCountText(count) {
|
|
7
|
+
return `${count} selected`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A `values` prop as a list; anything else is no choice. */
|
|
11
|
+
export function valueList(values) {
|
|
12
|
+
return Array.isArray(values) ? values : [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The option focused when the sheet opens: the first picked one that can be
|
|
17
|
+
* picked, else the first enabled one, else -1.
|
|
18
|
+
*
|
|
19
|
+
* @param {Array<{disabled?: boolean}>} options
|
|
20
|
+
* @param {(option: object) => boolean} isPicked
|
|
21
|
+
*/
|
|
22
|
+
export function initialFocusIndex(options, isPicked) {
|
|
23
|
+
const list = Array.isArray(options) ? options : [];
|
|
24
|
+
const picked = list.findIndex((option) => isPicked(option) && !option.disabled);
|
|
25
|
+
return picked !== -1 ? picked : list.findIndex((option) => !option.disabled);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The draft with `value` added, or removed when it is already in it. Values
|
|
30
|
+
* compare strictly, so 1 and "1" are two choices.
|
|
31
|
+
*
|
|
32
|
+
* @param {Array<string|number>} draft
|
|
33
|
+
* @param {string|number} value
|
|
34
|
+
*/
|
|
35
|
+
export function toggleValue(draft, value) {
|
|
36
|
+
const list = valueList(draft);
|
|
37
|
+
return list.includes(value) ? list.filter((own) => own !== value) : [...list, value];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The values Done reports: the picked options in the order they are listed,
|
|
42
|
+
* then any picked value no option carries any more, so confirming without a
|
|
43
|
+
* change never drops a stored value.
|
|
44
|
+
*
|
|
45
|
+
* @param {Array<{value: string|number}>} options
|
|
46
|
+
* @param {Array<string|number>} draft
|
|
47
|
+
*/
|
|
48
|
+
export function committedValues(options, draft) {
|
|
49
|
+
const list = valueList(draft);
|
|
50
|
+
const known = (Array.isArray(options) ? options : []).map((option) => option.value);
|
|
51
|
+
return [
|
|
52
|
+
...known.filter((value) => list.includes(value)),
|
|
53
|
+
...list.filter((value) => !known.includes(value)),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Whether two value lists hold the same values, whatever their order. */
|
|
58
|
+
export function sameValues(a, b) {
|
|
59
|
+
const left = valueList(a);
|
|
60
|
+
const right = valueList(b);
|
|
61
|
+
return left.length === right.length && left.every((value) => right.includes(value));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The options whose value is picked, in the order they are listed. */
|
|
65
|
+
export function pickedOptions(options, values) {
|
|
66
|
+
const list = valueList(values);
|
|
67
|
+
return (Array.isArray(options) ? options : []).filter((option) => list.includes(option.value));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What a `multiple` trigger shows: the placeholder with nothing picked, the
|
|
72
|
+
* picked labels as chips once they are measured to fit on its one line,
|
|
73
|
+
* otherwise the count. Until both widths are known it shows the count, so
|
|
74
|
+
* the trigger never draws chips that overflow it.
|
|
75
|
+
*
|
|
76
|
+
* @param {{picked: number, available?: number|null, needed?: number|null}} sizes
|
|
77
|
+
* @returns {"placeholder"|"chips"|"count"}
|
|
78
|
+
*/
|
|
79
|
+
export function triggerSummary({ picked, available, needed }) {
|
|
80
|
+
if (!picked) return "placeholder";
|
|
81
|
+
const measured = Number.isFinite(available) && Number.isFinite(needed) && available > 0;
|
|
82
|
+
return measured && needed <= available ? "chips" : "count";
|
|
83
|
+
}
|