@ecohouse/ui 0.1.44 → 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.44",
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,
@@ -0,0 +1,20 @@
1
+ import Svg, { Path } from "react-native-svg";
2
+ import { colors } from "../../theme";
3
+ import type { IconProps } from "../types";
4
+ import { NO_REVIEW_PATH } from "./noReviewPath";
5
+
6
+ export { NO_REVIEW_PATH } from "./noReviewPath";
7
+
8
+ export function NoReviewIcon({ size = 20, color = colors.grey150, strokeWidth = 2 }: IconProps) {
9
+ return (
10
+ <Svg width={size} height={size} viewBox="0 0 20 20" fill="none">
11
+ <Path
12
+ d={NO_REVIEW_PATH}
13
+ stroke={color}
14
+ strokeWidth={strokeWidth}
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ />
18
+ </Svg>
19
+ );
20
+ }
@@ -0,0 +1,19 @@
1
+ import { colors } from "../../theme";
2
+ import type { IconProps } from "../types";
3
+ import { NO_REVIEW_PATH } from "./noReviewPath";
4
+
5
+ export function NoReviewIcon({ size = 20, color = colors.grey150, strokeWidth = 2 }: IconProps) {
6
+ return (
7
+ <svg width={size} height={size} viewBox="0 0 20 20" fill="none" aria-hidden>
8
+ <path
9
+ d={NO_REVIEW_PATH}
10
+ stroke={color}
11
+ strokeWidth={strokeWidth}
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ />
15
+ </svg>
16
+ );
17
+ }
18
+
19
+ export { NO_REVIEW_PATH } from "./noReviewPath";
@@ -0,0 +1 @@
1
+ export { NoReviewIcon, NO_REVIEW_PATH } from "./NoReviewIcon";
@@ -0,0 +1,3 @@
1
+ /** No-review / empty comment path — 20×20 viewBox. */
2
+ export const NO_REVIEW_PATH =
3
+ "M8.33333 10.833L9.99999 9.16633M9.99999 9.16633L11.6666 7.49967M9.99999 9.16633L8.33333 7.49967M9.99999 9.16633L11.6666 10.833M5.93631 15.584L4.66634 16.6C3.97287 17.1547 3.62596 17.4322 3.33415 17.4326C3.08036 17.4328 2.84037 17.3174 2.68205 17.1191C2.5 16.891 2.5 16.4471 2.5 15.559V5.99984C2.5 5.06642 2.5 4.59936 2.68166 4.24284C2.84144 3.92924 3.09623 3.67445 3.40983 3.51466C3.76635 3.33301 4.23341 3.33301 5.16683 3.33301H14.8335C15.7669 3.33301 16.233 3.33301 16.5895 3.51466C16.9031 3.67445 17.1587 3.92924 17.3185 4.24284C17.5 4.59901 17.5 5.0655 17.5 5.9971V12.336C17.5 13.2676 17.5 13.7334 17.3185 14.0896C17.1587 14.4032 16.9036 14.6584 16.59 14.8182C16.2338 14.9997 15.7675 14.9997 14.8359 14.9997H7.60173C7.25503 14.9997 7.08231 14.9997 6.9165 15.0337C6.7694 15.0639 6.6268 15.1136 6.49307 15.182C6.34401 15.2581 6.21041 15.365 5.94565 15.5768L5.93631 15.584Z";
@@ -27,6 +27,7 @@ export { ChevronLeftIcon } from "./ChevronLeft";
27
27
  export { ClockIcon } from "./Clock";
28
28
  export { CloseIcon } from "./Close";
29
29
  export { CloudUploadIcon } from "./CloudUpload";
30
+ export { NoReviewIcon } from "./NoReview";
30
31
  export { CurrentLocationIcon } from "./CurrentLocation";
31
32
  export { EditIcon } from "./Edit";
32
33
  export { EyeIcon } from "./Eye";
@@ -17,6 +17,7 @@ import { ChevronLeftIcon } from "./ChevronLeft";
17
17
  import { ClockIcon } from "./Clock";
18
18
  import { CloseIcon } from "./Close";
19
19
  import { CloudUploadIcon } from "./CloudUpload";
20
+ import { NoReviewIcon } from "./NoReview";
20
21
  import { EditIcon } from "./Edit";
21
22
  import { EyeIcon } from "./Eye";
22
23
  import {
@@ -117,6 +118,7 @@ export const icons = {
117
118
  clock: ClockIcon,
118
119
  close: CloseIcon,
119
120
  cloudUpload: CloudUploadIcon,
121
+ noReview: NoReviewIcon,
120
122
  edit: EditIcon,
121
123
  clockFilled: ClockFilledIcon,
122
124
  copy: CopyIcon,
@@ -235,6 +237,7 @@ export const iconGroups = {
235
237
  "calendarOutline",
236
238
  "close",
237
239
  "cloudUpload",
240
+ "noReview",
238
241
  "edit",
239
242
  "ratingStar",
240
243
  "saveHeart",
package/src/index.ts CHANGED
@@ -149,6 +149,7 @@ export {
149
149
  ClockIcon,
150
150
  CloseIcon,
151
151
  CloudUploadIcon,
152
+ NoReviewIcon,
152
153
  CurrentLocationIcon,
153
154
  EditIcon,
154
155
  BathroomsIcon,
@@ -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;