@ecohouse/ui 0.1.23 → 0.1.25

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecohouse/ui",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Cross-platform presentation-only UI components for EcoHouse (React Native + React Native Web)",
@@ -1,7 +1,7 @@
1
1
  import { useState } from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react-vite";
3
3
  import { StyleSheet, View } from "react-native";
4
- import { LayoutGridIcon } from "../../icons";
4
+ import { LayoutGridIcon, ListViewIcon, MapViewIcon } from "../../icons";
5
5
  import { SegmentedToggle } from "./SegmentedToggle";
6
6
 
7
7
  const optionsWithIcons = [
@@ -14,6 +14,21 @@ const optionsWithoutIcons = [
14
14
  { value: "second", label: "Label" },
15
15
  ] as const;
16
16
 
17
+ const multilingualOptions = [
18
+ { value: "daily", label: "Օրավարձ" },
19
+ { value: "overnight", label: "Գիշերակաց երկար տարբերակ" },
20
+ ] as const;
21
+
22
+ const multilingualIconOptions = [
23
+ { value: "list", label: "Ցուցակի տարբերակ", icon: LayoutGridIcon },
24
+ { value: "map", label: "Քարտեզի շատ երկար տարբերակ", icon: LayoutGridIcon },
25
+ ] as const;
26
+
27
+ const armenianViewOptions = [
28
+ { value: "list", label: "Ցանկ", icon: ListViewIcon },
29
+ { value: "map", label: "Քարտեզ", icon: MapViewIcon },
30
+ ] as const;
31
+
17
32
  const meta = {
18
33
  title: "Components/SegmentedToggle",
19
34
  component: SegmentedToggle,
@@ -39,6 +54,38 @@ export const ContentWidth: Story = {
39
54
  args: { fullWidth: false },
40
55
  };
41
56
 
57
+ export const MultilingualContentWidth: Story = {
58
+ args: {
59
+ options: multilingualOptions,
60
+ defaultValue: "daily",
61
+ fullWidth: false,
62
+ },
63
+ };
64
+
65
+ export const MultilingualIconsContentWidth: Story = {
66
+ args: {
67
+ options: multilingualIconOptions,
68
+ defaultValue: "list",
69
+ fullWidth: false,
70
+ },
71
+ };
72
+
73
+ export const ArmenianViewMode: Story = {
74
+ args: {
75
+ options: armenianViewOptions,
76
+ defaultValue: "list",
77
+ fullWidth: false,
78
+ },
79
+ };
80
+
81
+ export const ArmenianViewModeMapSelected: Story = {
82
+ args: {
83
+ options: armenianViewOptions,
84
+ defaultValue: "map",
85
+ fullWidth: false,
86
+ },
87
+ };
88
+
42
89
  export const In400PixelContainer: Story = {
43
90
  render: () => (
44
91
  <View style={storyStyles.container}>
@@ -37,7 +37,9 @@ export interface SegmentedToggleProps extends Omit<ViewProps, "style" | "childre
37
37
  const ICON_SIZE = 20;
38
38
  const ANIMATION_DURATION = 200;
39
39
  const OPTION_HORIZONTAL_PADDING = 16;
40
+ const OPTION_CONTENT_GAP = 12;
40
41
  const OPTION_MIN_WIDTH = 97;
42
+ const OPTION_MEASUREMENT_TOLERANCE = 0.5;
41
43
 
42
44
  export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedToggleProps>(
43
45
  function SegmentedToggle(
@@ -67,12 +69,23 @@ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedTo
67
69
  const resolvedClassName = className?.trim() || undefined;
68
70
  const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
69
71
  const progress = useRef(new Animated.Value(selectedIndex)).current;
72
+ const [contentWidths, setContentWidths] = useState<[number, number]>([0, 0]);
70
73
  const [containerWidth, setContainerWidth] = useState(0);
71
74
  const availableOptionWidth = Math.max(0, (containerWidth - 12) / 2);
72
- const optionWidth = fullWidth ? availableOptionWidth : OPTION_MIN_WIDTH;
75
+ const resolveContentOptionWidth = (contentWidth: number) =>
76
+ contentWidth > 0
77
+ ? Math.max(
78
+ OPTION_MIN_WIDTH,
79
+ contentWidth + OPTION_HORIZONTAL_PADDING * 2 + OPTION_MEASUREMENT_TOLERANCE,
80
+ )
81
+ : 0;
82
+ const optionWidths: [number, number] = fullWidth
83
+ ? [availableOptionWidth, availableOptionWidth]
84
+ : [resolveContentOptionWidth(contentWidths[0]), resolveContentOptionWidth(contentWidths[1])];
85
+ const selectedOptionWidth = optionWidths[selectedIndex];
73
86
  const indicatorTranslateX = progress.interpolate({
74
87
  inputRange: [0, 1],
75
- outputRange: [0, optionWidth + 4],
88
+ outputRange: [0, optionWidths[0] + 4],
76
89
  });
77
90
 
78
91
  useApplyWebClassName(containerRef, resolvedClassName);
@@ -114,16 +127,40 @@ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedTo
114
127
  ]}
115
128
  accessibilityRole="radiogroup"
116
129
  >
117
- {optionWidth > 0 ? (
130
+ {!fullWidth
131
+ ? options.map(({ value: optionValue, label, icon: Icon }, index) => (
132
+ <View
133
+ key={`${optionValue}-measurement`}
134
+ pointerEvents="none"
135
+ aria-hidden
136
+ accessibilityElementsHidden
137
+ importantForAccessibility="no-hide-descendants"
138
+ onLayout={(event) => {
139
+ const nextWidth = event.nativeEvent.layout.width;
140
+ setContentWidths((currentWidths) => {
141
+ if (currentWidths[index] === nextWidth) return currentWidths;
142
+ const nextWidths: [number, number] = [currentWidths[0], currentWidths[1]];
143
+ nextWidths[index] = nextWidth;
144
+ return nextWidths;
145
+ });
146
+ }}
147
+ style={styles.optionMeasurement}
148
+ >
149
+ {Icon ? <Icon size={ICON_SIZE} /> : null}
150
+ <Text style={[styles.label, styles.contentWidthLabel]}>{label}</Text>
151
+ </View>
152
+ ))
153
+ : null}
154
+ {selectedOptionWidth > 0 ? (
118
155
  <Animated.View
119
156
  pointerEvents="none"
120
157
  style={[
121
158
  styles.activeIndicator,
122
- { width: optionWidth, transform: [{ translateX: indicatorTranslateX }] },
159
+ { width: selectedOptionWidth, transform: [{ translateX: indicatorTranslateX }] },
123
160
  ]}
124
161
  />
125
162
  ) : null}
126
- {options.map(({ value: optionValue, label, icon: Icon }) => {
163
+ {options.map(({ value: optionValue, label, icon: Icon }, index) => {
127
164
  const selected = optionValue === selectedValue;
128
165
  const iconColor = selected ? colors.primary : colors.whiteAlpha20;
129
166
  const labelColor = selected ? colors.whiteAlpha86 : colors.whiteAlpha40;
@@ -141,12 +178,22 @@ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedTo
141
178
  styles.option,
142
179
  fullWidth
143
180
  ? styles.optionFullWidth
144
- : { width: OPTION_MIN_WIDTH, minWidth: OPTION_MIN_WIDTH },
181
+ : [
182
+ styles.optionContentWidth,
183
+ optionWidths[index] > 0 && { width: optionWidths[index] },
184
+ ],
145
185
  ]}
146
186
  >
147
187
  <View style={styles.optionContent}>
148
188
  {Icon ? <Icon size={ICON_SIZE} color={iconColor} /> : null}
149
- <Text numberOfLines={1} style={[styles.label, { color: labelColor }]}>
189
+ <Text
190
+ numberOfLines={fullWidth ? 1 : undefined}
191
+ style={[
192
+ styles.label,
193
+ !fullWidth && styles.contentWidthLabel,
194
+ { color: labelColor },
195
+ ]}
196
+ >
150
197
  {label}
151
198
  </Text>
152
199
  </View>
@@ -190,12 +237,22 @@ const styles = StyleSheet.create({
190
237
  maxWidth: "100%",
191
238
  flexDirection: "row",
192
239
  alignItems: "center",
193
- gap: 12,
240
+ gap: OPTION_CONTENT_GAP,
241
+ },
242
+ optionMeasurement: {
243
+ position: "absolute",
244
+ flexDirection: "row",
245
+ alignItems: "center",
246
+ gap: OPTION_CONTENT_GAP,
247
+ opacity: 0,
194
248
  },
195
249
  optionFullWidth: {
196
250
  flex: 1,
197
251
  minWidth: 0,
198
252
  },
253
+ optionContentWidth: {
254
+ minWidth: OPTION_MIN_WIDTH,
255
+ },
199
256
  activeIndicator: {
200
257
  position: "absolute",
201
258
  top: 4,
@@ -208,4 +265,7 @@ const styles = StyleSheet.create({
208
265
  ...segmentedToggleTypography,
209
266
  flexShrink: 1,
210
267
  },
268
+ contentWidthLabel: {
269
+ flexShrink: 0,
270
+ },
211
271
  });