@ecohouse/ui 0.1.33 → 0.1.35
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/dist/index.cjs +30078 -656
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -7
- package/dist/index.d.ts +114 -7
- package/dist/index.js +30007 -593
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AccordionItem/AccordionItem.stories.tsx +54 -0
- package/src/components/AccordionItem/AccordionItem.tsx +223 -0
- package/src/components/AccordionItem/index.ts +2 -0
- package/src/components/AccountHeader/AccountHeader.tsx +50 -29
- package/src/components/Button/Button.stories.tsx +10 -1
- package/src/components/Button/Button.tsx +32 -3
- package/src/components/ContactInfoCard/ContactInfoCard.stories.tsx +30 -0
- package/src/components/ContactInfoCard/ContactInfoCard.tsx +141 -0
- package/src/components/ContactInfoCard/index.ts +2 -0
- package/src/components/DatePicker/DatePicker.tsx +51 -15
- 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/Modal/Modal.stories.tsx +100 -0
- package/src/components/Modal/Modal.tsx +165 -0
- package/src/components/Modal/index.ts +2 -0
- package/src/components/Select/Select.tsx +90 -5
- package/src/components/Sidebar/Sidebar.tsx +19 -3
- package/src/components/Textarea/Textarea.tsx +29 -4
- package/src/components/ToggleRow/ToggleRow.stories.tsx +46 -0
- package/src/components/ToggleRow/ToggleRow.tsx +121 -0
- package/src/components/ToggleRow/index.ts +2 -0
- package/src/components/VerificationCodeInput/VerificationCodeInput.tsx +80 -60
- package/src/components/index.ts +17 -1
- package/src/icons/CameraFilled/CameraFilledIcon.tsx +15 -0
- package/src/icons/CameraFilled/CameraFilledIcon.web.tsx +21 -0
- package/src/icons/CameraFilled/cameraFilledPath.ts +2 -0
- package/src/icons/CameraFilled/index.ts +1 -0
- 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/Headset/HeadsetIcon.tsx +26 -0
- package/src/icons/Headset/HeadsetIcon.web.tsx +25 -0
- package/src/icons/Headset/headsetPath.ts +6 -0
- package/src/icons/Headset/index.ts +1 -0
- package/src/icons/Hide/HideIcon.tsx +21 -0
- package/src/icons/Hide/HideIcon.web.tsx +27 -0
- package/src/icons/Hide/hidePath.ts +3 -0
- package/src/icons/Hide/index.ts +1 -0
- package/src/icons/Key/keyPath.ts +1 -1
- package/src/icons/MenuLines/MenuLinesIcon.tsx +20 -0
- package/src/icons/MenuLines/MenuLinesIcon.web.tsx +26 -0
- package/src/icons/MenuLines/index.ts +1 -0
- package/src/icons/MenuLines/menuLinesPath.ts +3 -0
- package/src/icons/index.ts +4 -0
- package/src/icons/registry.ts +11 -0
- package/src/index.ts +13 -0
- package/src/theme/typography.ts +3 -3
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { forwardRef, useMemo, useRef, type ComponentRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
StyleSheet,
|
|
4
|
+
Text,
|
|
5
|
+
useWindowDimensions,
|
|
6
|
+
View,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
type ViewProps,
|
|
9
|
+
type ViewStyle,
|
|
10
|
+
} from "react-native";
|
|
11
|
+
import type { IconComponent, IconProps } from "../../icons";
|
|
12
|
+
import { colors, fonts } from "../../theme";
|
|
13
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
14
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
15
|
+
|
|
16
|
+
export interface ContactInfoCardProps extends Omit<ViewProps, "style" | "children"> {
|
|
17
|
+
title: string;
|
|
18
|
+
value: string;
|
|
19
|
+
icon: IconComponent;
|
|
20
|
+
iconProps?: Omit<IconProps, "size">;
|
|
21
|
+
iconSize?: number;
|
|
22
|
+
/** Icon slot fill. Defaults to `#E38E5E0F`. */
|
|
23
|
+
iconSlotBackgroundColor?: string;
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DESKTOP_MIN_WIDTH = 1024;
|
|
29
|
+
const CARD_HEIGHT_MOBILE = 78;
|
|
30
|
+
const CARD_HEIGHT_DESKTOP = 96;
|
|
31
|
+
const CARD_PADDING_MOBILE = 16;
|
|
32
|
+
const CARD_PADDING_DESKTOP = 24;
|
|
33
|
+
const CARD_GAP_MOBILE = 16;
|
|
34
|
+
const CARD_GAP_DESKTOP = 24;
|
|
35
|
+
const ICON_SLOT = 48;
|
|
36
|
+
const ICON_SIZE = 24;
|
|
37
|
+
const ICON_SLOT_BG = "#E38E5E0F";
|
|
38
|
+
const ICON_COLOR = "#B76533";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Support contact strip — icon slot + title/value.
|
|
42
|
+
* Mobile: height 78, padding 16, gap 16, radius 20.
|
|
43
|
+
* Desktop: height 96, padding 24, gap 24, radius 20.
|
|
44
|
+
*/
|
|
45
|
+
export const ContactInfoCard = forwardRef<ComponentRef<typeof View>, ContactInfoCardProps>(
|
|
46
|
+
function ContactInfoCard(
|
|
47
|
+
{
|
|
48
|
+
title,
|
|
49
|
+
value,
|
|
50
|
+
icon: Icon,
|
|
51
|
+
iconProps,
|
|
52
|
+
iconSize = ICON_SIZE,
|
|
53
|
+
iconSlotBackgroundColor = ICON_SLOT_BG,
|
|
54
|
+
style,
|
|
55
|
+
className,
|
|
56
|
+
accessibilityLabel,
|
|
57
|
+
...rest
|
|
58
|
+
},
|
|
59
|
+
forwardedRef,
|
|
60
|
+
) {
|
|
61
|
+
const containerRef = useRef<ComponentRef<typeof View>>(null);
|
|
62
|
+
const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
|
|
63
|
+
const { width: viewportWidth } = useWindowDimensions();
|
|
64
|
+
const isDesktop = viewportWidth >= DESKTOP_MIN_WIDTH;
|
|
65
|
+
|
|
66
|
+
useApplyWebClassName(containerRef, className);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<View
|
|
70
|
+
{...rest}
|
|
71
|
+
ref={setContainerRef}
|
|
72
|
+
accessibilityRole="text"
|
|
73
|
+
accessibilityLabel={accessibilityLabel ?? `${title}. ${value}`}
|
|
74
|
+
style={[
|
|
75
|
+
styles.root,
|
|
76
|
+
{
|
|
77
|
+
height: isDesktop ? CARD_HEIGHT_DESKTOP : CARD_HEIGHT_MOBILE,
|
|
78
|
+
gap: isDesktop ? CARD_GAP_DESKTOP : CARD_GAP_MOBILE,
|
|
79
|
+
padding: isDesktop ? CARD_PADDING_DESKTOP : CARD_PADDING_MOBILE,
|
|
80
|
+
},
|
|
81
|
+
style,
|
|
82
|
+
]}
|
|
83
|
+
>
|
|
84
|
+
<View style={[styles.iconSlot, { backgroundColor: iconSlotBackgroundColor }]}>
|
|
85
|
+
<Icon
|
|
86
|
+
{...iconProps}
|
|
87
|
+
size={iconSize}
|
|
88
|
+
color={iconProps?.color ?? ICON_COLOR}
|
|
89
|
+
strokeWidth={iconProps?.strokeWidth ?? 2}
|
|
90
|
+
/>
|
|
91
|
+
</View>
|
|
92
|
+
<View style={styles.textBlock}>
|
|
93
|
+
<Text style={styles.title}>{title}</Text>
|
|
94
|
+
<Text style={styles.value}>{value}</Text>
|
|
95
|
+
</View>
|
|
96
|
+
</View>
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const styles = StyleSheet.create({
|
|
102
|
+
root: {
|
|
103
|
+
width: "100%",
|
|
104
|
+
flexDirection: "row",
|
|
105
|
+
alignItems: "center",
|
|
106
|
+
borderRadius: 20,
|
|
107
|
+
backgroundColor: colors.grey750,
|
|
108
|
+
opacity: 1,
|
|
109
|
+
},
|
|
110
|
+
iconSlot: {
|
|
111
|
+
width: ICON_SLOT,
|
|
112
|
+
height: ICON_SLOT,
|
|
113
|
+
padding: 12,
|
|
114
|
+
borderRadius: 12,
|
|
115
|
+
alignItems: "center",
|
|
116
|
+
justifyContent: "center",
|
|
117
|
+
flexShrink: 0,
|
|
118
|
+
},
|
|
119
|
+
textBlock: {
|
|
120
|
+
flexShrink: 1,
|
|
121
|
+
minWidth: 0,
|
|
122
|
+
flexDirection: "column",
|
|
123
|
+
gap: 8,
|
|
124
|
+
},
|
|
125
|
+
title: {
|
|
126
|
+
fontFamily: fonts.sans,
|
|
127
|
+
fontSize: 14,
|
|
128
|
+
fontWeight: "600",
|
|
129
|
+
lineHeight: 14,
|
|
130
|
+
letterSpacing: 0.42,
|
|
131
|
+
color: colors.white,
|
|
132
|
+
},
|
|
133
|
+
value: {
|
|
134
|
+
fontFamily: fonts.sans,
|
|
135
|
+
fontSize: 14,
|
|
136
|
+
fontWeight: "400",
|
|
137
|
+
lineHeight: 14,
|
|
138
|
+
letterSpacing: 0.42,
|
|
139
|
+
color: colors.lightGrey,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
@@ -119,15 +119,24 @@ export type DatePickerRangeProps = DatePickerBaseProps & {
|
|
|
119
119
|
|
|
120
120
|
export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps;
|
|
121
121
|
|
|
122
|
-
const DEFAULT_WIDTH =
|
|
122
|
+
const DEFAULT_WIDTH = 320;
|
|
123
|
+
const DEFAULT_MENU_HEIGHT = 388;
|
|
124
|
+
const MENU_CONTENT_WIDTH = 296;
|
|
123
125
|
const RANGE_TRIGGER_WIDTH = 382;
|
|
124
126
|
const TRIGGER_HEIGHT = 44;
|
|
125
127
|
const TRIGGER_RADIUS = 60;
|
|
126
128
|
const MENU_RADIUS = 20;
|
|
129
|
+
const MENU_PADDING = 12;
|
|
127
130
|
const ICON_SIZE = 20;
|
|
128
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;
|
|
129
137
|
const DAY_CELL_HEIGHT = 44;
|
|
130
|
-
const
|
|
138
|
+
const DAY_CELL_RADIUS = 67;
|
|
139
|
+
const DAY_CELL_PADDING = 12;
|
|
131
140
|
|
|
132
141
|
function RangeTriggerField({
|
|
133
142
|
label,
|
|
@@ -507,7 +516,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
|
|
|
507
516
|
<View>
|
|
508
517
|
<View style={styles.weekdayRow}>
|
|
509
518
|
{weekdayLabels.map((weekdayLabel, index) => (
|
|
510
|
-
<View key={`${weekdayLabel}-${index}`} style={styles.
|
|
519
|
+
<View key={`${weekdayLabel}-${index}`} style={styles.weekdayCell}>
|
|
511
520
|
<Text style={styles.weekdayText}>{weekdayLabel}</Text>
|
|
512
521
|
</View>
|
|
513
522
|
))}
|
|
@@ -751,12 +760,15 @@ const styles = StyleSheet.create({
|
|
|
751
760
|
position: "absolute",
|
|
752
761
|
top: "100%",
|
|
753
762
|
width: DEFAULT_WIDTH,
|
|
763
|
+
height: DEFAULT_MENU_HEIGHT,
|
|
754
764
|
marginTop: 8,
|
|
755
765
|
zIndex: 10,
|
|
756
766
|
borderRadius: MENU_RADIUS,
|
|
757
|
-
padding:
|
|
767
|
+
padding: MENU_PADDING,
|
|
758
768
|
gap: 0,
|
|
769
|
+
opacity: 1,
|
|
759
770
|
backgroundColor: colors.grey700,
|
|
771
|
+
overflow: "hidden",
|
|
760
772
|
...Platform.select({
|
|
761
773
|
web: {
|
|
762
774
|
boxShadow: `0 8px 24px ${colors.black}73`,
|
|
@@ -785,14 +797,17 @@ const styles = StyleSheet.create({
|
|
|
785
797
|
width: RANGE_TRIGGER_WIDTH,
|
|
786
798
|
},
|
|
787
799
|
calendarHeader: {
|
|
788
|
-
height:
|
|
800
|
+
height: HEADER_HEIGHT,
|
|
801
|
+
paddingTop: 8,
|
|
802
|
+
paddingBottom: 8,
|
|
789
803
|
flexDirection: "row",
|
|
790
804
|
alignItems: "center",
|
|
791
805
|
justifyContent: "space-between",
|
|
806
|
+
opacity: 1,
|
|
792
807
|
},
|
|
793
808
|
navButton: {
|
|
794
|
-
width:
|
|
795
|
-
height:
|
|
809
|
+
width: HEADER_HEIGHT,
|
|
810
|
+
height: HEADER_HEIGHT,
|
|
796
811
|
alignItems: "center",
|
|
797
812
|
justifyContent: "center",
|
|
798
813
|
},
|
|
@@ -805,7 +820,21 @@ const styles = StyleSheet.create({
|
|
|
805
820
|
textAlign: "center",
|
|
806
821
|
},
|
|
807
822
|
weekdayRow: {
|
|
823
|
+
width: MENU_CONTENT_WIDTH,
|
|
824
|
+
height: WEEKDAY_ROW_HEIGHT,
|
|
825
|
+
marginTop: WEEKDAY_ROW_GAP,
|
|
808
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",
|
|
809
838
|
},
|
|
810
839
|
weekdayText: {
|
|
811
840
|
...datePickerTypography.weekday,
|
|
@@ -814,29 +843,36 @@ const styles = StyleSheet.create({
|
|
|
814
843
|
textTransform: "uppercase",
|
|
815
844
|
},
|
|
816
845
|
weekRow: {
|
|
846
|
+
width: MENU_CONTENT_WIDTH,
|
|
817
847
|
flexDirection: "row",
|
|
818
848
|
position: "relative",
|
|
819
849
|
},
|
|
820
850
|
weekRowSpaced: {
|
|
821
|
-
marginTop:
|
|
851
|
+
marginTop: WEEK_ROW_GAP,
|
|
822
852
|
},
|
|
823
853
|
rangeHighlight: {
|
|
824
854
|
position: "absolute",
|
|
825
|
-
top:
|
|
826
|
-
bottom:
|
|
827
|
-
borderRadius:
|
|
855
|
+
top: 0,
|
|
856
|
+
bottom: 0,
|
|
857
|
+
borderRadius: DAY_CELL_RADIUS,
|
|
828
858
|
backgroundColor: colors.primaryHover,
|
|
829
859
|
},
|
|
830
860
|
dayCellWrap: {
|
|
831
|
-
|
|
861
|
+
width: DAY_CELL_WIDTH,
|
|
832
862
|
height: DAY_CELL_HEIGHT,
|
|
863
|
+
flexGrow: 1,
|
|
864
|
+
flexShrink: 1,
|
|
865
|
+
flexBasis: 0,
|
|
833
866
|
alignItems: "center",
|
|
834
867
|
justifyContent: "center",
|
|
868
|
+
opacity: 1,
|
|
835
869
|
},
|
|
836
870
|
dayCircle: {
|
|
837
|
-
width:
|
|
838
|
-
height:
|
|
839
|
-
borderRadius:
|
|
871
|
+
width: "100%",
|
|
872
|
+
height: "100%",
|
|
873
|
+
borderRadius: DAY_CELL_RADIUS,
|
|
874
|
+
padding: DAY_CELL_PADDING,
|
|
875
|
+
gap: 10,
|
|
840
876
|
alignItems: "center",
|
|
841
877
|
justifyContent: "center",
|
|
842
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";
|