@ecohouse/ui 0.1.53 → 0.1.55

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.53",
3
+ "version": "0.1.55",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Cross-platform presentation-only UI components for EcoHouse (React Native + React Native Web)",
@@ -26,6 +26,7 @@ const accountMenuItems: AvatarMenuItem[] = [
26
26
  onPress: fn(),
27
27
  backgroundColor: colors.redMuted,
28
28
  hoverColor: colors.redHover,
29
+ labelStyle: { color: colors.red },
29
30
  },
30
31
  ];
31
32
 
@@ -37,12 +38,14 @@ const meta = {
37
38
  email: "anna.hakobyan@example.com",
38
39
  imageUrl: "https://i.pravatar.cc/150?img=47",
39
40
  showChevron: true,
41
+ variant: "light",
40
42
  disabled: false,
41
43
  onPress: fn(),
42
44
  },
43
45
  argTypes: {
44
46
  disabled: { control: "boolean" },
45
47
  showChevron: { control: "boolean" },
48
+ variant: { control: "inline-radio", options: ["light", "dark"] },
46
49
  onPress: { action: "pressed" },
47
50
  },
48
51
  parameters: {
@@ -55,6 +58,11 @@ type Story = StoryObj<typeof meta>;
55
58
 
56
59
  export const Default: Story = {};
57
60
 
61
+ /** Original solid avatar design, preserved through the `dark` variant. */
62
+ export const Dark: Story = {
63
+ args: { variant: "dark" },
64
+ };
65
+
58
66
  /** No `imageUrl` — falls back to the placeholder person icon. */
59
67
  export const WithoutImage: Story = {
60
68
  args: { imageUrl: undefined },
@@ -28,13 +28,15 @@ import { MenuItem, type MenuItemProps } from "../../primitives";
28
28
  /** Data shape for an Avatar dropdown option — same fields as `MenuItem`. */
29
29
  export type AvatarMenuItem = Pick<
30
30
  MenuItemProps,
31
- "icon" | "label" | "disabled" | "backgroundColor" | "hoverColor"
31
+ "icon" | "label" | "disabled" | "backgroundColor" | "hoverColor" | "labelStyle"
32
32
  > & {
33
33
  /** Stable identity for React keys; falls back to `label`. */
34
34
  key?: string;
35
35
  onPress: () => void;
36
36
  };
37
37
 
38
+ export type AvatarVariant = "light" | "dark";
39
+
38
40
  export interface AvatarProps extends Omit<
39
41
  PressableProps,
40
42
  "onPress" | "disabled" | "style" | "children" | "hitSlop"
@@ -51,6 +53,8 @@ export interface AvatarProps extends Omit<
51
53
  disabled?: boolean;
52
54
  /** Trailing chevron, typically used to hint a dropdown/menu. */
53
55
  showChevron?: boolean;
56
+ /** Visual treatment. `light` is the translucent header design; `dark` preserves the original solid design. */
57
+ variant?: AvatarVariant;
54
58
  /**
55
59
  * Account-menu options, supplied entirely by the consumer (icon, label,
56
60
  * action, and per-item colors). When set, pressing the chip opens a
@@ -69,11 +73,28 @@ export interface AvatarProps extends Omit<
69
73
  }
70
74
 
71
75
  const IMAGE_SIZE = 48;
76
+ const CONTAINER_HEIGHT = 56;
72
77
  const CONTAINER_RADIUS = 71;
73
78
  const CHEVRON_SIZE = 20;
74
79
  const FALLBACK_ICON_SIZE = 20;
75
80
  const MENU_RADIUS = 16;
76
81
 
82
+ const triggerWebBlurStyle: ViewStyle =
83
+ Platform.OS === "web"
84
+ ? ({
85
+ backdropFilter: "blur(40px)",
86
+ WebkitBackdropFilter: "blur(40px)",
87
+ } as ViewStyle)
88
+ : {};
89
+
90
+ const menuWebBlurStyle: ViewStyle =
91
+ Platform.OS === "web"
92
+ ? ({
93
+ backdropFilter: "blur(40px)",
94
+ WebkitBackdropFilter: "blur(40px)",
95
+ } as ViewStyle)
96
+ : {};
97
+
77
98
  export const Avatar = forwardRef<ComponentRef<typeof View>, AvatarProps>(function Avatar(
78
99
  {
79
100
  name,
@@ -82,6 +103,7 @@ export const Avatar = forwardRef<ComponentRef<typeof View>, AvatarProps>(functio
82
103
  onPress,
83
104
  disabled = false,
84
105
  showChevron = true,
106
+ variant = "light",
85
107
  menuItems,
86
108
  open: openProp,
87
109
  defaultOpen = false,
@@ -106,6 +128,7 @@ export const Avatar = forwardRef<ComponentRef<typeof View>, AvatarProps>(functio
106
128
  const isOpenControlled = typeof openProp === "boolean";
107
129
  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
108
130
  const isOpen = hasMenu && (isOpenControlled ? openProp : uncontrolledOpen);
131
+ const isLightVariant = variant === "light";
109
132
 
110
133
  useApplyWebClassName(containerRef, className);
111
134
 
@@ -194,7 +217,13 @@ export const Avatar = forwardRef<ComponentRef<typeof View>, AvatarProps>(functio
194
217
  accessibilityState={
195
218
  isInteractive ? { disabled, expanded: hasMenu ? isOpen : undefined } : undefined
196
219
  }
197
- style={[styles.root, disabled && styles.disabled, style]}
220
+ style={[
221
+ styles.root,
222
+ isLightVariant ? styles.rootLight : styles.rootDark,
223
+ isLightVariant && triggerWebBlurStyle,
224
+ disabled && styles.disabled,
225
+ style,
226
+ ]}
198
227
  >
199
228
  <View style={styles.imageWrap}>
200
229
  {hasImage ? (
@@ -204,8 +233,16 @@ export const Avatar = forwardRef<ComponentRef<typeof View>, AvatarProps>(functio
204
233
  onError={() => setImageFailed(true)}
205
234
  />
206
235
  ) : (
207
- <View style={styles.imageFallback}>
208
- <UserIcon size={FALLBACK_ICON_SIZE} color={colors.primary} />
236
+ <View
237
+ style={[
238
+ styles.imageFallback,
239
+ isLightVariant ? styles.imageFallbackLight : styles.imageFallbackDark,
240
+ ]}
241
+ >
242
+ <UserIcon
243
+ size={FALLBACK_ICON_SIZE}
244
+ color={isLightVariant ? colors.white : colors.primary}
245
+ />
209
246
  </View>
210
247
  )}
211
248
  </View>
@@ -215,7 +252,10 @@ export const Avatar = forwardRef<ComponentRef<typeof View>, AvatarProps>(functio
215
252
  {name}
216
253
  </Text>
217
254
  {email ? (
218
- <Text style={styles.email} numberOfLines={1}>
255
+ <Text
256
+ style={[styles.email, isLightVariant ? styles.emailLight : styles.emailDark]}
257
+ numberOfLines={1}
258
+ >
219
259
  {email}
220
260
  </Text>
221
261
  ) : null}
@@ -223,13 +263,23 @@ export const Avatar = forwardRef<ComponentRef<typeof View>, AvatarProps>(functio
223
263
 
224
264
  {showChevron ? (
225
265
  <View style={[styles.chevronSlot, isOpen && styles.chevronOpen]}>
226
- <ChevronDownIcon size={CHEVRON_SIZE} color={isOpen ? colors.primary : colors.grey100} />
266
+ <ChevronDownIcon
267
+ size={CHEVRON_SIZE}
268
+ color={isLightVariant ? colors.white : isOpen ? colors.primary : colors.grey100}
269
+ />
227
270
  </View>
228
271
  ) : null}
229
272
  </Pressable>
230
273
 
231
274
  {hasMenu && isOpen ? (
232
- <View style={[styles.menu, menuWidth != null && { right: undefined, width: menuWidth }]}>
275
+ <View
276
+ style={[
277
+ styles.menu,
278
+ isLightVariant ? styles.menuLight : styles.menuDark,
279
+ isLightVariant && menuWebBlurStyle,
280
+ menuWidth != null && { right: undefined, width: menuWidth },
281
+ ]}
282
+ >
233
283
  {resolvedMenuItems.map((item) => {
234
284
  const itemKey = item.key ?? item.label;
235
285
  return (
@@ -239,8 +289,16 @@ export const Avatar = forwardRef<ComponentRef<typeof View>, AvatarProps>(functio
239
289
  label={item.label}
240
290
  disabled={item.disabled}
241
291
  backgroundColor={item.backgroundColor}
242
- hoverColor={item.hoverColor}
292
+ hoverColor={item.hoverColor ?? (isLightVariant ? colors.whiteAlpha7 : undefined)}
293
+ labelStyle={item.labelStyle}
243
294
  onPress={() => handleItemPress(item)}
295
+ style={
296
+ isLightVariant
297
+ ? item.backgroundColor
298
+ ? styles.emphasizedMenuItem
299
+ : styles.menuItem
300
+ : undefined
301
+ }
244
302
  />
245
303
  );
246
304
  })}
@@ -259,6 +317,7 @@ const styles = StyleSheet.create({
259
317
  zIndex: 20,
260
318
  },
261
319
  root: {
320
+ height: CONTAINER_HEIGHT,
262
321
  flexDirection: "row",
263
322
  alignItems: "center",
264
323
  alignSelf: "flex-start",
@@ -268,6 +327,11 @@ const styles = StyleSheet.create({
268
327
  paddingLeft: 4,
269
328
  paddingRight: 12,
270
329
  borderRadius: CONTAINER_RADIUS,
330
+ },
331
+ rootLight: {
332
+ backgroundColor: colors.whiteAlpha5,
333
+ },
334
+ rootDark: {
271
335
  backgroundColor: colors.grey700,
272
336
  },
273
337
  disabled: {
@@ -289,6 +353,14 @@ const styles = StyleSheet.create({
289
353
  height: IMAGE_SIZE,
290
354
  alignItems: "center",
291
355
  justifyContent: "center",
356
+ },
357
+ imageFallbackLight: {
358
+ borderRadius: IMAGE_SIZE / 2,
359
+ backgroundColor: "transparent",
360
+ borderWidth: 1,
361
+ borderColor: colors.whiteAlpha10,
362
+ },
363
+ imageFallbackDark: {
292
364
  backgroundColor: colors.grey600,
293
365
  },
294
366
  textColumn: {
@@ -302,6 +374,11 @@ const styles = StyleSheet.create({
302
374
  },
303
375
  email: {
304
376
  ...avatarTypography.email,
377
+ },
378
+ emailLight: {
379
+ color: colors.whiteAlpha60,
380
+ },
381
+ emailDark: {
305
382
  color: colors.grey0,
306
383
  },
307
384
  chevronSlot: {
@@ -324,6 +401,14 @@ const styles = StyleSheet.create({
324
401
  borderRadius: MENU_RADIUS,
325
402
  padding: 8,
326
403
  gap: 8,
404
+ },
405
+ menuLight: {
406
+ borderWidth: 1,
407
+ borderColor: colors.whiteAlpha10,
408
+ backgroundColor: colors.whiteAlpha5,
409
+ overflow: "hidden",
410
+ },
411
+ menuDark: {
327
412
  backgroundColor: colors.grey700,
328
413
  ...Platform.select({
329
414
  web: {
@@ -334,4 +419,14 @@ const styles = StyleSheet.create({
334
419
  },
335
420
  }),
336
421
  },
422
+ menuItem: {
423
+ height: 32,
424
+ minHeight: 32,
425
+ paddingVertical: 6,
426
+ },
427
+ emphasizedMenuItem: {
428
+ height: 46,
429
+ minHeight: 46,
430
+ paddingVertical: 11,
431
+ },
337
432
  });
@@ -1,2 +1,2 @@
1
1
  export { Avatar } from "./Avatar";
2
- export type { AvatarProps, AvatarMenuItem } from "./Avatar";
2
+ export type { AvatarProps, AvatarMenuItem, AvatarVariant } from "./Avatar";
@@ -60,15 +60,24 @@ export const defaultLanguageOptions = [
60
60
 
61
61
  const CONTROL_WIDTH = 96;
62
62
  const CONTROL_HEIGHT = 35;
63
+ const MENU_WIDTH = 90;
63
64
  const ICON_SIZE = 16;
64
65
  const ICON_BUTTON_SIZE = 32;
65
66
  const ICON_MENU_WIDTH = 67;
66
67
  const ICON_MENU_HEIGHT = 129;
67
68
  const ICON_MENU_TOP = 39;
68
69
  const ITEM_HEIGHT = 35;
69
- /** Active option wash from design (`#E38E5E` at 7%). */
70
+ /** Active option wash for the compact account-header variant. */
70
71
  const ACTIVE_ITEM_BACKGROUND = "#E38E5E12";
71
72
 
73
+ const menuWebBlurStyle: ViewStyle =
74
+ Platform.OS === "web"
75
+ ? ({
76
+ backdropFilter: "blur(40px)",
77
+ WebkitBackdropFilter: "blur(40px)",
78
+ } as ViewStyle)
79
+ : {};
80
+
72
81
  export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSwitcherProps>(
73
82
  function LanguageSwitcher(
74
83
  {
@@ -184,7 +193,7 @@ export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSw
184
193
  onHoverOut={() => setTriggerHovered(false)}
185
194
  style={({ pressed }) => [
186
195
  isIconVariant ? styles.triggerIcon : styles.trigger,
187
- !isIconVariant && (triggerHovered || pressed || isOpen) && styles.triggerActive,
196
+ !isIconVariant && (triggerHovered || pressed) && styles.triggerActive,
188
197
  ]}
189
198
  >
190
199
  <GlobeIcon size={ICON_SIZE} color={colors.white} strokeWidth={isIconVariant ? 1.5 : 2} />
@@ -202,7 +211,11 @@ export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSw
202
211
 
203
212
  {isOpen && options.length > 0 ? (
204
213
  <View
205
- style={[styles.menu, isIconVariant ? styles.menuIcon : styles.menuDefault]}
214
+ style={[
215
+ styles.menu,
216
+ !isIconVariant && menuWebBlurStyle,
217
+ isIconVariant ? styles.menuIcon : styles.menuDefault,
218
+ ]}
206
219
  accessibilityRole="menu"
207
220
  >
208
221
  {options.map((option) => {
@@ -218,7 +231,7 @@ export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSw
218
231
  onHoverOut={() => setHoveredValue(null)}
219
232
  style={({ pressed }) => [
220
233
  styles.item,
221
- isIconVariant ? styles.itemIcon : null,
234
+ isIconVariant ? styles.itemIcon : styles.itemDefault,
222
235
  selected
223
236
  ? isIconVariant
224
237
  ? styles.itemSelectedIcon
@@ -226,15 +239,17 @@ export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSw
226
239
  : (hoveredValue === option.value || pressed) && styles.itemHovered,
227
240
  ]}
228
241
  >
229
- <Text
230
- style={[
231
- styles.itemLabel,
232
- selected &&
233
- (isIconVariant ? styles.itemLabelSelectedIcon : styles.itemLabelSelected),
234
- ]}
235
- >
236
- {option.label}
237
- </Text>
242
+ {({ pressed }) => (
243
+ <Text
244
+ style={[
245
+ styles.itemLabel,
246
+ (selected || hoveredValue === option.value || pressed) &&
247
+ styles.itemLabelActive,
248
+ ]}
249
+ >
250
+ {option.label}
251
+ </Text>
252
+ )}
238
253
  </Pressable>
239
254
  );
240
255
  })}
@@ -314,10 +329,14 @@ const styles = StyleSheet.create({
314
329
  },
315
330
  menuDefault: {
316
331
  top: CONTROL_HEIGHT + 8,
317
- left: 0,
318
- width: CONTROL_WIDTH,
319
- gap: 8,
320
- backgroundColor: colors.whiteAlpha6,
332
+ left: (CONTROL_WIDTH - MENU_WIDTH) / 2,
333
+ width: MENU_WIDTH,
334
+ height: 129,
335
+ gap: 4,
336
+ backgroundColor: colors.whiteAlpha5,
337
+ borderWidth: 1,
338
+ borderColor: colors.whiteAlpha10,
339
+ borderRadius: 16,
321
340
  },
322
341
  menuIcon: {
323
342
  top: ICON_MENU_TOP,
@@ -345,11 +364,15 @@ const styles = StyleSheet.create({
345
364
  width: 51,
346
365
  alignSelf: "stretch",
347
366
  },
367
+ itemDefault: {
368
+ width: 74,
369
+ alignSelf: "center",
370
+ },
348
371
  itemHovered: {
349
- backgroundColor: colors.whiteAlpha6,
372
+ backgroundColor: colors.whiteAlpha7,
350
373
  },
351
374
  itemSelected: {
352
- backgroundColor: colors.whiteAlpha6,
375
+ backgroundColor: colors.whiteAlpha7,
353
376
  },
354
377
  itemSelectedIcon: {
355
378
  backgroundColor: ACTIVE_ITEM_BACKGROUND,
@@ -357,12 +380,9 @@ const styles = StyleSheet.create({
357
380
  itemLabel: {
358
381
  ...languageSwitcherTypography,
359
382
  ...Platform.select({ web: { fontFamily: `${fonts.sans}, sans-serif` } }),
360
- color: colors.white,
361
- },
362
- itemLabelSelected: {
363
- color: colors.primary,
383
+ color: colors.whiteAlpha60,
364
384
  },
365
- itemLabelSelectedIcon: {
366
- color: colors.primary,
385
+ itemLabelActive: {
386
+ color: colors.white,
367
387
  },
368
388
  });