@ecohouse/ui 0.1.34 → 0.1.36
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 +1 -0
- package/dist/index.cjs +903 -489
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +169 -129
- package/dist/index.d.ts +169 -129
- package/dist/index.js +901 -491
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AccordionItem/AccordionItem.tsx +14 -11
- package/src/components/AccountHeader/AccountHeader.tsx +16 -0
- package/src/components/DatePicker/DatePicker.tsx +47 -14
- package/src/components/Input/Input.tsx +54 -21
- package/src/components/LanguageSwitcher/LanguageSwitcher.stories.tsx +11 -0
- package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +103 -19
- package/src/components/LanguageSwitcher/index.ts +5 -1
- package/src/components/SegmentedToggle/SegmentedToggle.stories.tsx +9 -0
- package/src/components/SegmentedToggle/SegmentedToggle.tsx +19 -13
- package/src/components/Select/Select.tsx +90 -5
- package/src/components/Textarea/Textarea.tsx +29 -4
- package/src/components/index.ts +5 -1
- package/src/icons/Globe/GlobeIcon.tsx +2 -2
- package/src/icons/Globe/GlobeIcon.web.tsx +2 -2
- package/src/icons/Globe/globePath.ts +2 -2
- package/src/icons/Key/keyPath.ts +1 -1
- package/src/icons/MoreHorizontal/MoreHorizontalIcon.tsx +26 -0
- package/src/icons/MoreHorizontal/MoreHorizontalIcon.web.tsx +25 -0
- package/src/icons/MoreHorizontal/index.ts +1 -0
- package/src/icons/MoreHorizontal/moreHorizontalPath.ts +2 -0
- package/src/icons/TableView/TableViewIcon.tsx +20 -0
- package/src/icons/TableView/TableViewIcon.web.tsx +26 -0
- package/src/icons/TableView/index.ts +1 -0
- package/src/icons/TableView/tableViewPath.ts +2 -0
- package/src/icons/index.ts +2 -0
- package/src/icons/registry.ts +6 -0
- package/src/index.ts +7 -0
- package/src/primitives/IconStatusBadge/IconStatusBadge.stories.tsx +21 -0
- package/src/primitives/IconStatusBadge/IconStatusBadge.tsx +76 -0
- package/src/primitives/IconStatusBadge/index.ts +2 -0
- package/src/primitives/InfoCardV2/InfoCardV2.stories.tsx +64 -0
- package/src/primitives/InfoCardV2/InfoCardV2.tsx +116 -0
- package/src/primitives/InfoCardV2/index.ts +2 -0
- package/src/primitives/index.ts +7 -0
- package/src/theme/typography.ts +3 -3
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
Animated,
|
|
12
12
|
Easing,
|
|
13
|
+
Platform,
|
|
13
14
|
Pressable,
|
|
14
15
|
StyleSheet,
|
|
15
16
|
Text,
|
|
@@ -119,9 +120,13 @@ export const AccordionItem = forwardRef<ComponentRef<typeof View>, AccordionItem
|
|
|
119
120
|
const bodyTextStyle = [styles.body, { fontSize: bodyFontSize, lineHeight: bodyLineHeight }];
|
|
120
121
|
|
|
121
122
|
return (
|
|
122
|
-
<
|
|
123
|
+
<Pressable
|
|
123
124
|
{...rest}
|
|
124
125
|
ref={setContainerRef}
|
|
126
|
+
accessibilityRole="button"
|
|
127
|
+
accessibilityState={{ expanded: open }}
|
|
128
|
+
accessibilityLabel={accessibilityLabel ?? title}
|
|
129
|
+
onPress={toggle}
|
|
125
130
|
style={[
|
|
126
131
|
styles.root,
|
|
127
132
|
{
|
|
@@ -129,16 +134,11 @@ export const AccordionItem = forwardRef<ComponentRef<typeof View>, AccordionItem
|
|
|
129
134
|
paddingRight: paddingX,
|
|
130
135
|
paddingLeft: paddingX,
|
|
131
136
|
},
|
|
137
|
+
Platform.OS === "web" ? styles.rootWeb : null,
|
|
132
138
|
style,
|
|
133
139
|
]}
|
|
134
140
|
>
|
|
135
|
-
<
|
|
136
|
-
accessibilityRole="button"
|
|
137
|
-
accessibilityState={{ expanded: open }}
|
|
138
|
-
accessibilityLabel={accessibilityLabel ?? title}
|
|
139
|
-
onPress={toggle}
|
|
140
|
-
style={styles.header}
|
|
141
|
-
>
|
|
141
|
+
<View style={styles.header}>
|
|
142
142
|
<Text style={styles.title}>{title}</Text>
|
|
143
143
|
<Animated.View style={[styles.chevron, { transform: [{ rotate: chevronRotate }] }]}>
|
|
144
144
|
<ChevronDownIcon
|
|
@@ -147,7 +147,7 @@ export const AccordionItem = forwardRef<ComponentRef<typeof View>, AccordionItem
|
|
|
147
147
|
strokeWidth={2}
|
|
148
148
|
/>
|
|
149
149
|
</Animated.View>
|
|
150
|
-
</
|
|
150
|
+
</View>
|
|
151
151
|
|
|
152
152
|
<View
|
|
153
153
|
style={[styles.measureHidden, { left: paddingX, right: paddingX }]}
|
|
@@ -157,12 +157,12 @@ export const AccordionItem = forwardRef<ComponentRef<typeof View>, AccordionItem
|
|
|
157
157
|
<Text style={bodyTextStyle}>{children}</Text>
|
|
158
158
|
</View>
|
|
159
159
|
|
|
160
|
-
<Animated.View style={[styles.bodyClip, { height: bodyHeight }]}>
|
|
160
|
+
<Animated.View style={[styles.bodyClip, { height: bodyHeight }]} pointerEvents="none">
|
|
161
161
|
<View style={styles.bodyInner}>
|
|
162
162
|
<Text style={bodyTextStyle}>{children}</Text>
|
|
163
163
|
</View>
|
|
164
164
|
</Animated.View>
|
|
165
|
-
</
|
|
165
|
+
</Pressable>
|
|
166
166
|
);
|
|
167
167
|
},
|
|
168
168
|
);
|
|
@@ -176,6 +176,9 @@ const styles = StyleSheet.create({
|
|
|
176
176
|
opacity: 1,
|
|
177
177
|
overflow: "hidden",
|
|
178
178
|
},
|
|
179
|
+
rootWeb: {
|
|
180
|
+
cursor: "pointer" as unknown as ViewStyle["cursor"],
|
|
181
|
+
},
|
|
179
182
|
header: {
|
|
180
183
|
flexDirection: "row",
|
|
181
184
|
alignItems: "center",
|
|
@@ -24,6 +24,8 @@ export interface AccountHeaderProps extends Omit<ViewProps, "style" | "children"
|
|
|
24
24
|
/** When set, shows the 28×28 menu control (mobile account shell). Ignored if `onBackPress` is set. */
|
|
25
25
|
onMenuPress?: () => void;
|
|
26
26
|
onNotificationsPress?: () => void;
|
|
27
|
+
/** Optional control rendered to the left of notifications (e.g. language switcher). */
|
|
28
|
+
languageSlot?: ReactNode;
|
|
27
29
|
/** Optional status chip (or other accessory) rendered after the title with a 12px gap. */
|
|
28
30
|
badge?: ReactNode;
|
|
29
31
|
/** Accessible name for the back control. Defaults to `"Back"`. */
|
|
@@ -65,6 +67,7 @@ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeader
|
|
|
65
67
|
onBackPress,
|
|
66
68
|
onMenuPress,
|
|
67
69
|
onNotificationsPress,
|
|
70
|
+
languageSlot,
|
|
68
71
|
badge,
|
|
69
72
|
backAccessibilityLabel = "Back",
|
|
70
73
|
menuAccessibilityLabel = "Open menu",
|
|
@@ -141,6 +144,7 @@ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeader
|
|
|
141
144
|
</View>
|
|
142
145
|
|
|
143
146
|
<View style={styles.end}>
|
|
147
|
+
{languageSlot != null ? <View style={styles.languageSlot}>{languageSlot}</View> : null}
|
|
144
148
|
<Pressable
|
|
145
149
|
accessibilityRole="button"
|
|
146
150
|
accessibilityLabel={notificationsAccessibilityLabel}
|
|
@@ -171,6 +175,8 @@ const styles = StyleSheet.create({
|
|
|
171
175
|
backgroundColor: colors.dark,
|
|
172
176
|
borderBottomWidth: 1,
|
|
173
177
|
borderBottomColor: colors.grey700,
|
|
178
|
+
zIndex: 40,
|
|
179
|
+
position: "relative",
|
|
174
180
|
},
|
|
175
181
|
start: {
|
|
176
182
|
flex: 1,
|
|
@@ -194,6 +200,16 @@ const styles = StyleSheet.create({
|
|
|
194
200
|
end: {
|
|
195
201
|
flexShrink: 0,
|
|
196
202
|
marginLeft: RIGHT_GAP,
|
|
203
|
+
flexDirection: "row",
|
|
204
|
+
alignItems: "center",
|
|
205
|
+
gap: 8,
|
|
206
|
+
zIndex: 50,
|
|
207
|
+
position: "relative",
|
|
208
|
+
},
|
|
209
|
+
languageSlot: {
|
|
210
|
+
flexShrink: 0,
|
|
211
|
+
zIndex: 50,
|
|
212
|
+
position: "relative",
|
|
197
213
|
},
|
|
198
214
|
iconButton: {
|
|
199
215
|
width: ICON_BUTTON_SIZE,
|
|
@@ -121,14 +121,22 @@ export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps;
|
|
|
121
121
|
|
|
122
122
|
const DEFAULT_WIDTH = 320;
|
|
123
123
|
const DEFAULT_MENU_HEIGHT = 388;
|
|
124
|
+
const MENU_CONTENT_WIDTH = 296;
|
|
124
125
|
const RANGE_TRIGGER_WIDTH = 382;
|
|
125
126
|
const TRIGGER_HEIGHT = 44;
|
|
126
127
|
const TRIGGER_RADIUS = 60;
|
|
127
128
|
const MENU_RADIUS = 20;
|
|
129
|
+
const MENU_PADDING = 12;
|
|
128
130
|
const ICON_SIZE = 20;
|
|
129
131
|
const NAV_ICON_SIZE = 20;
|
|
132
|
+
const HEADER_HEIGHT = 36;
|
|
133
|
+
const WEEKDAY_ROW_HEIGHT = 44;
|
|
134
|
+
const WEEKDAY_ROW_GAP = 8;
|
|
135
|
+
const WEEK_ROW_GAP = 16;
|
|
136
|
+
const DAY_CELL_WIDTH = 42.28571701049805;
|
|
130
137
|
const DAY_CELL_HEIGHT = 44;
|
|
131
|
-
const
|
|
138
|
+
const DAY_CELL_RADIUS = 67;
|
|
139
|
+
const DAY_CELL_PADDING = 12;
|
|
132
140
|
|
|
133
141
|
function RangeTriggerField({
|
|
134
142
|
label,
|
|
@@ -508,7 +516,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
|
|
|
508
516
|
<View>
|
|
509
517
|
<View style={styles.weekdayRow}>
|
|
510
518
|
{weekdayLabels.map((weekdayLabel, index) => (
|
|
511
|
-
<View key={`${weekdayLabel}-${index}`} style={styles.
|
|
519
|
+
<View key={`${weekdayLabel}-${index}`} style={styles.weekdayCell}>
|
|
512
520
|
<Text style={styles.weekdayText}>{weekdayLabel}</Text>
|
|
513
521
|
</View>
|
|
514
522
|
))}
|
|
@@ -756,10 +764,11 @@ const styles = StyleSheet.create({
|
|
|
756
764
|
marginTop: 8,
|
|
757
765
|
zIndex: 10,
|
|
758
766
|
borderRadius: MENU_RADIUS,
|
|
759
|
-
padding:
|
|
767
|
+
padding: MENU_PADDING,
|
|
760
768
|
gap: 0,
|
|
761
769
|
opacity: 1,
|
|
762
770
|
backgroundColor: colors.grey700,
|
|
771
|
+
overflow: "hidden",
|
|
763
772
|
...Platform.select({
|
|
764
773
|
web: {
|
|
765
774
|
boxShadow: `0 8px 24px ${colors.black}73`,
|
|
@@ -788,14 +797,17 @@ const styles = StyleSheet.create({
|
|
|
788
797
|
width: RANGE_TRIGGER_WIDTH,
|
|
789
798
|
},
|
|
790
799
|
calendarHeader: {
|
|
791
|
-
height:
|
|
800
|
+
height: HEADER_HEIGHT,
|
|
801
|
+
paddingTop: 8,
|
|
802
|
+
paddingBottom: 8,
|
|
792
803
|
flexDirection: "row",
|
|
793
804
|
alignItems: "center",
|
|
794
805
|
justifyContent: "space-between",
|
|
806
|
+
opacity: 1,
|
|
795
807
|
},
|
|
796
808
|
navButton: {
|
|
797
|
-
width:
|
|
798
|
-
height:
|
|
809
|
+
width: HEADER_HEIGHT,
|
|
810
|
+
height: HEADER_HEIGHT,
|
|
799
811
|
alignItems: "center",
|
|
800
812
|
justifyContent: "center",
|
|
801
813
|
},
|
|
@@ -808,7 +820,21 @@ const styles = StyleSheet.create({
|
|
|
808
820
|
textAlign: "center",
|
|
809
821
|
},
|
|
810
822
|
weekdayRow: {
|
|
823
|
+
width: MENU_CONTENT_WIDTH,
|
|
824
|
+
height: WEEKDAY_ROW_HEIGHT,
|
|
825
|
+
marginTop: WEEKDAY_ROW_GAP,
|
|
811
826
|
flexDirection: "row",
|
|
827
|
+
alignItems: "center",
|
|
828
|
+
opacity: 1,
|
|
829
|
+
},
|
|
830
|
+
weekdayCell: {
|
|
831
|
+
width: DAY_CELL_WIDTH,
|
|
832
|
+
height: WEEKDAY_ROW_HEIGHT,
|
|
833
|
+
flexGrow: 1,
|
|
834
|
+
flexShrink: 1,
|
|
835
|
+
flexBasis: 0,
|
|
836
|
+
alignItems: "center",
|
|
837
|
+
justifyContent: "center",
|
|
812
838
|
},
|
|
813
839
|
weekdayText: {
|
|
814
840
|
...datePickerTypography.weekday,
|
|
@@ -817,29 +843,36 @@ const styles = StyleSheet.create({
|
|
|
817
843
|
textTransform: "uppercase",
|
|
818
844
|
},
|
|
819
845
|
weekRow: {
|
|
846
|
+
width: MENU_CONTENT_WIDTH,
|
|
820
847
|
flexDirection: "row",
|
|
821
848
|
position: "relative",
|
|
822
849
|
},
|
|
823
850
|
weekRowSpaced: {
|
|
824
|
-
marginTop:
|
|
851
|
+
marginTop: WEEK_ROW_GAP,
|
|
825
852
|
},
|
|
826
853
|
rangeHighlight: {
|
|
827
854
|
position: "absolute",
|
|
828
|
-
top:
|
|
829
|
-
bottom:
|
|
830
|
-
borderRadius:
|
|
855
|
+
top: 0,
|
|
856
|
+
bottom: 0,
|
|
857
|
+
borderRadius: DAY_CELL_RADIUS,
|
|
831
858
|
backgroundColor: colors.primaryHover,
|
|
832
859
|
},
|
|
833
860
|
dayCellWrap: {
|
|
834
|
-
|
|
861
|
+
width: DAY_CELL_WIDTH,
|
|
835
862
|
height: DAY_CELL_HEIGHT,
|
|
863
|
+
flexGrow: 1,
|
|
864
|
+
flexShrink: 1,
|
|
865
|
+
flexBasis: 0,
|
|
836
866
|
alignItems: "center",
|
|
837
867
|
justifyContent: "center",
|
|
868
|
+
opacity: 1,
|
|
838
869
|
},
|
|
839
870
|
dayCircle: {
|
|
840
|
-
width:
|
|
841
|
-
height:
|
|
842
|
-
borderRadius:
|
|
871
|
+
width: "100%",
|
|
872
|
+
height: "100%",
|
|
873
|
+
borderRadius: DAY_CELL_RADIUS,
|
|
874
|
+
padding: DAY_CELL_PADDING,
|
|
875
|
+
gap: 10,
|
|
843
876
|
alignItems: "center",
|
|
844
877
|
justifyContent: "center",
|
|
845
878
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { forwardRef, useMemo, useRef, useState, type ComponentRef } from "react";
|
|
1
|
+
import { forwardRef, useId, useMemo, useRef, useState, type ComponentRef } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Platform,
|
|
4
|
+
Pressable,
|
|
4
5
|
StyleSheet,
|
|
5
6
|
Text,
|
|
6
7
|
TextInput,
|
|
@@ -183,6 +184,8 @@ export const Input = forwardRef<ComponentRef<typeof TextInput>, InputProps>(func
|
|
|
183
184
|
const [focused, setFocused] = useState(false);
|
|
184
185
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
|
|
185
186
|
const metrics = sizeConfig[size];
|
|
187
|
+
const reactId = useId();
|
|
188
|
+
const inputDomId = `eh-input-${reactId.replace(/:/g, "")}`;
|
|
186
189
|
|
|
187
190
|
const isControlled = typeof value === "string";
|
|
188
191
|
const currentValue = isControlled ? value : uncontrolledValue;
|
|
@@ -204,31 +207,44 @@ export const Input = forwardRef<ComponentRef<typeof TextInput>, InputProps>(func
|
|
|
204
207
|
color: chrome.rightIconColor,
|
|
205
208
|
});
|
|
206
209
|
|
|
210
|
+
const focusInput = () => {
|
|
211
|
+
if (disabled) return;
|
|
212
|
+
inputRef.current?.focus();
|
|
213
|
+
};
|
|
214
|
+
|
|
207
215
|
useApplyWebClassName(rootRef, className);
|
|
208
216
|
useApplyWebClassName(inputRef, inputClassName);
|
|
209
217
|
|
|
218
|
+
const fieldChromeStyle = [
|
|
219
|
+
styles.field,
|
|
220
|
+
{
|
|
221
|
+
height: metrics.height,
|
|
222
|
+
borderRadius: metrics.borderRadius,
|
|
223
|
+
paddingTop: metrics.paddingVertical,
|
|
224
|
+
paddingBottom: metrics.paddingVertical,
|
|
225
|
+
paddingLeft: metrics.paddingHorizontal,
|
|
226
|
+
paddingRight: metrics.paddingHorizontal,
|
|
227
|
+
gap: metrics.gap,
|
|
228
|
+
borderColor: chrome.borderColor,
|
|
229
|
+
backgroundColor: chrome.backgroundColor,
|
|
230
|
+
},
|
|
231
|
+
Platform.OS === "web" ? styles.fieldWeb : null,
|
|
232
|
+
fieldStyle,
|
|
233
|
+
];
|
|
234
|
+
|
|
210
235
|
return (
|
|
211
236
|
<View ref={rootRef} style={[styles.root, disabled && styles.disabled, style]}>
|
|
212
237
|
{showLabel && label ? (
|
|
213
|
-
<
|
|
238
|
+
<Pressable disabled={disabled} onPress={focusInput} style={styles.labelPressable}>
|
|
239
|
+
<Text style={[styles.label, { color: chrome.labelColor }]}>{label}</Text>
|
|
240
|
+
</Pressable>
|
|
214
241
|
) : null}
|
|
215
242
|
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
borderRadius: metrics.borderRadius,
|
|
222
|
-
paddingTop: metrics.paddingVertical,
|
|
223
|
-
paddingBottom: metrics.paddingVertical,
|
|
224
|
-
paddingLeft: metrics.paddingHorizontal,
|
|
225
|
-
paddingRight: metrics.paddingHorizontal,
|
|
226
|
-
gap: metrics.gap,
|
|
227
|
-
borderColor: chrome.borderColor,
|
|
228
|
-
backgroundColor: chrome.backgroundColor,
|
|
229
|
-
},
|
|
230
|
-
fieldStyle,
|
|
231
|
-
]}
|
|
243
|
+
<Pressable
|
|
244
|
+
accessibilityRole="none"
|
|
245
|
+
disabled={disabled}
|
|
246
|
+
onPress={focusInput}
|
|
247
|
+
style={fieldChromeStyle}
|
|
232
248
|
>
|
|
233
249
|
{hasLeftIcon ? (
|
|
234
250
|
<View style={[styles.iconSlot, { width: metrics.iconSize, height: metrics.iconSize }]}>
|
|
@@ -239,6 +255,7 @@ export const Input = forwardRef<ComponentRef<typeof TextInput>, InputProps>(func
|
|
|
239
255
|
<TextInput
|
|
240
256
|
ref={setInputRef}
|
|
241
257
|
{...textInputProps}
|
|
258
|
+
nativeID={inputDomId}
|
|
242
259
|
value={isControlled ? value : undefined}
|
|
243
260
|
defaultValue={isControlled ? undefined : defaultValue}
|
|
244
261
|
editable={!disabled}
|
|
@@ -281,12 +298,17 @@ export const Input = forwardRef<ComponentRef<typeof TextInput>, InputProps>(func
|
|
|
281
298
|
{rightIconNode}
|
|
282
299
|
</TouchableOpacity>
|
|
283
300
|
) : (
|
|
284
|
-
<
|
|
301
|
+
<Pressable
|
|
302
|
+
accessibilityRole="none"
|
|
303
|
+
disabled={disabled}
|
|
304
|
+
onPress={focusInput}
|
|
305
|
+
style={[styles.iconSlot, { width: metrics.iconSize, height: metrics.iconSize }]}
|
|
306
|
+
>
|
|
285
307
|
{rightIconNode}
|
|
286
|
-
</
|
|
308
|
+
</Pressable>
|
|
287
309
|
)
|
|
288
310
|
) : null}
|
|
289
|
-
</
|
|
311
|
+
</Pressable>
|
|
290
312
|
|
|
291
313
|
{showHint && hint ? (
|
|
292
314
|
<Text style={[styles.hint, { color: chrome.hintColor }]}>{hint}</Text>
|
|
@@ -304,6 +326,9 @@ const styles = StyleSheet.create({
|
|
|
304
326
|
disabled: {
|
|
305
327
|
opacity: 0.6,
|
|
306
328
|
},
|
|
329
|
+
labelPressable: {
|
|
330
|
+
alignSelf: "flex-start",
|
|
331
|
+
},
|
|
307
332
|
label: {
|
|
308
333
|
...inputTypography.label,
|
|
309
334
|
},
|
|
@@ -312,13 +337,21 @@ const styles = StyleSheet.create({
|
|
|
312
337
|
flexDirection: "row",
|
|
313
338
|
alignItems: "center",
|
|
314
339
|
},
|
|
340
|
+
fieldWeb: {
|
|
341
|
+
// Web-only cursor for the clickable field chrome.
|
|
342
|
+
cursor: "text" as unknown as ViewStyle["cursor"],
|
|
343
|
+
},
|
|
315
344
|
input: {
|
|
316
345
|
flex: 1,
|
|
346
|
+
alignSelf: "stretch",
|
|
317
347
|
paddingVertical: 0,
|
|
318
348
|
margin: 0,
|
|
319
349
|
},
|
|
320
350
|
inputWeb: {
|
|
321
351
|
outlineStyle: "none",
|
|
352
|
+
height: "100%",
|
|
353
|
+
minWidth: 0,
|
|
354
|
+
cursor: "text" as unknown as TextStyle["cursor"],
|
|
322
355
|
} as TextStyle,
|
|
323
356
|
inputAndroid: {
|
|
324
357
|
includeFontPadding: false,
|
|
@@ -50,3 +50,14 @@ export const CustomLanguages: Story = {
|
|
|
50
50
|
</View>
|
|
51
51
|
),
|
|
52
52
|
};
|
|
53
|
+
|
|
54
|
+
export const IconVariant: Story = {
|
|
55
|
+
args: { variant: "icon", defaultOpen: true },
|
|
56
|
+
decorators: [
|
|
57
|
+
(Story) => (
|
|
58
|
+
<View style={{ alignItems: "flex-end", padding: 24, minHeight: 200 }}>
|
|
59
|
+
<Story />
|
|
60
|
+
</View>
|
|
61
|
+
),
|
|
62
|
+
],
|
|
63
|
+
};
|
|
@@ -30,6 +30,8 @@ export interface LanguageOption {
|
|
|
30
30
|
label: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export type LanguageSwitcherVariant = "default" | "icon";
|
|
34
|
+
|
|
33
35
|
export interface LanguageSwitcherProps extends Omit<ViewProps, "style" | "children"> {
|
|
34
36
|
options?: readonly LanguageOption[];
|
|
35
37
|
value?: string;
|
|
@@ -39,6 +41,11 @@ export interface LanguageSwitcherProps extends Omit<ViewProps, "style" | "childr
|
|
|
39
41
|
defaultOpen?: boolean;
|
|
40
42
|
onOpenChange?: (open: boolean) => void;
|
|
41
43
|
disabled?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* `default` — globe + label + chevron (marketing header).
|
|
46
|
+
* `icon` — icon-only control matching account header action buttons.
|
|
47
|
+
*/
|
|
48
|
+
variant?: LanguageSwitcherVariant;
|
|
42
49
|
accessibilityLabel?: string;
|
|
43
50
|
style?: StyleProp<ViewStyle>;
|
|
44
51
|
className?: string;
|
|
@@ -54,6 +61,13 @@ export const defaultLanguageOptions = [
|
|
|
54
61
|
const CONTROL_WIDTH = 90;
|
|
55
62
|
const CONTROL_HEIGHT = 35;
|
|
56
63
|
const ICON_SIZE = 16;
|
|
64
|
+
const ICON_BUTTON_SIZE = 32;
|
|
65
|
+
const ICON_MENU_WIDTH = 67;
|
|
66
|
+
const ICON_MENU_HEIGHT = 129;
|
|
67
|
+
const ICON_MENU_TOP = 39;
|
|
68
|
+
const ITEM_HEIGHT = 35;
|
|
69
|
+
/** Active option wash from design (`#E38E5E` at 7%). */
|
|
70
|
+
const ACTIVE_ITEM_BACKGROUND = "#E38E5E12";
|
|
57
71
|
|
|
58
72
|
export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSwitcherProps>(
|
|
59
73
|
function LanguageSwitcher(
|
|
@@ -66,6 +80,7 @@ export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSw
|
|
|
66
80
|
defaultOpen = false,
|
|
67
81
|
onOpenChange,
|
|
68
82
|
disabled = false,
|
|
83
|
+
variant = "default",
|
|
69
84
|
accessibilityLabel = "Language",
|
|
70
85
|
style,
|
|
71
86
|
className,
|
|
@@ -88,6 +103,7 @@ export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSw
|
|
|
88
103
|
const isOpen = isOpenControlled ? openProp : uncontrolledOpen;
|
|
89
104
|
const selectedOption =
|
|
90
105
|
options.find((option) => option.value === selectedValue) ?? options[0] ?? null;
|
|
106
|
+
const isIconVariant = variant === "icon";
|
|
91
107
|
|
|
92
108
|
useApplyWebClassName(wrapperRef, className?.trim() || undefined);
|
|
93
109
|
|
|
@@ -147,7 +163,14 @@ export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSw
|
|
|
147
163
|
<View
|
|
148
164
|
{...rest}
|
|
149
165
|
ref={setWrapperRef}
|
|
150
|
-
style={[
|
|
166
|
+
style={[
|
|
167
|
+
styles.wrapper,
|
|
168
|
+
isIconVariant ? styles.wrapperIcon : styles.wrapperDefault,
|
|
169
|
+
isOpen && styles.wrapperOpen,
|
|
170
|
+
isOpen && isIconVariant && styles.wrapperOpenIcon,
|
|
171
|
+
disabled && styles.disabled,
|
|
172
|
+
style,
|
|
173
|
+
]}
|
|
151
174
|
>
|
|
152
175
|
<Pressable
|
|
153
176
|
ref={triggerRef}
|
|
@@ -160,21 +183,28 @@ export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSw
|
|
|
160
183
|
onHoverIn={() => setTriggerHovered(true)}
|
|
161
184
|
onHoverOut={() => setTriggerHovered(false)}
|
|
162
185
|
style={({ pressed }) => [
|
|
163
|
-
styles.trigger,
|
|
164
|
-
(triggerHovered || pressed || isOpen) && styles.triggerActive,
|
|
186
|
+
isIconVariant ? styles.triggerIcon : styles.trigger,
|
|
187
|
+
!isIconVariant && (triggerHovered || pressed || isOpen) && styles.triggerActive,
|
|
165
188
|
]}
|
|
166
189
|
>
|
|
167
|
-
<GlobeIcon size={ICON_SIZE} color={colors.white} strokeWidth={2} />
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
190
|
+
<GlobeIcon size={ICON_SIZE} color={colors.white} strokeWidth={isIconVariant ? 1.5 : 2} />
|
|
191
|
+
{!isIconVariant ? (
|
|
192
|
+
<>
|
|
193
|
+
<Text style={styles.triggerLabel} numberOfLines={1}>
|
|
194
|
+
{selectedOption?.label ?? ""}
|
|
195
|
+
</Text>
|
|
196
|
+
<View style={[styles.chevron, isOpen && styles.chevronOpen]}>
|
|
197
|
+
<ChevronDownIcon size={ICON_SIZE} color={colors.white} strokeWidth={2} />
|
|
198
|
+
</View>
|
|
199
|
+
</>
|
|
200
|
+
) : null}
|
|
174
201
|
</Pressable>
|
|
175
202
|
|
|
176
203
|
{isOpen && options.length > 0 ? (
|
|
177
|
-
<View
|
|
204
|
+
<View
|
|
205
|
+
style={[styles.menu, isIconVariant ? styles.menuIcon : styles.menuDefault]}
|
|
206
|
+
accessibilityRole="menu"
|
|
207
|
+
>
|
|
178
208
|
{options.map((option) => {
|
|
179
209
|
const selected = option.value === selectedValue;
|
|
180
210
|
return (
|
|
@@ -188,12 +218,21 @@ export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSw
|
|
|
188
218
|
onHoverOut={() => setHoveredValue(null)}
|
|
189
219
|
style={({ pressed }) => [
|
|
190
220
|
styles.item,
|
|
221
|
+
isIconVariant ? styles.itemIcon : null,
|
|
191
222
|
selected
|
|
192
|
-
?
|
|
223
|
+
? isIconVariant
|
|
224
|
+
? styles.itemSelectedIcon
|
|
225
|
+
: styles.itemSelected
|
|
193
226
|
: (hoveredValue === option.value || pressed) && styles.itemHovered,
|
|
194
227
|
]}
|
|
195
228
|
>
|
|
196
|
-
<Text
|
|
229
|
+
<Text
|
|
230
|
+
style={[
|
|
231
|
+
styles.itemLabel,
|
|
232
|
+
selected &&
|
|
233
|
+
(isIconVariant ? styles.itemLabelSelectedIcon : styles.itemLabelSelected),
|
|
234
|
+
]}
|
|
235
|
+
>
|
|
197
236
|
{option.label}
|
|
198
237
|
</Text>
|
|
199
238
|
</Pressable>
|
|
@@ -209,11 +248,19 @@ export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSw
|
|
|
209
248
|
const styles = StyleSheet.create({
|
|
210
249
|
wrapper: {
|
|
211
250
|
position: "relative",
|
|
212
|
-
width: CONTROL_WIDTH,
|
|
213
251
|
alignSelf: "flex-start",
|
|
214
252
|
},
|
|
253
|
+
wrapperDefault: {
|
|
254
|
+
width: CONTROL_WIDTH,
|
|
255
|
+
},
|
|
256
|
+
wrapperIcon: {
|
|
257
|
+
width: ICON_BUTTON_SIZE,
|
|
258
|
+
},
|
|
215
259
|
wrapperOpen: {
|
|
216
|
-
zIndex:
|
|
260
|
+
zIndex: 100,
|
|
261
|
+
},
|
|
262
|
+
wrapperOpenIcon: {
|
|
263
|
+
zIndex: 200,
|
|
217
264
|
},
|
|
218
265
|
disabled: {
|
|
219
266
|
opacity: 0.6,
|
|
@@ -228,6 +275,17 @@ const styles = StyleSheet.create({
|
|
|
228
275
|
borderRadius: 12,
|
|
229
276
|
backgroundColor: "transparent",
|
|
230
277
|
},
|
|
278
|
+
triggerIcon: {
|
|
279
|
+
width: ICON_BUTTON_SIZE,
|
|
280
|
+
height: ICON_BUTTON_SIZE,
|
|
281
|
+
padding: 8,
|
|
282
|
+
alignItems: "center",
|
|
283
|
+
justifyContent: "center",
|
|
284
|
+
borderRadius: 5,
|
|
285
|
+
backgroundColor: colors.grey800,
|
|
286
|
+
borderWidth: 1,
|
|
287
|
+
borderColor: colors.grey700,
|
|
288
|
+
},
|
|
231
289
|
triggerActive: {
|
|
232
290
|
backgroundColor: colors.whiteAlpha6,
|
|
233
291
|
},
|
|
@@ -250,22 +308,42 @@ const styles = StyleSheet.create({
|
|
|
250
308
|
},
|
|
251
309
|
menu: {
|
|
252
310
|
position: "absolute",
|
|
311
|
+
padding: 8,
|
|
312
|
+
borderRadius: 12,
|
|
313
|
+
overflow: "hidden",
|
|
314
|
+
},
|
|
315
|
+
menuDefault: {
|
|
253
316
|
top: CONTROL_HEIGHT + 8,
|
|
254
317
|
left: 0,
|
|
255
318
|
width: CONTROL_WIDTH,
|
|
256
|
-
padding: 8,
|
|
257
319
|
gap: 8,
|
|
258
|
-
borderRadius: 12,
|
|
259
320
|
backgroundColor: colors.whiteAlpha6,
|
|
260
|
-
|
|
321
|
+
},
|
|
322
|
+
menuIcon: {
|
|
323
|
+
top: ICON_MENU_TOP,
|
|
324
|
+
left: (ICON_BUTTON_SIZE - ICON_MENU_WIDTH) / 2,
|
|
325
|
+
right: "auto",
|
|
326
|
+
width: ICON_MENU_WIDTH,
|
|
327
|
+
height: ICON_MENU_HEIGHT,
|
|
328
|
+
gap: 4,
|
|
329
|
+
opacity: 1,
|
|
330
|
+
zIndex: 200,
|
|
331
|
+
backgroundColor: colors.grey800,
|
|
332
|
+
borderWidth: 1,
|
|
333
|
+
borderColor: colors.grey700,
|
|
261
334
|
},
|
|
262
335
|
item: {
|
|
263
|
-
height:
|
|
336
|
+
height: ITEM_HEIGHT,
|
|
264
337
|
alignItems: "center",
|
|
265
338
|
justifyContent: "center",
|
|
266
339
|
paddingVertical: 8,
|
|
267
340
|
paddingHorizontal: 12,
|
|
268
341
|
borderRadius: 8,
|
|
342
|
+
gap: 8,
|
|
343
|
+
},
|
|
344
|
+
itemIcon: {
|
|
345
|
+
width: 51,
|
|
346
|
+
alignSelf: "stretch",
|
|
269
347
|
},
|
|
270
348
|
itemHovered: {
|
|
271
349
|
backgroundColor: colors.whiteAlpha6,
|
|
@@ -273,6 +351,9 @@ const styles = StyleSheet.create({
|
|
|
273
351
|
itemSelected: {
|
|
274
352
|
backgroundColor: colors.whiteAlpha6,
|
|
275
353
|
},
|
|
354
|
+
itemSelectedIcon: {
|
|
355
|
+
backgroundColor: ACTIVE_ITEM_BACKGROUND,
|
|
356
|
+
},
|
|
276
357
|
itemLabel: {
|
|
277
358
|
...languageSwitcherTypography,
|
|
278
359
|
...Platform.select({ web: { fontFamily: `${fonts.sans}, sans-serif` } }),
|
|
@@ -281,4 +362,7 @@ const styles = StyleSheet.create({
|
|
|
281
362
|
itemLabelSelected: {
|
|
282
363
|
color: colors.primary,
|
|
283
364
|
},
|
|
365
|
+
itemLabelSelectedIcon: {
|
|
366
|
+
color: colors.primary,
|
|
367
|
+
},
|
|
284
368
|
});
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { LanguageSwitcher, defaultLanguageOptions } from "./LanguageSwitcher";
|
|
2
|
-
export type {
|
|
2
|
+
export type {
|
|
3
|
+
LanguageOption,
|
|
4
|
+
LanguageSwitcherProps,
|
|
5
|
+
LanguageSwitcherVariant,
|
|
6
|
+
} from "./LanguageSwitcher";
|
|
@@ -52,6 +52,15 @@ type Story = StoryObj<typeof meta>;
|
|
|
52
52
|
|
|
53
53
|
export const WithIcons: Story = {};
|
|
54
54
|
|
|
55
|
+
export const IconOnly: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
options: [
|
|
58
|
+
{ value: "grid", label: "Grid view", icon: LayoutGridIcon, showLabel: false },
|
|
59
|
+
{ value: "list", label: "List view", icon: ListViewIcon, showLabel: false },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
55
64
|
export const WithoutIcons: Story = {
|
|
56
65
|
args: { options: optionsWithoutIcons, defaultValue: "first" },
|
|
57
66
|
};
|