@lotics/ui 2.4.0 → 2.4.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/avatar.web.tsx +102 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
@@ -0,0 +1,102 @@
1
+ import type { ImageContentFit, ImageSource } from "expo-image";
2
+ import { Image, View, StyleSheet, StyleProp, ViewStyle, ImageStyle } from "react-native";
3
+ import { Text } from "./text";
4
+ import { colors } from "./colors";
5
+
6
+ interface AvatarProps {
7
+ size?: number;
8
+ source?: ImageSource;
9
+ name?: string;
10
+ style?: StyleProp<ViewStyle | ImageStyle>;
11
+ contentFit?: ImageContentFit;
12
+ /**
13
+ * When true, the avatar announces its `name` to assistive tech. Default
14
+ * false because avatars almost always appear adjacent to the name text —
15
+ * announcing the image as well would double-read. Pass `announce` when the
16
+ * avatar is standalone (with no visible name nearby).
17
+ */
18
+ announce?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Web Avatar. The native `avatar.tsx` renders through `expo-image`, a native
23
+ * module that pulls expo's runtime into the graph and fails to load under
24
+ * pure-web bundlers (Vite dev throws on its CJS interop; vitest can't resolve
25
+ * expo's winter runtime). On web, `react-native`'s `Image` (→ react-native-web
26
+ * → `<img>`) renders the same circular avatar with none of that cost. The prop
27
+ * surface is identical — `ImageSource`/`ImageContentFit` are kept as erased
28
+ * type-only imports so no expo-image module is ever loaded.
29
+ */
30
+ export function Avatar(props: AvatarProps) {
31
+ const { source, size = 32, name = "Unknown", style, contentFit, announce } = props;
32
+ const decorative = !announce;
33
+
34
+ if (!source || !source.uri) {
35
+ return (
36
+ <View
37
+ accessible={!decorative}
38
+ accessibilityLabel={decorative ? undefined : name}
39
+ accessibilityElementsHidden={decorative}
40
+ importantForAccessibility={decorative ? "no-hide-descendants" : undefined}
41
+ aria-hidden={decorative || undefined}
42
+ style={[
43
+ styles.base,
44
+ { backgroundColor: colors.blue["600"], width: size, height: size },
45
+ style,
46
+ ]}
47
+ >
48
+ {/* Initials are a visual shorthand for the name; the accessible name is
49
+ on the container so the SR does not read "HM" in addition. */}
50
+ <Text
51
+ userSelect="none"
52
+ size="xs"
53
+ weight="medium"
54
+ color="inverted"
55
+ accessibilityElementsHidden
56
+ importantForAccessibility="no-hide-descendants"
57
+ aria-hidden
58
+ >
59
+ {getInitials(name, size)}
60
+ </Text>
61
+ </View>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <Image
67
+ accessibilityLabel={decorative ? undefined : name}
68
+ accessibilityElementsHidden={decorative}
69
+ importantForAccessibility={decorative ? "no-hide-descendants" : undefined}
70
+ style={[styles.base, { width: size, height: size }, style as ImageStyle]}
71
+ source={{ uri: source.uri }}
72
+ resizeMode={contentFit === "contain" ? "contain" : "cover"}
73
+ />
74
+ );
75
+ }
76
+
77
+ function getInitials(name: string, size?: number): string {
78
+ let initials = 2;
79
+
80
+ if (size && size <= 32) {
81
+ initials = 1;
82
+
83
+ if (name.length <= 2) {
84
+ return name;
85
+ }
86
+ }
87
+
88
+ return name
89
+ .split(" ")
90
+ .map((c) => c.charAt(0).toUpperCase())
91
+ .slice(0, initials)
92
+ .join("");
93
+ }
94
+
95
+ const styles = StyleSheet.create({
96
+ base: {
97
+ borderRadius: 999,
98
+ justifyContent: "center",
99
+ alignItems: "center",
100
+ userSelect: "none",
101
+ },
102
+ });