@colixsystems/widget-sdk 0.244.0 → 0.246.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 +42 -2
- package/dist/button-tokens.js +154 -0
- package/dist/button-view.js +251 -93
- package/dist/button.js +5 -3
- package/dist/button.native.js +6 -4
- package/dist/contract.cjs +90 -35
- package/dist/contract.js +90 -35
- package/dist/detail-parts-view.js +31 -2
- package/dist/file-image-logic.js +162 -0
- package/dist/file-image-source.js +71 -0
- package/dist/file-image-view.js +119 -0
- package/dist/file-image.js +8 -0
- package/dist/file-image.native.js +8 -0
- package/dist/form-control-logic.js +23 -0
- package/dist/form-control-tokens.js +25 -10
- package/dist/index.d.ts +258 -18
- package/dist/index.js +7 -1
- package/dist/index.native.js +7 -1
- package/dist/menu-view.js +168 -0
- package/dist/overlay-tokens.js +50 -0
- package/dist/overlay-view.js +67 -24
- package/dist/overlay.native.js +3 -1
- package/dist/popover-tokens.js +145 -0
- package/dist/popover-view.js +252 -0
- package/dist/popover.js +23 -0
- package/dist/popover.native.js +22 -0
- package/dist/radio-group-logic.js +61 -0
- package/dist/radio-group-tokens.js +62 -0
- package/dist/radio-group-view.js +262 -0
- package/dist/radio-group.js +12 -0
- package/dist/radio-group.native.js +7 -0
- package/dist/select-view.js +15 -31
- package/dist/tabs-tokens.js +59 -0
- package/dist/tabs-view.js +9 -43
- package/dist/tooltip-view.js +137 -0
- package/dist/web-modal.js +95 -23
- package/package.json +1 -1
- package/src/contract.js +90 -35
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The values `<RadioGroup>` paints with. Its list and card variants are the
|
|
2
|
+
// form controls' own `muted` scope (`resolveFormControlTokens`); the segmented
|
|
3
|
+
// one is the `tabs` scope's segmented strip. Platform-free.
|
|
4
|
+
|
|
5
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
6
|
+
import { CHECKBOX_SIZE, resolveFormControlTokens } from "./form-control-tokens.js";
|
|
7
|
+
import { resolveTabsTokens } from "./tabs-tokens.js";
|
|
8
|
+
|
|
9
|
+
export const RADIO_DOT_SIZE = 8;
|
|
10
|
+
// Narrow enough for two cards side by side in a phone-width panel.
|
|
11
|
+
export const RADIO_CARD_MIN_WIDTH = 140;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve one radio group from the workspace theme.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} theme — the resolved workspace theme (`useTheme()` shape).
|
|
17
|
+
*/
|
|
18
|
+
export function resolveRadioGroupTokens(theme) {
|
|
19
|
+
const base = theme && typeof theme === "object" ? theme : DEFAULT_THEME_TOKENS;
|
|
20
|
+
const colors = base.colors || DEFAULT_THEME_TOKENS.colors;
|
|
21
|
+
const spacing = base.spacing || DEFAULT_THEME_TOKENS.spacing;
|
|
22
|
+
const typography = base.typography || DEFAULT_THEME_TOKENS.typography;
|
|
23
|
+
const field = resolveFormControlTokens(base);
|
|
24
|
+
return {
|
|
25
|
+
...field,
|
|
26
|
+
// A picked row lifts out of the muted panel onto the surface.
|
|
27
|
+
pickedBackground: colors.surface,
|
|
28
|
+
descriptionColor: colors.onSurfaceMuted,
|
|
29
|
+
descriptionSize: typography.sizes.sm,
|
|
30
|
+
optionGap: spacing.sm,
|
|
31
|
+
cardPadding: spacing.md,
|
|
32
|
+
segmented: resolveTabsTokens(base, { style: "segmented" }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The round mark of one option: the input surface, filled with the accent
|
|
38
|
+
* and a dot when picked, so it reads as the `<Checkbox>` box's sibling.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} t — resolved radio group tokens.
|
|
41
|
+
* @param {{checked: boolean, invalid?: boolean}} state
|
|
42
|
+
*/
|
|
43
|
+
export function radioMarkStyles(t, { checked, invalid = false }) {
|
|
44
|
+
return {
|
|
45
|
+
ring: {
|
|
46
|
+
width: CHECKBOX_SIZE,
|
|
47
|
+
height: CHECKBOX_SIZE,
|
|
48
|
+
borderRadius: CHECKBOX_SIZE / 2,
|
|
49
|
+
borderWidth: 1,
|
|
50
|
+
alignItems: "center",
|
|
51
|
+
justifyContent: "center",
|
|
52
|
+
borderColor: checked ? t.accent : invalid ? t.errorColor : t.borderColor,
|
|
53
|
+
backgroundColor: checked ? t.accent : t.background,
|
|
54
|
+
},
|
|
55
|
+
dot: {
|
|
56
|
+
width: RADIO_DOT_SIZE,
|
|
57
|
+
height: RADIO_DOT_SIZE,
|
|
58
|
+
borderRadius: RADIO_DOT_SIZE / 2,
|
|
59
|
+
backgroundColor: t.onAccent,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// The ONE implementation behind the `<RadioGroup>` SDK primitive. The platform
|
|
2
|
+
// bindings (`radio-group.js` / `radio-group.native.js`) differ only in where
|
|
3
|
+
// the primitives come from, so the hosts cannot drift.
|
|
4
|
+
//
|
|
5
|
+
// Widgets each drew their own pick-one list, marking the choice with a glyph,
|
|
6
|
+
// an icon or a tab role. This one has the radio role and the app's look.
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { useHostTheme } from "./hooks.js";
|
|
10
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
11
|
+
import { Icon } from "./icon.js";
|
|
12
|
+
import { CHECKBOX_SIZE, CONTROL_MIN_TARGET, focusHaloStyle } from "./form-control-tokens.js";
|
|
13
|
+
import {
|
|
14
|
+
errorMessage,
|
|
15
|
+
hasNote,
|
|
16
|
+
isShown,
|
|
17
|
+
isText,
|
|
18
|
+
needsFrame,
|
|
19
|
+
spaceActivates,
|
|
20
|
+
spokenLabel,
|
|
21
|
+
} from "./form-control-logic.js";
|
|
22
|
+
import { makeFieldFrame } from "./form-control-view.js";
|
|
23
|
+
import { RADIO_CARD_MIN_WIDTH, radioMarkStyles, resolveRadioGroupTokens } from "./radio-group-tokens.js";
|
|
24
|
+
import {
|
|
25
|
+
arrowTargetIndex,
|
|
26
|
+
radioGroupVariant,
|
|
27
|
+
tabStopIndex,
|
|
28
|
+
} from "./radio-group-logic.js";
|
|
29
|
+
import { TAB_ITEM_LAYOUT, segmentedStripStyles, tabLabelStyle } from "./tabs-tokens.js";
|
|
30
|
+
|
|
31
|
+
const h = React.createElement;
|
|
32
|
+
|
|
33
|
+
function groupStyle(kind, t, theme, invalid) {
|
|
34
|
+
if (kind === "segmented") {
|
|
35
|
+
const strip = segmentedStripStyles(t.segmented, theme);
|
|
36
|
+
return [
|
|
37
|
+
strip.bar,
|
|
38
|
+
strip.content,
|
|
39
|
+
// A form row's control spans the row; its segments share it evenly.
|
|
40
|
+
{ flexDirection: "row", alignSelf: "stretch", borderWidth: 1, borderColor: "transparent" },
|
|
41
|
+
invalid ? { borderColor: t.errorColor } : null,
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
if (kind === "card") return { flexDirection: "row", flexWrap: "wrap", gap: t.optionGap };
|
|
45
|
+
return {
|
|
46
|
+
borderWidth: 1,
|
|
47
|
+
borderColor: invalid ? t.errorColor : t.borderColor,
|
|
48
|
+
borderRadius: t.radius,
|
|
49
|
+
backgroundColor: t.background,
|
|
50
|
+
overflow: "hidden",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function optionStyle(kind, t, theme, { checked, invalid, focused, web }) {
|
|
55
|
+
if (kind === "segmented") {
|
|
56
|
+
return [
|
|
57
|
+
TAB_ITEM_LAYOUT,
|
|
58
|
+
segmentedStripStyles(t.segmented, theme).item(checked),
|
|
59
|
+
{ flexGrow: 1, flexShrink: 1, flexBasis: 0 },
|
|
60
|
+
// The segment's own border is its lift, so focus draws outside it.
|
|
61
|
+
focused && web
|
|
62
|
+
? { outlineWidth: 2, outlineStyle: "solid", outlineColor: t.focusColor, outlineOffset: 1 }
|
|
63
|
+
: null,
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
const row = {
|
|
67
|
+
flexDirection: "row",
|
|
68
|
+
alignItems: kind === "card" ? "flex-start" : "center",
|
|
69
|
+
gap: t.optionGap,
|
|
70
|
+
minHeight: CONTROL_MIN_TARGET,
|
|
71
|
+
};
|
|
72
|
+
if (kind === "card") {
|
|
73
|
+
return [
|
|
74
|
+
row,
|
|
75
|
+
{
|
|
76
|
+
flexGrow: 1,
|
|
77
|
+
flexBasis: RADIO_CARD_MIN_WIDTH,
|
|
78
|
+
padding: t.cardPadding,
|
|
79
|
+
borderWidth: 1,
|
|
80
|
+
borderRadius: t.radius,
|
|
81
|
+
borderColor: checked ? t.accent : invalid ? t.errorColor : t.borderColor,
|
|
82
|
+
backgroundColor: checked ? t.pickedBackground : t.background,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
return [
|
|
87
|
+
row,
|
|
88
|
+
{ paddingHorizontal: t.paddingHorizontal, paddingVertical: t.paddingVertical },
|
|
89
|
+
checked ? { backgroundColor: t.pickedBackground } : null,
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Bind the shared radio group to one platform's primitives.
|
|
95
|
+
*
|
|
96
|
+
* @param {{View: any, Text: any, Pressable: any, Platform: any}} rn
|
|
97
|
+
*/
|
|
98
|
+
export function makeRadioGroup(rn) {
|
|
99
|
+
const { View, Text, Pressable, Platform } = rn;
|
|
100
|
+
const FieldFrame = makeFieldFrame(rn);
|
|
101
|
+
const web = Boolean(Platform && Platform.OS === "web");
|
|
102
|
+
|
|
103
|
+
function RadioMark({ t, checked, invalid, focused }) {
|
|
104
|
+
const mark = radioMarkStyles(t, { checked, invalid });
|
|
105
|
+
return h(
|
|
106
|
+
View,
|
|
107
|
+
{ style: focusHaloStyle(t, focused, CHECKBOX_SIZE / 2) },
|
|
108
|
+
h(View, { style: mark.ring }, checked ? h(View, { style: mark.dot }) : null),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function OptionText({ t, option, kind }) {
|
|
113
|
+
const label = isText(option.label)
|
|
114
|
+
? h(
|
|
115
|
+
Text,
|
|
116
|
+
{
|
|
117
|
+
style: {
|
|
118
|
+
color: t.textColor,
|
|
119
|
+
fontFamily: t.fontFamily,
|
|
120
|
+
fontSize: t.fontSize,
|
|
121
|
+
...(kind === "card" ? { fontWeight: "600" } : null),
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
option.label,
|
|
125
|
+
)
|
|
126
|
+
: option.label;
|
|
127
|
+
const description = isText(option.description)
|
|
128
|
+
? h(
|
|
129
|
+
Text,
|
|
130
|
+
{ style: { color: t.descriptionColor, fontFamily: t.fontFamily, fontSize: t.descriptionSize } },
|
|
131
|
+
option.description,
|
|
132
|
+
)
|
|
133
|
+
: isShown(option.description)
|
|
134
|
+
? option.description
|
|
135
|
+
: null;
|
|
136
|
+
return h(View, { style: { flexGrow: 1, flexShrink: 1, gap: 2 } }, label, description);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function optionContent({ t, theme, kind, option, checked, invalid, focused }) {
|
|
140
|
+
if (kind === "segmented") {
|
|
141
|
+
const s = t.segmented;
|
|
142
|
+
return [
|
|
143
|
+
option.icon
|
|
144
|
+
? h(Icon, { key: "icon", name: option.icon, size: 16, color: checked ? s.activeTextColor : s.textColor })
|
|
145
|
+
: null,
|
|
146
|
+
h(Text, { key: "label", numberOfLines: 1, style: tabLabelStyle(s, theme, checked) }, option.label),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
return [
|
|
150
|
+
h(RadioMark, { key: "mark", t, checked, invalid, focused }),
|
|
151
|
+
option.icon
|
|
152
|
+
? h(Icon, { key: "icon", name: option.icon, size: 18, color: checked ? t.accent : t.descriptionColor })
|
|
153
|
+
: null,
|
|
154
|
+
h(OptionText, { key: "text", t, option, kind }),
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const RadioGroup = React.forwardRef(function RadioGroup(props, ref) {
|
|
159
|
+
const {
|
|
160
|
+
options,
|
|
161
|
+
value,
|
|
162
|
+
onValueChange,
|
|
163
|
+
variant,
|
|
164
|
+
label,
|
|
165
|
+
helper,
|
|
166
|
+
error,
|
|
167
|
+
disabled = false,
|
|
168
|
+
accessibilityLabel,
|
|
169
|
+
style,
|
|
170
|
+
containerStyle,
|
|
171
|
+
...rest
|
|
172
|
+
} = props;
|
|
173
|
+
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
174
|
+
const t = resolveRadioGroupTokens(theme);
|
|
175
|
+
const kind = radioGroupVariant(variant);
|
|
176
|
+
const list = Array.isArray(options) ? options.filter((o) => o && typeof o === "object") : [];
|
|
177
|
+
const nodes = React.useRef([]);
|
|
178
|
+
const [focusedIndex, setFocusedIndex] = React.useState(-1);
|
|
179
|
+
const noteId = React.useId();
|
|
180
|
+
const invalid = isShown(error);
|
|
181
|
+
const noted = hasNote({ helper, error });
|
|
182
|
+
const framed = needsFrame({ label, helper, error });
|
|
183
|
+
const tabStop = tabStopIndex(list, value, disabled);
|
|
184
|
+
|
|
185
|
+
const pick = (option) => {
|
|
186
|
+
if (disabled || option.disabled || option.value === value) return;
|
|
187
|
+
if (typeof onValueChange === "function") onValueChange(option.value, option);
|
|
188
|
+
};
|
|
189
|
+
const keyHandler = (option, index) => (event) => {
|
|
190
|
+
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
191
|
+
const target = arrowTargetIndex(list, index, event.key);
|
|
192
|
+
if (target === -1) return spaceActivates(() => pick(option))(event);
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
pick(list[target]);
|
|
195
|
+
const node = nodes.current[target];
|
|
196
|
+
if (node && typeof node.focus === "function") node.focus();
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const renderOption = (option, index) => {
|
|
200
|
+
const checked = option.value === value;
|
|
201
|
+
const optionDisabled = disabled || Boolean(option.disabled);
|
|
202
|
+
const focused = focusedIndex === index;
|
|
203
|
+
return h(
|
|
204
|
+
Pressable,
|
|
205
|
+
{
|
|
206
|
+
key: String(option.value),
|
|
207
|
+
ref: (node) => {
|
|
208
|
+
nodes.current[index] = node;
|
|
209
|
+
},
|
|
210
|
+
onPress: () => pick(option),
|
|
211
|
+
...(optionDisabled ? null : { onKeyDown: keyHandler(option, index) }),
|
|
212
|
+
onFocus: () => setFocusedIndex(index),
|
|
213
|
+
onBlur: () => setFocusedIndex((at) => (at === index ? -1 : at)),
|
|
214
|
+
disabled: optionDisabled,
|
|
215
|
+
// accessibilityRole + aria-*: the pair that announces on both hosts.
|
|
216
|
+
accessibilityRole: "radio",
|
|
217
|
+
"aria-checked": checked,
|
|
218
|
+
"aria-disabled": optionDisabled,
|
|
219
|
+
accessibilityLabel: option.accessibilityLabel,
|
|
220
|
+
testID: option.testID,
|
|
221
|
+
// One tab stop per group on the web; the arrows move between options.
|
|
222
|
+
...(web ? { tabIndex: index === tabStop ? 0 : -1 } : null),
|
|
223
|
+
style: ({ pressed }) => [
|
|
224
|
+
optionStyle(kind, t, theme, { checked, invalid, focused, web }),
|
|
225
|
+
pressed && !optionDisabled ? { opacity: 0.7 } : null,
|
|
226
|
+
option.disabled && !disabled ? { opacity: t.disabledOpacity } : null,
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
...optionContent({ t, theme, kind, option, checked, invalid, focused }),
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const group = h(
|
|
234
|
+
View,
|
|
235
|
+
{
|
|
236
|
+
...rest,
|
|
237
|
+
ref,
|
|
238
|
+
accessibilityRole: "radiogroup",
|
|
239
|
+
accessibilityLabel: spokenLabel(accessibilityLabel, label),
|
|
240
|
+
"aria-disabled": disabled,
|
|
241
|
+
...(invalid ? { "aria-invalid": true } : null),
|
|
242
|
+
...(noted ? { "aria-describedby": noteId } : null),
|
|
243
|
+
style: [
|
|
244
|
+
groupStyle(kind, t, theme, invalid),
|
|
245
|
+
// The frame dims a framed group as a whole.
|
|
246
|
+
disabled && !framed ? { opacity: t.disabledOpacity } : null,
|
|
247
|
+
style,
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
...list.map(renderOption),
|
|
251
|
+
);
|
|
252
|
+
if (!framed) return group;
|
|
253
|
+
return h(
|
|
254
|
+
FieldFrame,
|
|
255
|
+
{ t, label, helper, error: errorMessage(error), disabled, noteId, style: containerStyle },
|
|
256
|
+
group,
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
RadioGroup.displayName = "RadioGroup";
|
|
261
|
+
return RadioGroup;
|
|
262
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// `<RadioGroup>` (web). Binds ./radio-group-view.js to react-native-web; see
|
|
2
|
+
// ./primitives.js for why not the bare specifier.
|
|
3
|
+
|
|
4
|
+
import * as ReactNative from "react-native-web";
|
|
5
|
+
import { makeRadioGroup } from "./radio-group-view.js";
|
|
6
|
+
|
|
7
|
+
export const RadioGroup = makeRadioGroup({
|
|
8
|
+
View: ReactNative.View,
|
|
9
|
+
Text: ReactNative.Text,
|
|
10
|
+
Pressable: ReactNative.Pressable,
|
|
11
|
+
Platform: ReactNative.Platform,
|
|
12
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// `<RadioGroup>` (native). Binds ./radio-group-view.js to React Native's own
|
|
2
|
+
// primitives; only the import differs from ./radio-group.js.
|
|
3
|
+
|
|
4
|
+
import { View, Text, Pressable, Platform } from "react-native";
|
|
5
|
+
import { makeRadioGroup } from "./radio-group-view.js";
|
|
6
|
+
|
|
7
|
+
export const RadioGroup = makeRadioGroup({ View, Text, Pressable, Platform });
|
package/dist/select-view.js
CHANGED
|
@@ -13,10 +13,9 @@ import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
|
13
13
|
import { Icon } from "./icon.js";
|
|
14
14
|
import { TEXT_FIELD_TOKEN_NAMES } from "./text-field-tokens.js";
|
|
15
15
|
import {
|
|
16
|
-
CONTROL_MIN_TARGET,
|
|
17
16
|
FIELD_OUTLINE_OFF,
|
|
18
17
|
fieldStateStyle,
|
|
19
|
-
|
|
18
|
+
optionRowStyle,
|
|
20
19
|
resolveFormControlTokens,
|
|
21
20
|
selectedOption,
|
|
22
21
|
} from "./form-control-tokens.js";
|
|
@@ -25,10 +24,11 @@ import {
|
|
|
25
24
|
hasNote,
|
|
26
25
|
isShown,
|
|
27
26
|
isText,
|
|
27
|
+
optionKeyDown,
|
|
28
28
|
shareRef,
|
|
29
|
-
spaceActivates,
|
|
30
29
|
spokenLabel,
|
|
31
30
|
} from "./form-control-logic.js";
|
|
31
|
+
import { popoverPanelStyle } from "./popover-tokens.js";
|
|
32
32
|
import { makeFieldFrame } from "./form-control-view.js";
|
|
33
33
|
import { checkboxBox } from "./checkbox-view.js";
|
|
34
34
|
import {
|
|
@@ -43,7 +43,6 @@ import {
|
|
|
43
43
|
} from "./select-logic.js";
|
|
44
44
|
|
|
45
45
|
const h = React.createElement;
|
|
46
|
-
const NAVIGATION_KEYS = ["ArrowDown", "ArrowUp", "Home", "End"];
|
|
47
46
|
// Wide enough that no picked chip shrinks while it is measured.
|
|
48
47
|
const MEASURE_SPAN = 10000;
|
|
49
48
|
// A compact chip that fits the field's own line, so the trigger keeps its height.
|
|
@@ -123,33 +122,23 @@ export function makeSelect(rn) {
|
|
|
123
122
|
}
|
|
124
123
|
},
|
|
125
124
|
onPress: pick,
|
|
126
|
-
onKeyDown: (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
spaceActivates(pick)(event);
|
|
133
|
-
},
|
|
125
|
+
onKeyDown: optionKeyDown({
|
|
126
|
+
options,
|
|
127
|
+
index,
|
|
128
|
+
focusAt: (next) => focusNode(nodes.current[next]),
|
|
129
|
+
activate: pick,
|
|
130
|
+
}),
|
|
134
131
|
disabled: option.disabled,
|
|
135
132
|
role: "option",
|
|
136
133
|
"aria-selected": selected,
|
|
137
134
|
"aria-disabled": !!option.disabled,
|
|
138
135
|
testID: option.testID,
|
|
139
|
-
style: ({ pressed }) =>
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
style: ({ pressed }) =>
|
|
137
|
+
optionRowStyle(t, {
|
|
138
|
+
pressed,
|
|
139
|
+
disabled: option.disabled,
|
|
143
140
|
justifyContent: multiple ? "flex-start" : "space-between",
|
|
144
|
-
|
|
145
|
-
minHeight: CONTROL_MIN_TARGET,
|
|
146
|
-
paddingHorizontal: t.paddingHorizontal,
|
|
147
|
-
paddingVertical: t.paddingVertical,
|
|
148
|
-
borderRadius: t.radius,
|
|
149
|
-
},
|
|
150
|
-
pressed ? { opacity: 0.7 } : null,
|
|
151
|
-
option.disabled ? { opacity: t.disabledOpacity } : null,
|
|
152
|
-
],
|
|
141
|
+
}),
|
|
153
142
|
},
|
|
154
143
|
multiple ? checkboxBox(View, t, { checked: selected }) : null,
|
|
155
144
|
h(
|
|
@@ -417,12 +406,7 @@ export function makeSelect(rn) {
|
|
|
417
406
|
size: "sm",
|
|
418
407
|
accessibilityLabel: isText(sheetTitle) ? String(sheetTitle) : name,
|
|
419
408
|
...(closeAccessibilityLabel ? { closeAccessibilityLabel } : null),
|
|
420
|
-
style: {
|
|
421
|
-
backgroundColor: t.popover.background,
|
|
422
|
-
borderWidth: 1,
|
|
423
|
-
borderColor: t.popover.borderColor,
|
|
424
|
-
padding: t.rowGap,
|
|
425
|
-
},
|
|
409
|
+
style: { ...popoverPanelStyle(t.popover, "dialog"), padding: t.rowGap },
|
|
426
410
|
},
|
|
427
411
|
h(OptionSheet, {
|
|
428
412
|
t,
|
package/dist/tabs-tokens.js
CHANGED
|
@@ -67,3 +67,62 @@ export function resolveTabsTokens(theme, overrides) {
|
|
|
67
67
|
rule: colors.border,
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
const SEGMENT_INSET = 4;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The segmented strip's surfaces: the muted bar, its inset and each segment.
|
|
75
|
+
* `<Tabs>` and a segmented `<RadioGroup>` both draw from it, so they match.
|
|
76
|
+
* A concentric outer radius keeps the inset chip's corners parallel, and the
|
|
77
|
+
* active segment is outlined as well as lifted: a shadow alone disappears on
|
|
78
|
+
* a dark theme.
|
|
79
|
+
*
|
|
80
|
+
* @param {object} t — resolved tab tokens.
|
|
81
|
+
* @param {object} theme — the resolved workspace theme.
|
|
82
|
+
*/
|
|
83
|
+
export function segmentedStripStyles(t, theme) {
|
|
84
|
+
const elevation = (theme && theme.elevation) || DEFAULT_THEME_TOKENS.elevation;
|
|
85
|
+
return {
|
|
86
|
+
bar: {
|
|
87
|
+
backgroundColor: t.barBackground,
|
|
88
|
+
borderRadius: t.radius + 2,
|
|
89
|
+
alignSelf: "flex-start",
|
|
90
|
+
maxWidth: "100%",
|
|
91
|
+
},
|
|
92
|
+
content: { padding: SEGMENT_INSET, gap: SEGMENT_INSET },
|
|
93
|
+
item: (active) => ({
|
|
94
|
+
borderRadius: t.radius,
|
|
95
|
+
borderWidth: 1,
|
|
96
|
+
borderColor: active ? t.rule : "transparent",
|
|
97
|
+
...(active ? { backgroundColor: t.activeSurface, ...(elevation.sm || {}) } : null),
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** One tab or segment's box, before its style's own surface. */
|
|
103
|
+
export const TAB_ITEM_LAYOUT = Object.freeze({
|
|
104
|
+
flexDirection: "row",
|
|
105
|
+
alignItems: "center",
|
|
106
|
+
justifyContent: "center",
|
|
107
|
+
gap: 6,
|
|
108
|
+
minHeight: 44,
|
|
109
|
+
paddingHorizontal: 12,
|
|
110
|
+
paddingVertical: 8,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A tab or segment's label.
|
|
115
|
+
*
|
|
116
|
+
* @param {object} t — resolved tab tokens.
|
|
117
|
+
* @param {object} theme — the resolved workspace theme.
|
|
118
|
+
* @param {boolean} active
|
|
119
|
+
*/
|
|
120
|
+
export function tabLabelStyle(t, theme, active) {
|
|
121
|
+
const typography = (theme && theme.typography) || DEFAULT_THEME_TOKENS.typography;
|
|
122
|
+
return {
|
|
123
|
+
fontFamily: typography.fontFamily,
|
|
124
|
+
fontSize: typography.sizes.sm,
|
|
125
|
+
fontWeight: active ? "600" : "500",
|
|
126
|
+
color: active ? t.activeTextColor : t.textColor,
|
|
127
|
+
};
|
|
128
|
+
}
|
package/dist/tabs-view.js
CHANGED
|
@@ -10,13 +10,15 @@ import React from "react";
|
|
|
10
10
|
import { useHostTheme } from "./hooks.js";
|
|
11
11
|
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
12
12
|
import { Icon } from "./icon.js";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
import {
|
|
14
|
+
TAB_ITEM_LAYOUT,
|
|
15
|
+
resolveTabsTokens,
|
|
16
|
+
segmentedStripStyles,
|
|
17
|
+
tabLabelStyle,
|
|
18
|
+
} from "./tabs-tokens.js";
|
|
16
19
|
|
|
17
20
|
function stripStyles(t, theme) {
|
|
18
21
|
const spacing = theme.spacing || DEFAULT_THEME_TOKENS.spacing;
|
|
19
|
-
const elevation = theme.elevation || DEFAULT_THEME_TOKENS.elevation;
|
|
20
22
|
if (t.style === "pill") {
|
|
21
23
|
// An idle chip is outlined: a muted fill vanishes on a white card.
|
|
22
24
|
return {
|
|
@@ -42,26 +44,7 @@ function stripStyles(t, theme) {
|
|
|
42
44
|
}),
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
|
-
|
|
46
|
-
// bar hugs its segments, and the active one is outlined as well as lifted
|
|
47
|
-
// because a shadow alone disappears on a dark theme.
|
|
48
|
-
return {
|
|
49
|
-
bar: {
|
|
50
|
-
backgroundColor: t.barBackground,
|
|
51
|
-
borderRadius: t.radius + 2,
|
|
52
|
-
alignSelf: "flex-start",
|
|
53
|
-
maxWidth: "100%",
|
|
54
|
-
},
|
|
55
|
-
content: { padding: SEGMENT_INSET, gap: SEGMENT_INSET },
|
|
56
|
-
item: (active) => ({
|
|
57
|
-
borderRadius: t.radius,
|
|
58
|
-
borderWidth: 1,
|
|
59
|
-
borderColor: active ? t.rule : "transparent",
|
|
60
|
-
...(active
|
|
61
|
-
? { backgroundColor: t.activeSurface, ...(elevation.sm || {}) }
|
|
62
|
-
: null),
|
|
63
|
-
}),
|
|
64
|
-
};
|
|
47
|
+
return segmentedStripStyles(t, theme);
|
|
65
48
|
}
|
|
66
49
|
|
|
67
50
|
/**
|
|
@@ -98,7 +81,6 @@ export function makeTabs(rn) {
|
|
|
98
81
|
textColor,
|
|
99
82
|
radius,
|
|
100
83
|
});
|
|
101
|
-
const typography = theme.typography || DEFAULT_THEME_TOKENS.typography;
|
|
102
84
|
const strip = stripStyles(t, theme);
|
|
103
85
|
|
|
104
86
|
return React.createElement(
|
|
@@ -130,15 +112,7 @@ export function makeTabs(rn) {
|
|
|
130
112
|
"aria-disabled": !!item.disabled,
|
|
131
113
|
testID: item.testID,
|
|
132
114
|
style: [
|
|
133
|
-
|
|
134
|
-
flexDirection: "row",
|
|
135
|
-
alignItems: "center",
|
|
136
|
-
justifyContent: "center",
|
|
137
|
-
gap: 6,
|
|
138
|
-
minHeight: 44,
|
|
139
|
-
paddingHorizontal: 12,
|
|
140
|
-
paddingVertical: 8,
|
|
141
|
-
},
|
|
115
|
+
TAB_ITEM_LAYOUT,
|
|
142
116
|
strip.item(active),
|
|
143
117
|
item.disabled ? { opacity: 0.5 } : null,
|
|
144
118
|
],
|
|
@@ -148,15 +122,7 @@ export function makeTabs(rn) {
|
|
|
148
122
|
: null,
|
|
149
123
|
React.createElement(
|
|
150
124
|
Text,
|
|
151
|
-
{
|
|
152
|
-
numberOfLines: 1,
|
|
153
|
-
style: {
|
|
154
|
-
fontFamily: typography.fontFamily,
|
|
155
|
-
fontSize: typography.sizes.sm,
|
|
156
|
-
fontWeight: active ? "600" : "500",
|
|
157
|
-
color: ink,
|
|
158
|
-
},
|
|
159
|
-
},
|
|
125
|
+
{ numberOfLines: 1, style: tabLabelStyle(t, theme, active) },
|
|
160
126
|
item.label,
|
|
161
127
|
),
|
|
162
128
|
);
|