@lotics/ui 3.0.0 → 3.2.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/package.json +2 -1
- package/src/button.tsx +14 -1
- package/src/card_select_item.tsx +14 -6
- package/src/combobox.tsx +36 -5
- package/src/segmented_control.tsx +205 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./tokens": "./src/tokens.ts",
|
|
@@ -127,6 +127,7 @@
|
|
|
127
127
|
"./stepper": "./src/stepper.tsx",
|
|
128
128
|
"./step_progress": "./src/step_progress.tsx",
|
|
129
129
|
"./tabs": "./src/tabs.tsx",
|
|
130
|
+
"./segmented_control": "./src/segmented_control.tsx",
|
|
130
131
|
"./auto_sizer": "./src/auto_sizer.tsx",
|
|
131
132
|
"./animation_horizontal_slide": "./src/animation_horizontal_slide.tsx",
|
|
132
133
|
"./group_avatar": "./src/group_avatar.tsx",
|
package/src/button.tsx
CHANGED
|
@@ -22,6 +22,10 @@ interface ButtonPropsBase {
|
|
|
22
22
|
icon?: IconName;
|
|
23
23
|
alignSelf?: "flex-start" | "flex-end" | "center" | "stretch" | "auto";
|
|
24
24
|
color?: ButtonColor;
|
|
25
|
+
/** Corner shape. `pill` (default) is the fully-rounded capsule — keep it for
|
|
26
|
+
* icon-only and toolbar buttons, and to preserve existing screens. `rounded`
|
|
27
|
+
* is a 10px radius — use it for labelled CTAs / action buttons. */
|
|
28
|
+
shape?: "pill" | "rounded";
|
|
25
29
|
loading?: boolean;
|
|
26
30
|
disabled?: boolean;
|
|
27
31
|
tooltip?: string | UseTooltipOptions;
|
|
@@ -49,6 +53,7 @@ export function Button(props: ButtonProps) {
|
|
|
49
53
|
alignSelf,
|
|
50
54
|
title,
|
|
51
55
|
color,
|
|
56
|
+
shape = "pill",
|
|
52
57
|
style,
|
|
53
58
|
disabled,
|
|
54
59
|
tooltip,
|
|
@@ -108,8 +113,11 @@ export function Button(props: ButtonProps) {
|
|
|
108
113
|
disabled={disabledOrLoading}
|
|
109
114
|
// @ts-ignore hovered is a react-native-web extension not in base RN types
|
|
110
115
|
style={({ pressed, hovered }) => {
|
|
116
|
+
const isPrimary = color === "primary" && !disabled;
|
|
117
|
+
const restPrimary = isPrimary && !pressed && !hovered;
|
|
111
118
|
return [
|
|
112
119
|
{
|
|
120
|
+
borderRadius: shape === "rounded" ? 10 : 999,
|
|
113
121
|
backgroundColor: disabled
|
|
114
122
|
? getButtonDisabledBackgroundColor(color)
|
|
115
123
|
: pressed
|
|
@@ -118,6 +126,12 @@ export function Button(props: ButtonProps) {
|
|
|
118
126
|
? getButtonHoverColor(color)
|
|
119
127
|
: getButtonBackgroundColor(color),
|
|
120
128
|
},
|
|
129
|
+
// Subtle depth on the primary: a soft drop shadow lifts it off the
|
|
130
|
+
// surface and a whisper of top highlight + a gentle vertical shade (the
|
|
131
|
+
// app-icon technique) catch the light — premium, not skeuomorphic. The
|
|
132
|
+
// lift stays on hover/press; only the gradient drops so the wash shows.
|
|
133
|
+
isPrimary && { boxShadow: "inset 0 1px 0 rgba(255,255,255,0.08), 0 1px 2px rgba(0,0,0,0.18)" },
|
|
134
|
+
restPrimary && { backgroundImage: `linear-gradient(180deg, ${colors.zinc["700"]} 0%, ${colors.zinc["900"]} 100%)` },
|
|
121
135
|
styles.button,
|
|
122
136
|
style,
|
|
123
137
|
alignSelf && { alignSelf },
|
|
@@ -228,7 +242,6 @@ const styles = StyleSheet.create({
|
|
|
228
242
|
justifyContent: "center",
|
|
229
243
|
height: 40,
|
|
230
244
|
paddingHorizontal: 10,
|
|
231
|
-
borderRadius: 999,
|
|
232
245
|
// react-native-web extensions
|
|
233
246
|
...({ touchAction: "manipulation", cursor: "pointer", transitionDuration: "0.1s", transitionProperty: "background-color" } as ViewStyle),
|
|
234
247
|
},
|
package/src/card_select_item.tsx
CHANGED
|
@@ -5,27 +5,35 @@ import { PressableHighlight } from "./pressable_highlight";
|
|
|
5
5
|
interface CardSelectItemProps {
|
|
6
6
|
children: React.ReactNode;
|
|
7
7
|
onPress: () => void;
|
|
8
|
+
/** Persistent selection — when true, the ring stays on (the current item in a
|
|
9
|
+
* list with one active selection: an org picker's current org, a switcher's
|
|
10
|
+
* open item). Reads as `aria-pressed`. */
|
|
11
|
+
selected?: boolean;
|
|
8
12
|
testID?: string;
|
|
13
|
+
accessibilityLabel?: string;
|
|
9
14
|
style?: StyleProp<ViewStyle>;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
|
-
* A bordered, card-shaped button — the selectable
|
|
14
|
-
* (organization picker, login choices)
|
|
15
|
-
* `button` that shows a 2px ring on hover/press,
|
|
16
|
-
* focus ring.
|
|
18
|
+
* A bordered, card-shaped button — the selectable item used on auth screens
|
|
19
|
+
* (organization picker, login choices) and in entity switchers. Always
|
|
20
|
+
* interactive: a real focusable `button` that shows a 2px ring on hover/press,
|
|
21
|
+
* matching the global keyboard focus ring. Pass `selected` to keep that ring on
|
|
22
|
+
* for the current item in a single-selection list. For a static surface use Card.
|
|
17
23
|
*/
|
|
18
24
|
export function CardSelectItem(props: CardSelectItemProps) {
|
|
19
|
-
const { children, onPress, testID, style } = props;
|
|
25
|
+
const { children, onPress, selected = false, testID, accessibilityLabel, style } = props;
|
|
20
26
|
|
|
21
27
|
return (
|
|
22
28
|
<PressableHighlight
|
|
23
29
|
testID={testID}
|
|
24
30
|
accessibilityRole="button"
|
|
31
|
+
accessibilityLabel={accessibilityLabel}
|
|
32
|
+
aria-pressed={selected}
|
|
25
33
|
onPress={onPress}
|
|
26
34
|
style={(state: PressableStateCallbackType) => {
|
|
27
35
|
const hovered = (state as { hovered?: boolean }).hovered;
|
|
28
|
-
const active = hovered || state.pressed;
|
|
36
|
+
const active = selected || hovered || state.pressed;
|
|
29
37
|
return [styles.container, active && styles.ring, style];
|
|
30
38
|
}}
|
|
31
39
|
>
|
package/src/combobox.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
|
|
11
11
|
import { colors } from "./colors";
|
|
12
12
|
import { Text } from "./text";
|
|
13
|
-
import { Icon } from "./icon";
|
|
13
|
+
import { Icon, type IconName } from "./icon";
|
|
14
14
|
import { TextInputField } from "./text_input_field";
|
|
15
15
|
import { MenuButton } from "./menu_button";
|
|
16
16
|
import { ActivityIndicator } from "./activity_indicator";
|
|
@@ -58,6 +58,11 @@ export interface ComboboxProps<T extends string = string, D = unknown> {
|
|
|
58
58
|
recentOptions?: PickerOption<T, D>[];
|
|
59
59
|
loading?: boolean;
|
|
60
60
|
searchDebounceMs?: number;
|
|
61
|
+
/** Leading icon inside the input. OMIT (the default) for a SELECT — no leading
|
|
62
|
+
* glyph, a trailing chevron — so a "pick a code/record" combobox reads as a
|
|
63
|
+
* picker, not a free-text search box. Opt IN to the search-box look explicitly
|
|
64
|
+
* with `icon="search"` (leading glyph, no chevron). Multi-select shows neither. */
|
|
65
|
+
icon?: IconName;
|
|
61
66
|
placeholder?: string;
|
|
62
67
|
recentsLabel?: string;
|
|
63
68
|
emptyText?: string;
|
|
@@ -109,6 +114,7 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
109
114
|
recentOptions,
|
|
110
115
|
loading = false,
|
|
111
116
|
searchDebounceMs = 200,
|
|
117
|
+
icon,
|
|
112
118
|
placeholder,
|
|
113
119
|
recentsLabel = "Recent",
|
|
114
120
|
emptyText = "No results",
|
|
@@ -157,11 +163,14 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
157
163
|
return options.filter((o) => (o.label ?? o.value).toLowerCase().includes(q));
|
|
158
164
|
}, [isServer, searching, options, multi, reflectSelection, single, query]);
|
|
159
165
|
|
|
160
|
-
// Free-entry row, appended when the query matches no option
|
|
166
|
+
// Free-entry row, appended when the query matches no option exactly. Match
|
|
167
|
+
// both label AND value — a query equal to an option's value (e.g. a code typed
|
|
168
|
+
// in full) must NOT also offer "Add <code>", or the two share a key.
|
|
161
169
|
const customRow = useMemo((): PickerOption<T, D> | null => {
|
|
162
170
|
if (!allowCustom || !searching) return null;
|
|
163
171
|
const q = query.trim();
|
|
164
|
-
const
|
|
172
|
+
const ql = q.toLowerCase();
|
|
173
|
+
const exact = options.some((o) => (o.label ?? o.value).toLowerCase() === ql || String(o.value).toLowerCase() === ql);
|
|
165
174
|
if (exact) return null;
|
|
166
175
|
const label = customOptionLabel ? customOptionLabel(q) : `Add "${q}"`;
|
|
167
176
|
if (label === null) return null;
|
|
@@ -257,6 +266,10 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
257
266
|
const showList = open && !disabled && (searching || list.length > 0);
|
|
258
267
|
const activeOptionId =
|
|
259
268
|
activeIndex >= 0 && activeIndex < list.length ? optionId(activeIndex) : undefined;
|
|
269
|
+
// Select mode (no leading icon): show a trailing chevron so the field reads as
|
|
270
|
+
// a picker, not a free-text search. The clear ✕ owns the right slot when
|
|
271
|
+
// present, so suppress the chevron then.
|
|
272
|
+
const showChevron = !multi && !icon && !clearable;
|
|
260
273
|
|
|
261
274
|
return (
|
|
262
275
|
<View style={style}>
|
|
@@ -281,7 +294,7 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
281
294
|
<TextInputField
|
|
282
295
|
ref={inputRef}
|
|
283
296
|
testID={testID}
|
|
284
|
-
icon={multi ? undefined :
|
|
297
|
+
icon={multi ? undefined : icon}
|
|
285
298
|
value={query}
|
|
286
299
|
clearable={!multi && clearable}
|
|
287
300
|
clearLabel={clearLabel}
|
|
@@ -308,13 +321,18 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
308
321
|
autoFocus={autoFocus}
|
|
309
322
|
autoCapitalize="none"
|
|
310
323
|
autoCorrect={false}
|
|
311
|
-
style={multi ? styles.multiInput : undefined}
|
|
324
|
+
style={multi ? styles.multiInput : showChevron ? styles.selectInput : undefined}
|
|
312
325
|
role="combobox"
|
|
313
326
|
aria-expanded={showList}
|
|
314
327
|
aria-controls={listboxId}
|
|
315
328
|
aria-activedescendant={activeOptionId}
|
|
316
329
|
aria-autocomplete="list"
|
|
317
330
|
/>
|
|
331
|
+
{showChevron ? (
|
|
332
|
+
<View style={styles.chevron}>
|
|
333
|
+
<Icon name="chevrons-up-down" size={16} color={colors.zinc["400"]} />
|
|
334
|
+
</View>
|
|
335
|
+
) : null}
|
|
318
336
|
</View>
|
|
319
337
|
<Popover
|
|
320
338
|
open={showList}
|
|
@@ -421,6 +439,19 @@ const styles = StyleSheet.create({
|
|
|
421
439
|
borderWidth: 0,
|
|
422
440
|
height: 28,
|
|
423
441
|
},
|
|
442
|
+
// Select mode: room on the right for the trailing chevron.
|
|
443
|
+
selectInput: {
|
|
444
|
+
paddingRight: 32,
|
|
445
|
+
},
|
|
446
|
+
chevron: {
|
|
447
|
+
position: "absolute",
|
|
448
|
+
right: 10,
|
|
449
|
+
top: 0,
|
|
450
|
+
bottom: 0,
|
|
451
|
+
justifyContent: "center",
|
|
452
|
+
pointerEvents: "none",
|
|
453
|
+
},
|
|
454
|
+
|
|
424
455
|
chip: {
|
|
425
456
|
flexDirection: "row",
|
|
426
457
|
alignItems: "center",
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
import { StyleSheet, View, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { PressableHighlight } from "./pressable_highlight";
|
|
4
|
+
import { Text } from "./text";
|
|
5
|
+
import { colors, withAlpha } from "./colors";
|
|
6
|
+
|
|
7
|
+
export interface SegmentOption<T extends string> {
|
|
8
|
+
label: string;
|
|
9
|
+
value: T;
|
|
10
|
+
testID?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SegmentedControlProps<T extends string> {
|
|
14
|
+
/** Accessible name of the group (it renders no visible label of its own). */
|
|
15
|
+
accessibilityLabel: string;
|
|
16
|
+
/** 2–4 mutually-exclusive peers. Above ~4, use Picker — segments stop fitting. */
|
|
17
|
+
options: SegmentOption<T>[];
|
|
18
|
+
value: T;
|
|
19
|
+
onValueChange: (value: T) => void;
|
|
20
|
+
/** Disables the whole control (e.g. while a mode-specific action runs). */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** The track hugs its content by default (`alignSelf: flex-start`); pass
|
|
23
|
+
* `{ alignSelf: "stretch" }` to fill the parent's width. */
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A segmented control: 2–4 visible peer segments in an inset track, the
|
|
29
|
+
* selected one raised as a white card. Semantically a **radiogroup** — a
|
|
30
|
+
* single choice among peers, all visible — NOT a tablist (no panels) and not
|
|
31
|
+
* a Switch (not on/off). Reach for it on a binary/few-way *mode* switch where
|
|
32
|
+
* both options should be one-tap and on screen (a generator mode, a list/grid
|
|
33
|
+
* view, a day/week/month range). Above ~4 options, use `Picker`.
|
|
34
|
+
*
|
|
35
|
+
* Keyboard follows the WAI-ARIA radio pattern: roving tabindex (only the
|
|
36
|
+
* selected segment is a tab stop), arrows move focus AND select (wrapping),
|
|
37
|
+
* Space/Enter select the focused segment.
|
|
38
|
+
*/
|
|
39
|
+
export function SegmentedControl<T extends string>(props: SegmentedControlProps<T>) {
|
|
40
|
+
const { accessibilityLabel, options, value, onValueChange, disabled = false, style } = props;
|
|
41
|
+
const segmentRefs = useRef<Array<View | null>>([]);
|
|
42
|
+
|
|
43
|
+
const select = useCallback(
|
|
44
|
+
(index: number) => {
|
|
45
|
+
const option = options[index];
|
|
46
|
+
if (!option || option.value === value) return;
|
|
47
|
+
onValueChange(option.value);
|
|
48
|
+
},
|
|
49
|
+
[options, value, onValueChange],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Arrows wrap and select per the radio pattern; Space/Enter select the
|
|
53
|
+
// focused segment. Focus follows selection so the new segment stays reachable.
|
|
54
|
+
const handleKeyDown = useCallback(
|
|
55
|
+
(event: { key: string; preventDefault?: () => void }, index: number) => {
|
|
56
|
+
if (disabled) return;
|
|
57
|
+
const last = options.length - 1;
|
|
58
|
+
let next = index;
|
|
59
|
+
switch (event.key) {
|
|
60
|
+
case "ArrowRight":
|
|
61
|
+
case "ArrowDown":
|
|
62
|
+
next = index === last ? 0 : index + 1;
|
|
63
|
+
break;
|
|
64
|
+
case "ArrowLeft":
|
|
65
|
+
case "ArrowUp":
|
|
66
|
+
next = index === 0 ? last : index - 1;
|
|
67
|
+
break;
|
|
68
|
+
case "Enter":
|
|
69
|
+
case " ":
|
|
70
|
+
event.preventDefault?.();
|
|
71
|
+
select(index);
|
|
72
|
+
return;
|
|
73
|
+
default:
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
event.preventDefault?.();
|
|
77
|
+
onValueChange(options[next].value);
|
|
78
|
+
segmentRefs.current[next]?.focus();
|
|
79
|
+
},
|
|
80
|
+
[disabled, options, onValueChange, select],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Roving tabindex: the selected segment is the tab stop. When `value` matches
|
|
84
|
+
// no option, the first segment is the fallback so the group stays reachable.
|
|
85
|
+
const selectedIndex = options.findIndex((option) => option.value === value);
|
|
86
|
+
const tabStopIndex = selectedIndex === -1 ? 0 : selectedIndex;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<View
|
|
90
|
+
style={[styles.track, disabled && styles.trackDisabled, style]}
|
|
91
|
+
accessibilityRole="radiogroup"
|
|
92
|
+
aria-label={accessibilityLabel}
|
|
93
|
+
aria-disabled={disabled}
|
|
94
|
+
>
|
|
95
|
+
{options.map((option, index) => (
|
|
96
|
+
<Segment
|
|
97
|
+
ref={(node: View | null) => {
|
|
98
|
+
segmentRefs.current[index] = node;
|
|
99
|
+
}}
|
|
100
|
+
key={option.value}
|
|
101
|
+
option={option}
|
|
102
|
+
selected={option.value === value}
|
|
103
|
+
isTabStop={index === tabStopIndex}
|
|
104
|
+
disabled={disabled}
|
|
105
|
+
onPress={() => select(index)}
|
|
106
|
+
onKeyDown={(event) => handleKeyDown(event, index)}
|
|
107
|
+
/>
|
|
108
|
+
))}
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface SegmentProps<T extends string> {
|
|
114
|
+
ref: (node: View | null) => void;
|
|
115
|
+
option: SegmentOption<T>;
|
|
116
|
+
selected: boolean;
|
|
117
|
+
isTabStop: boolean;
|
|
118
|
+
disabled: boolean;
|
|
119
|
+
onPress: () => void;
|
|
120
|
+
onKeyDown: (event: { key: string; preventDefault?: () => void }) => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function Segment<T extends string>(props: SegmentProps<T>) {
|
|
124
|
+
const { ref, option, selected, isTabStop, disabled, onPress, onKeyDown } = props;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<PressableHighlight
|
|
128
|
+
ref={ref}
|
|
129
|
+
// Own the per-state background: the track is zinc-100, so an unselected
|
|
130
|
+
// segment hovering must lift TOWARD the selected white (a half-white wash),
|
|
131
|
+
// not to PressableHighlight's default zinc-100 (invisible on the track).
|
|
132
|
+
style={(state) => {
|
|
133
|
+
const hovered = (state as { hovered?: boolean }).hovered;
|
|
134
|
+
return [
|
|
135
|
+
styles.segment,
|
|
136
|
+
selected
|
|
137
|
+
? styles.segmentSelected
|
|
138
|
+
: {
|
|
139
|
+
backgroundColor: state.pressed
|
|
140
|
+
? colors.white
|
|
141
|
+
: hovered
|
|
142
|
+
? withAlpha(colors.white, 0.5)
|
|
143
|
+
: "transparent",
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
}}
|
|
147
|
+
onPress={disabled ? undefined : onPress}
|
|
148
|
+
onKeyDown={onKeyDown}
|
|
149
|
+
testID={option.testID}
|
|
150
|
+
accessibilityRole="radio"
|
|
151
|
+
accessibilityLabel={option.label}
|
|
152
|
+
aria-checked={selected}
|
|
153
|
+
aria-disabled={disabled}
|
|
154
|
+
// Roving tabindex: only the selected segment (or the fallback) is a tab
|
|
155
|
+
// stop; the rest are reached with arrow keys.
|
|
156
|
+
focusable={isTabStop && !disabled}
|
|
157
|
+
>
|
|
158
|
+
{(state) => {
|
|
159
|
+
const hovered = (state as { hovered?: boolean }).hovered;
|
|
160
|
+
return (
|
|
161
|
+
<Text
|
|
162
|
+
size="sm"
|
|
163
|
+
weight={selected ? "medium" : "regular"}
|
|
164
|
+
color={selected || hovered ? "zinc-900" : "zinc-700"}
|
|
165
|
+
numberOfLines={1}
|
|
166
|
+
userSelect="none"
|
|
167
|
+
>
|
|
168
|
+
{option.label}
|
|
169
|
+
</Text>
|
|
170
|
+
);
|
|
171
|
+
}}
|
|
172
|
+
</PressableHighlight>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const styles = StyleSheet.create({
|
|
177
|
+
track: {
|
|
178
|
+
flexDirection: "row",
|
|
179
|
+
alignSelf: "flex-start",
|
|
180
|
+
gap: 2,
|
|
181
|
+
padding: 3,
|
|
182
|
+
borderRadius: 10,
|
|
183
|
+
backgroundColor: colors.zinc["100"],
|
|
184
|
+
},
|
|
185
|
+
trackDisabled: {
|
|
186
|
+
opacity: 0.55,
|
|
187
|
+
},
|
|
188
|
+
segment: {
|
|
189
|
+
// Grow to share a stretched track equally, but keep an `auto` basis so a
|
|
190
|
+
// hug-content track (the common toolbar case) sizes each segment to its
|
|
191
|
+
// label instead of collapsing it (flex-basis 0 → truncated text).
|
|
192
|
+
flexGrow: 1,
|
|
193
|
+
flexBasis: "auto",
|
|
194
|
+
minHeight: 30,
|
|
195
|
+
paddingVertical: 6,
|
|
196
|
+
paddingHorizontal: 14,
|
|
197
|
+
borderRadius: 7,
|
|
198
|
+
alignItems: "center",
|
|
199
|
+
justifyContent: "center",
|
|
200
|
+
},
|
|
201
|
+
segmentSelected: {
|
|
202
|
+
backgroundColor: colors.white,
|
|
203
|
+
boxShadow: colors.border_shadow,
|
|
204
|
+
},
|
|
205
|
+
});
|