@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.
@@ -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 });
@@ -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
- nextOptionIndex,
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: (event) => {
127
- if (NAVIGATION_KEYS.includes(event.key)) {
128
- event.preventDefault();
129
- focusNode(nodes.current[nextOptionIndex(options, index, event.key)]);
130
- return;
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
- flexDirection: "row",
142
- alignItems: "center",
136
+ style: ({ pressed }) =>
137
+ optionRowStyle(t, {
138
+ pressed,
139
+ disabled: option.disabled,
143
140
  justifyContent: multiple ? "flex-start" : "space-between",
144
- gap: t.rowGap,
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,
@@ -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 { resolveTabsTokens } from "./tabs-tokens.js";
14
-
15
- const SEGMENT_INSET = 4;
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
- // A concentric outer radius keeps the inset chip's corners parallel. The
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
  );