@ecohouse/ui 0.1.24 → 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.24",
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(
@@ -70,15 +72,20 @@ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedTo
70
72
  const [contentWidths, setContentWidths] = useState<[number, number]>([0, 0]);
71
73
  const [containerWidth, setContainerWidth] = useState(0);
72
74
  const availableOptionWidth = Math.max(0, (containerWidth - 12) / 2);
73
- const widestContentWidth = Math.max(...contentWidths);
74
- const contentBasedOptionWidth =
75
- widestContentWidth > 0
76
- ? Math.max(OPTION_MIN_WIDTH, widestContentWidth + OPTION_HORIZONTAL_PADDING * 2)
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
+ )
77
81
  : 0;
78
- const optionWidth = fullWidth ? availableOptionWidth : contentBasedOptionWidth;
82
+ const optionWidths: [number, number] = fullWidth
83
+ ? [availableOptionWidth, availableOptionWidth]
84
+ : [resolveContentOptionWidth(contentWidths[0]), resolveContentOptionWidth(contentWidths[1])];
85
+ const selectedOptionWidth = optionWidths[selectedIndex];
79
86
  const indicatorTranslateX = progress.interpolate({
80
87
  inputRange: [0, 1],
81
- outputRange: [0, optionWidth + 4],
88
+ outputRange: [0, optionWidths[0] + 4],
82
89
  });
83
90
 
84
91
  useApplyWebClassName(containerRef, resolvedClassName);
@@ -120,12 +127,36 @@ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedTo
120
127
  ]}
121
128
  accessibilityRole="radiogroup"
122
129
  >
123
- {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 ? (
124
155
  <Animated.View
125
156
  pointerEvents="none"
126
157
  style={[
127
158
  styles.activeIndicator,
128
- { width: optionWidth, transform: [{ translateX: indicatorTranslateX }] },
159
+ { width: selectedOptionWidth, transform: [{ translateX: indicatorTranslateX }] },
129
160
  ]}
130
161
  />
131
162
  ) : null}
@@ -147,23 +178,22 @@ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedTo
147
178
  styles.option,
148
179
  fullWidth
149
180
  ? styles.optionFullWidth
150
- : [styles.optionContentWidth, optionWidth > 0 && { width: optionWidth }],
181
+ : [
182
+ styles.optionContentWidth,
183
+ optionWidths[index] > 0 && { width: optionWidths[index] },
184
+ ],
151
185
  ]}
152
186
  >
153
- <View
154
- onLayout={(event) => {
155
- const nextWidth = event.nativeEvent.layout.width;
156
- setContentWidths((currentWidths) => {
157
- if (currentWidths[index] === nextWidth) return currentWidths;
158
- const nextWidths: [number, number] = [currentWidths[0], currentWidths[1]];
159
- nextWidths[index] = nextWidth;
160
- return nextWidths;
161
- });
162
- }}
163
- style={styles.optionContent}
164
- >
187
+ <View style={styles.optionContent}>
165
188
  {Icon ? <Icon size={ICON_SIZE} color={iconColor} /> : null}
166
- <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
+ >
167
197
  {label}
168
198
  </Text>
169
199
  </View>
@@ -207,7 +237,14 @@ const styles = StyleSheet.create({
207
237
  maxWidth: "100%",
208
238
  flexDirection: "row",
209
239
  alignItems: "center",
210
- 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,
211
248
  },
212
249
  optionFullWidth: {
213
250
  flex: 1,
@@ -228,4 +265,7 @@ const styles = StyleSheet.create({
228
265
  ...segmentedToggleTypography,
229
266
  flexShrink: 1,
230
267
  },
268
+ contentWidthLabel: {
269
+ flexShrink: 0,
270
+ },
231
271
  });