@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.
Files changed (56) hide show
  1. package/dist/index.cjs +30078 -656
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +114 -7
  4. package/dist/index.d.ts +114 -7
  5. package/dist/index.js +30007 -593
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/components/AccordionItem/AccordionItem.stories.tsx +54 -0
  9. package/src/components/AccordionItem/AccordionItem.tsx +223 -0
  10. package/src/components/AccordionItem/index.ts +2 -0
  11. package/src/components/AccountHeader/AccountHeader.tsx +50 -29
  12. package/src/components/Button/Button.stories.tsx +10 -1
  13. package/src/components/Button/Button.tsx +32 -3
  14. package/src/components/ContactInfoCard/ContactInfoCard.stories.tsx +30 -0
  15. package/src/components/ContactInfoCard/ContactInfoCard.tsx +141 -0
  16. package/src/components/ContactInfoCard/index.ts +2 -0
  17. package/src/components/DatePicker/DatePicker.tsx +51 -15
  18. package/src/components/Input/Input.tsx +54 -21
  19. package/src/components/LanguageSwitcher/LanguageSwitcher.stories.tsx +11 -0
  20. package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +103 -19
  21. package/src/components/LanguageSwitcher/index.ts +5 -1
  22. package/src/components/Modal/Modal.stories.tsx +100 -0
  23. package/src/components/Modal/Modal.tsx +165 -0
  24. package/src/components/Modal/index.ts +2 -0
  25. package/src/components/Select/Select.tsx +90 -5
  26. package/src/components/Sidebar/Sidebar.tsx +19 -3
  27. package/src/components/Textarea/Textarea.tsx +29 -4
  28. package/src/components/ToggleRow/ToggleRow.stories.tsx +46 -0
  29. package/src/components/ToggleRow/ToggleRow.tsx +121 -0
  30. package/src/components/ToggleRow/index.ts +2 -0
  31. package/src/components/VerificationCodeInput/VerificationCodeInput.tsx +80 -60
  32. package/src/components/index.ts +17 -1
  33. package/src/icons/CameraFilled/CameraFilledIcon.tsx +15 -0
  34. package/src/icons/CameraFilled/CameraFilledIcon.web.tsx +21 -0
  35. package/src/icons/CameraFilled/cameraFilledPath.ts +2 -0
  36. package/src/icons/CameraFilled/index.ts +1 -0
  37. package/src/icons/Globe/GlobeIcon.tsx +2 -2
  38. package/src/icons/Globe/GlobeIcon.web.tsx +2 -2
  39. package/src/icons/Globe/globePath.ts +2 -2
  40. package/src/icons/Headset/HeadsetIcon.tsx +26 -0
  41. package/src/icons/Headset/HeadsetIcon.web.tsx +25 -0
  42. package/src/icons/Headset/headsetPath.ts +6 -0
  43. package/src/icons/Headset/index.ts +1 -0
  44. package/src/icons/Hide/HideIcon.tsx +21 -0
  45. package/src/icons/Hide/HideIcon.web.tsx +27 -0
  46. package/src/icons/Hide/hidePath.ts +3 -0
  47. package/src/icons/Hide/index.ts +1 -0
  48. package/src/icons/Key/keyPath.ts +1 -1
  49. package/src/icons/MenuLines/MenuLinesIcon.tsx +20 -0
  50. package/src/icons/MenuLines/MenuLinesIcon.web.tsx +26 -0
  51. package/src/icons/MenuLines/index.ts +1 -0
  52. package/src/icons/MenuLines/menuLinesPath.ts +3 -0
  53. package/src/icons/index.ts +4 -0
  54. package/src/icons/registry.ts +11 -0
  55. package/src/index.ts +13 -0
  56. 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
+ });
@@ -0,0 +1,2 @@
1
+ export { ContactInfoCard } from "./ContactInfoCard";
2
+ export type { ContactInfoCardProps } from "./ContactInfoCard";
@@ -119,15 +119,24 @@ export type DatePickerRangeProps = DatePickerBaseProps & {
119
119
 
120
120
  export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps;
121
121
 
122
- const DEFAULT_WIDTH = 380;
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 DAY_CIRCLE_SIZE = 44;
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.dayCellWrap}>
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: 12,
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: 36,
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: 36,
795
- height: 36,
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: 8,
851
+ marginTop: WEEK_ROW_GAP,
822
852
  },
823
853
  rangeHighlight: {
824
854
  position: "absolute",
825
- top: (DAY_CELL_HEIGHT - DAY_CIRCLE_SIZE) / 2,
826
- bottom: (DAY_CELL_HEIGHT - DAY_CIRCLE_SIZE) / 2,
827
- borderRadius: DAY_CIRCLE_SIZE / 2,
855
+ top: 0,
856
+ bottom: 0,
857
+ borderRadius: DAY_CELL_RADIUS,
828
858
  backgroundColor: colors.primaryHover,
829
859
  },
830
860
  dayCellWrap: {
831
- flex: 1,
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: DAY_CIRCLE_SIZE,
838
- height: DAY_CIRCLE_SIZE,
839
- borderRadius: DAY_CIRCLE_SIZE / 2,
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
- <Text style={[styles.label, { color: chrome.labelColor }]}>{label}</Text>
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
- <View
217
- style={[
218
- styles.field,
219
- {
220
- height: metrics.height,
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
- <View style={[styles.iconSlot, { width: metrics.iconSize, height: metrics.iconSize }]}>
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
- </View>
308
+ </Pressable>
287
309
  )
288
310
  ) : null}
289
- </View>
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={[styles.wrapper, isOpen && styles.wrapperOpen, disabled && styles.disabled, 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
- <Text style={styles.triggerLabel} numberOfLines={1}>
169
- {selectedOption?.label ?? ""}
170
- </Text>
171
- <View style={[styles.chevron, isOpen && styles.chevronOpen]}>
172
- <ChevronDownIcon size={ICON_SIZE} color={colors.white} strokeWidth={2} />
173
- </View>
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 style={styles.menu} accessibilityRole="menu">
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
- ? styles.itemSelected
223
+ ? isIconVariant
224
+ ? styles.itemSelectedIcon
225
+ : styles.itemSelected
193
226
  : (hoveredValue === option.value || pressed) && styles.itemHovered,
194
227
  ]}
195
228
  >
196
- <Text style={[styles.itemLabel, selected && styles.itemLabelSelected]}>
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: 20,
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
- overflow: "hidden",
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: CONTROL_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 { LanguageOption, LanguageSwitcherProps } from "./LanguageSwitcher";
2
+ export type {
3
+ LanguageOption,
4
+ LanguageSwitcherProps,
5
+ LanguageSwitcherVariant,
6
+ } from "./LanguageSwitcher";