@ecohouse/ui 0.1.45 → 0.1.46

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.45",
3
+ "version": "0.1.46",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Cross-platform presentation-only UI components for EcoHouse (React Native + React Native Web)",
@@ -47,7 +47,6 @@ const ANIMATION_DURATION = 200;
47
47
  const OPTION_HORIZONTAL_PADDING = 16;
48
48
  const OPTION_CONTENT_GAP = 12;
49
49
  const OPTION_MIN_WIDTH = 97;
50
- const OPTION_MEASUREMENT_TOLERANCE = 0.5;
51
50
  const TRACK_PADDING = 4;
52
51
  const OPTION_GAP = 4;
53
52
 
@@ -91,7 +90,7 @@ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedTo
91
90
  const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
92
91
  const progress = useRef(new Animated.Value(selectedIndex)).current;
93
92
  const hasMounted = useRef(false);
94
- const [contentWidths, setContentWidths] = useState<number[]>(() =>
93
+ const [measuredOptionWidths, setMeasuredOptionWidths] = useState<number[]>(() =>
95
94
  Array.from({ length: optionCount }, () => 0),
96
95
  );
97
96
  const [containerWidth, setContainerWidth] = useState(0);
@@ -99,19 +98,12 @@ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedTo
99
98
  // Track chrome: left+right padding + gaps between options.
100
99
  const trackChrome = TRACK_PADDING * 2 + OPTION_GAP * (optionCount - 1);
101
100
  const availableOptionWidth = Math.max(0, (containerWidth - trackChrome) / optionCount);
102
- const resolveContentOptionWidth = (contentWidth: number) =>
103
- contentWidth > 0
104
- ? Math.max(
105
- OPTION_MIN_WIDTH,
106
- contentWidth + OPTION_HORIZONTAL_PADDING * 2 + OPTION_MEASUREMENT_TOLERANCE,
107
- )
108
- : 0;
109
-
110
101
  const optionWidths = fullWidth
111
102
  ? Array.from({ length: optionCount }, () => availableOptionWidth)
112
- : contentWidths.map((width) => resolveContentOptionWidth(width));
103
+ : measuredOptionWidths;
113
104
 
114
- const selectedOptionWidth = optionWidths[selectedIndex] ?? 0;
105
+ const measurementsReady = optionWidths.every((width) => width > 0);
106
+ const selectedOptionWidth = measurementsReady ? (optionWidths[selectedIndex] ?? 0) : 0;
115
107
  const indicatorInputRange = options.map((_, index) => index);
116
108
  const indicatorOutputRange = buildIndicatorOutputRange(optionWidths);
117
109
  const indicatorTranslateX = progress.interpolate({
@@ -122,7 +114,7 @@ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedTo
122
114
  useApplyWebClassName(containerRef, resolvedClassName);
123
115
 
124
116
  useEffect(() => {
125
- setContentWidths((current) => {
117
+ setMeasuredOptionWidths((current) => {
126
118
  if (current.length === optionCount) return current;
127
119
  return Array.from({ length: optionCount }, (_, index) => current[index] ?? 0);
128
120
  });
@@ -171,32 +163,6 @@ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedTo
171
163
  ]}
172
164
  accessibilityRole="radiogroup"
173
165
  >
174
- {!fullWidth
175
- ? options.map(({ value: optionValue, label, icon: Icon, showLabel = true }, index) => (
176
- <View
177
- key={`${optionValue}-measurement`}
178
- pointerEvents="none"
179
- aria-hidden
180
- accessibilityElementsHidden
181
- importantForAccessibility="no-hide-descendants"
182
- onLayout={(event) => {
183
- const nextWidth = event.nativeEvent.layout.width;
184
- setContentWidths((currentWidths) => {
185
- if (currentWidths[index] === nextWidth) return currentWidths;
186
- const nextWidths = [...currentWidths];
187
- nextWidths[index] = nextWidth;
188
- return nextWidths;
189
- });
190
- }}
191
- style={styles.optionMeasurement}
192
- >
193
- {Icon ? <Icon size={ICON_SIZE} /> : null}
194
- {showLabel ? (
195
- <Text style={[styles.label, styles.contentWidthLabel]}>{label}</Text>
196
- ) : null}
197
- </View>
198
- ))
199
- : null}
200
166
  {selectedOptionWidth > 0 ? (
201
167
  <Animated.View
202
168
  pointerEvents="none"
@@ -210,7 +176,6 @@ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedTo
210
176
  const selected = optionValue === selectedValue;
211
177
  const iconColor = selected ? colors.primary : colors.whiteAlpha20;
212
178
  const labelColor = selected ? colors.white : colors.grey150;
213
- const optionWidth = optionWidths[index] ?? 0;
214
179
 
215
180
  return (
216
181
  <Pressable
@@ -218,18 +183,45 @@ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedTo
218
183
  onPress={() => selectOption(optionValue)}
219
184
  disabled={disabled}
220
185
  hitSlop={hitSlop}
186
+ onLayout={
187
+ fullWidth
188
+ ? undefined
189
+ : (event) => {
190
+ const nextWidth = event.nativeEvent.layout.width;
191
+ setMeasuredOptionWidths((currentWidths) => {
192
+ if (currentWidths[index] === nextWidth) return currentWidths;
193
+ const nextWidths = [...currentWidths];
194
+ nextWidths[index] = nextWidth;
195
+ return nextWidths;
196
+ });
197
+ }
198
+ }
221
199
  accessibilityRole="radio"
222
200
  accessibilityLabel={label}
223
201
  accessibilityState={{ selected, disabled }}
224
202
  style={[
225
203
  styles.option,
226
- fullWidth
227
- ? styles.optionFullWidth
228
- : [styles.optionContentWidth, optionWidth > 0 && { width: optionWidth }],
229
- selected && selectedOptionWidth === 0 && styles.initialSelectedOption,
204
+ fullWidth ? styles.optionFullWidth : styles.optionContentWidth,
205
+ selected && !measurementsReady && styles.initialSelectedOption,
230
206
  ]}
231
207
  >
232
- <View style={styles.optionContent}>
208
+ {!fullWidth ? (
209
+ <View
210
+ aria-hidden
211
+ accessibilityElementsHidden
212
+ importantForAccessibility="no-hide-descendants"
213
+ pointerEvents="none"
214
+ style={[styles.optionContent, styles.optionSizeProbe]}
215
+ >
216
+ {Icon ? <Icon size={ICON_SIZE} /> : null}
217
+ {showLabel ? (
218
+ <Text style={[styles.label, styles.selectedLabel, styles.contentWidthLabel]}>
219
+ {label}
220
+ </Text>
221
+ ) : null}
222
+ </View>
223
+ ) : null}
224
+ <View style={[styles.optionContent, !fullWidth && styles.contentWidthVisibleContent]}>
233
225
  {Icon ? <Icon size={ICON_SIZE} color={iconColor} /> : null}
234
226
  {showLabel ? (
235
227
  <Text
@@ -287,13 +279,17 @@ const styles = StyleSheet.create({
287
279
  alignItems: "center",
288
280
  gap: OPTION_CONTENT_GAP,
289
281
  },
290
- optionMeasurement: {
291
- position: "absolute",
292
- flexDirection: "row",
293
- alignItems: "center",
294
- gap: OPTION_CONTENT_GAP,
282
+ optionSizeProbe: {
295
283
  opacity: 0,
296
284
  },
285
+ contentWidthVisibleContent: {
286
+ position: "absolute",
287
+ top: 0,
288
+ left: 0,
289
+ width: "100%",
290
+ height: "100%",
291
+ justifyContent: "center",
292
+ },
297
293
  optionFullWidth: {
298
294
  flex: 1,
299
295
  minWidth: 0,
@@ -156,7 +156,7 @@ export const selectTypography = {
156
156
  },
157
157
  } as const;
158
158
 
159
- /** Avatar name / email / menu-item typography — 100% line-height, 0 letter-spacing. */
159
+ /** Avatar name / email / menu-item typography — compact line heights, 0 letter-spacing. */
160
160
  export const avatarTypography = {
161
161
  name: {
162
162
  fontFamily: fonts.sans,
@@ -176,7 +176,7 @@ export const avatarTypography = {
176
176
  fontFamily: fonts.sans,
177
177
  fontSize: 12,
178
178
  fontWeight: "600" as const,
179
- lineHeight: 12,
179
+ lineHeight: 20,
180
180
  letterSpacing: 0,
181
181
  },
182
182
  } as const;