@oxyhq/bloom 0.8.5 → 0.9.0

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 (119) hide show
  1. package/lib/commonjs/avatar-group/AvatarGroup.js +26 -0
  2. package/lib/commonjs/avatar-group/AvatarGroup.js.map +1 -0
  3. package/lib/commonjs/avatar-group/AvatarGroup.web.js +116 -0
  4. package/lib/commonjs/avatar-group/AvatarGroup.web.js.map +1 -0
  5. package/lib/commonjs/avatar-group/AvatarGroupBase.js +184 -0
  6. package/lib/commonjs/avatar-group/AvatarGroupBase.js.map +1 -0
  7. package/lib/commonjs/avatar-group/index.js +13 -0
  8. package/lib/commonjs/avatar-group/index.js.map +1 -0
  9. package/lib/commonjs/avatar-group/index.web.js +13 -0
  10. package/lib/commonjs/avatar-group/index.web.js.map +1 -0
  11. package/lib/commonjs/avatar-group/types.js +6 -0
  12. package/lib/commonjs/avatar-group/types.js.map +1 -0
  13. package/lib/commonjs/index.js +60 -44
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/index.web.js +74 -58
  16. package/lib/commonjs/index.web.js.map +1 -1
  17. package/lib/commonjs/theme/color-scope/index.web.js +6 -7
  18. package/lib/commonjs/theme/color-scope/index.web.js.map +1 -1
  19. package/lib/commonjs/theme/color-scope/style-builder.js +18 -6
  20. package/lib/commonjs/theme/color-scope/style-builder.js.map +1 -1
  21. package/lib/commonjs/user-hover-card/UserHoverCard.js +193 -0
  22. package/lib/commonjs/user-hover-card/UserHoverCard.js.map +1 -0
  23. package/lib/commonjs/user-hover-card/index.js +13 -0
  24. package/lib/commonjs/user-hover-card/index.js.map +1 -0
  25. package/lib/commonjs/user-hover-card/types.js +6 -0
  26. package/lib/commonjs/user-hover-card/types.js.map +1 -0
  27. package/lib/module/avatar-group/AvatarGroup.js +21 -0
  28. package/lib/module/avatar-group/AvatarGroup.js.map +1 -0
  29. package/lib/module/avatar-group/AvatarGroup.web.js +111 -0
  30. package/lib/module/avatar-group/AvatarGroup.web.js.map +1 -0
  31. package/lib/module/avatar-group/AvatarGroupBase.js +179 -0
  32. package/lib/module/avatar-group/AvatarGroupBase.js.map +1 -0
  33. package/lib/module/avatar-group/index.js +4 -0
  34. package/lib/module/avatar-group/index.js.map +1 -0
  35. package/lib/module/avatar-group/index.web.js +4 -0
  36. package/lib/module/avatar-group/index.web.js.map +1 -0
  37. package/lib/module/avatar-group/types.js +4 -0
  38. package/lib/module/avatar-group/types.js.map +1 -0
  39. package/lib/module/index.js +2 -0
  40. package/lib/module/index.js.map +1 -1
  41. package/lib/module/index.web.js +2 -0
  42. package/lib/module/index.web.js.map +1 -1
  43. package/lib/module/theme/color-scope/index.web.js +6 -7
  44. package/lib/module/theme/color-scope/index.web.js.map +1 -1
  45. package/lib/module/theme/color-scope/style-builder.js +19 -7
  46. package/lib/module/theme/color-scope/style-builder.js.map +1 -1
  47. package/lib/module/user-hover-card/UserHoverCard.js +188 -0
  48. package/lib/module/user-hover-card/UserHoverCard.js.map +1 -0
  49. package/lib/module/user-hover-card/index.js +4 -0
  50. package/lib/module/user-hover-card/index.js.map +1 -0
  51. package/lib/module/user-hover-card/types.js +4 -0
  52. package/lib/module/user-hover-card/types.js.map +1 -0
  53. package/lib/typescript/commonjs/avatar-group/AvatarGroup.d.ts +4 -0
  54. package/lib/typescript/commonjs/avatar-group/AvatarGroup.d.ts.map +1 -0
  55. package/lib/typescript/commonjs/avatar-group/AvatarGroup.web.d.ts +4 -0
  56. package/lib/typescript/commonjs/avatar-group/AvatarGroup.web.d.ts.map +1 -0
  57. package/lib/typescript/commonjs/avatar-group/AvatarGroupBase.d.ts +16 -0
  58. package/lib/typescript/commonjs/avatar-group/AvatarGroupBase.d.ts.map +1 -0
  59. package/lib/typescript/commonjs/avatar-group/index.d.ts +3 -0
  60. package/lib/typescript/commonjs/avatar-group/index.d.ts.map +1 -0
  61. package/lib/typescript/commonjs/avatar-group/index.web.d.ts +3 -0
  62. package/lib/typescript/commonjs/avatar-group/index.web.d.ts.map +1 -0
  63. package/lib/typescript/commonjs/avatar-group/types.d.ts +63 -0
  64. package/lib/typescript/commonjs/avatar-group/types.d.ts.map +1 -0
  65. package/lib/typescript/commonjs/index.d.ts +4 -0
  66. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/index.web.d.ts +4 -0
  68. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/theme/color-scope/index.web.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts +7 -5
  71. package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/user-hover-card/UserHoverCard.d.ts +4 -0
  73. package/lib/typescript/commonjs/user-hover-card/UserHoverCard.d.ts.map +1 -0
  74. package/lib/typescript/commonjs/user-hover-card/index.d.ts +3 -0
  75. package/lib/typescript/commonjs/user-hover-card/index.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/user-hover-card/types.d.ts +37 -0
  77. package/lib/typescript/commonjs/user-hover-card/types.d.ts.map +1 -0
  78. package/lib/typescript/module/avatar-group/AvatarGroup.d.ts +4 -0
  79. package/lib/typescript/module/avatar-group/AvatarGroup.d.ts.map +1 -0
  80. package/lib/typescript/module/avatar-group/AvatarGroup.web.d.ts +4 -0
  81. package/lib/typescript/module/avatar-group/AvatarGroup.web.d.ts.map +1 -0
  82. package/lib/typescript/module/avatar-group/AvatarGroupBase.d.ts +16 -0
  83. package/lib/typescript/module/avatar-group/AvatarGroupBase.d.ts.map +1 -0
  84. package/lib/typescript/module/avatar-group/index.d.ts +3 -0
  85. package/lib/typescript/module/avatar-group/index.d.ts.map +1 -0
  86. package/lib/typescript/module/avatar-group/index.web.d.ts +3 -0
  87. package/lib/typescript/module/avatar-group/index.web.d.ts.map +1 -0
  88. package/lib/typescript/module/avatar-group/types.d.ts +63 -0
  89. package/lib/typescript/module/avatar-group/types.d.ts.map +1 -0
  90. package/lib/typescript/module/index.d.ts +4 -0
  91. package/lib/typescript/module/index.d.ts.map +1 -1
  92. package/lib/typescript/module/index.web.d.ts +4 -0
  93. package/lib/typescript/module/index.web.d.ts.map +1 -1
  94. package/lib/typescript/module/theme/color-scope/index.web.d.ts.map +1 -1
  95. package/lib/typescript/module/theme/color-scope/style-builder.d.ts +7 -5
  96. package/lib/typescript/module/theme/color-scope/style-builder.d.ts.map +1 -1
  97. package/lib/typescript/module/user-hover-card/UserHoverCard.d.ts +4 -0
  98. package/lib/typescript/module/user-hover-card/UserHoverCard.d.ts.map +1 -0
  99. package/lib/typescript/module/user-hover-card/index.d.ts +3 -0
  100. package/lib/typescript/module/user-hover-card/index.d.ts.map +1 -0
  101. package/lib/typescript/module/user-hover-card/types.d.ts +37 -0
  102. package/lib/typescript/module/user-hover-card/types.d.ts.map +1 -0
  103. package/package.json +30 -1
  104. package/src/__tests__/BloomColorScope.test.tsx +12 -4
  105. package/src/avatar-group/AvatarGroup.stories.tsx +135 -0
  106. package/src/avatar-group/AvatarGroup.tsx +19 -0
  107. package/src/avatar-group/AvatarGroup.web.tsx +145 -0
  108. package/src/avatar-group/AvatarGroupBase.tsx +249 -0
  109. package/src/avatar-group/index.ts +2 -0
  110. package/src/avatar-group/index.web.tsx +2 -0
  111. package/src/avatar-group/types.ts +64 -0
  112. package/src/index.ts +4 -0
  113. package/src/index.web.ts +4 -0
  114. package/src/theme/color-scope/index.web.tsx +6 -7
  115. package/src/theme/color-scope/style-builder.ts +19 -7
  116. package/src/user-hover-card/UserHoverCard.stories.tsx +113 -0
  117. package/src/user-hover-card/UserHoverCard.tsx +204 -0
  118. package/src/user-hover-card/index.ts +2 -0
  119. package/src/user-hover-card/types.ts +38 -0
@@ -1,7 +1,7 @@
1
1
  import type React from 'react';
2
2
  import { Platform, type StyleProp, type ViewStyle } from 'react-native';
3
3
 
4
- import { getResolvedTokens } from '../token-registry';
4
+ import { CANONICAL_TOKENS, getResolvedTokens } from '../token-registry';
5
5
  import type { AppColorName } from '../color-presets';
6
6
  import { lazyRequire } from '../../utils/lazy-require';
7
7
 
@@ -25,17 +25,29 @@ const getNativeWindVars = lazyRequire<NativeWindVarsModule>('nativewind');
25
25
 
26
26
  /**
27
27
  * Build the CSS custom-property map for a preset, ready to be applied to a
28
- * subtree. Every token is resolved to an sRGB `rgb(...)` string via
29
- * `getResolvedTokens` — the single canonical token pipeline shared by web and
30
- * native — so Tailwind v4 `@theme` utilities (e.g. `bg-background`) and
31
- * `color-mix`-based alpha utilities (`bg-primary/10`) both resolve against the
32
- * same color values. Keys retain the leading `--`.
28
+ * subtree. Every canonical `--x` token is resolved to an sRGB `rgb(...)` string
29
+ * via `getResolvedTokens` — the single token pipeline shared by web and native.
30
+ *
31
+ * The returned map also includes Tailwind v4 `--color-x` aliases generated from
32
+ * those same canonical values. Scoped aliases are required on web because
33
+ * inherited custom properties like `--color-primary: var(--primary)` compute at
34
+ * the document root and would otherwise keep pointing at the app-wide preset.
33
35
  */
34
36
  export function buildScopeVars(
35
37
  colorPreset: AppColorName,
36
38
  mode: 'light' | 'dark',
37
39
  ): Record<string, string> {
38
- return getResolvedTokens(colorPreset, mode);
40
+ const tokens = getResolvedTokens(colorPreset, mode);
41
+ const vars: Record<string, string> = { ...tokens };
42
+
43
+ for (const token of CANONICAL_TOKENS) {
44
+ const value = tokens[`--${token}`];
45
+ if (value !== undefined) {
46
+ vars[`--color-${token}`] = value;
47
+ }
48
+ }
49
+
50
+ return vars;
39
51
  }
40
52
 
41
53
  /**
@@ -0,0 +1,113 @@
1
+ import React from 'react';
2
+ import { Pressable, Text, View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { UserHoverCard } from './UserHoverCard';
6
+
7
+ const SAMPLE_AVATAR =
8
+ 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200&h=200&fit=crop';
9
+
10
+ function DemoFollowButton() {
11
+ return (
12
+ <Pressable
13
+ style={{
14
+ paddingHorizontal: 14,
15
+ paddingVertical: 7,
16
+ borderRadius: 999,
17
+ backgroundColor: '#1A73E8',
18
+ }}
19
+ >
20
+ <Text style={{ color: '#fff', fontWeight: '600', fontSize: 13 }}>
21
+ Follow
22
+ </Text>
23
+ </Pressable>
24
+ );
25
+ }
26
+
27
+ const meta: Meta<typeof UserHoverCard> = {
28
+ title: 'Components/UserHoverCard',
29
+ component: UserHoverCard,
30
+ argTypes: {
31
+ verified: { control: 'boolean' },
32
+ },
33
+ };
34
+
35
+ export default meta;
36
+
37
+ type Story = StoryObj<typeof UserHoverCard>;
38
+
39
+ export const Basic: Story = {
40
+ args: {
41
+ avatar: SAMPLE_AVATAR,
42
+ displayName: 'Nate Isern',
43
+ username: 'nate',
44
+ bio: 'Building the Oxy ecosystem. Designer, engineer, and occasional rose grower.',
45
+ stats: [
46
+ { label: 'Following', value: '312' },
47
+ { label: 'Followers', value: '4.8K' },
48
+ ],
49
+ verified: true,
50
+ },
51
+ };
52
+
53
+ export const WithAction: Story = {
54
+ args: {
55
+ avatar: SAMPLE_AVATAR,
56
+ displayName: 'Ada Lovelace',
57
+ username: 'ada',
58
+ bio: 'Mathematician. Wrote the first algorithm intended for a machine.',
59
+ stats: [
60
+ { label: 'Following', value: '12' },
61
+ { label: 'Followers', value: '1.2M' },
62
+ ],
63
+ verified: true,
64
+ action: <DemoFollowButton />,
65
+ },
66
+ name: 'With action slot',
67
+ };
68
+
69
+ export const NoAction: Story = {
70
+ args: {
71
+ avatar: null,
72
+ displayName: 'Grace Hopper',
73
+ username: 'grace',
74
+ bio: 'Computer scientist and US Navy rear admiral.',
75
+ stats: [
76
+ { label: 'Following', value: '88' },
77
+ { label: 'Followers', value: '900K' },
78
+ ],
79
+ },
80
+ name: 'No action slot',
81
+ };
82
+
83
+ export const Minimal: Story = {
84
+ args: {
85
+ displayName: 'Anonymous',
86
+ },
87
+ name: 'Minimal (name only)',
88
+ };
89
+
90
+ export const Pressable_: Story = {
91
+ render: (args) => (
92
+ <View style={{ gap: 8 }}>
93
+ <UserHoverCard
94
+ {...args}
95
+ avatar={SAMPLE_AVATAR}
96
+ displayName="Linus Torvalds"
97
+ username="linus"
98
+ bio="Creator of Linux and Git."
99
+ stats={[
100
+ { label: 'Following', value: '3' },
101
+ { label: 'Followers', value: '2.1M' },
102
+ ]}
103
+ action={<DemoFollowButton />}
104
+ onPressProfile={() => {
105
+ // eslint-disable-next-line no-alert
106
+ if (typeof window !== 'undefined') window.alert('open profile');
107
+ }}
108
+ />
109
+ <Text>Click the identity area to open the profile.</Text>
110
+ </View>
111
+ ),
112
+ name: 'Pressable identity',
113
+ };
@@ -0,0 +1,204 @@
1
+ import React, { memo, useMemo } from 'react';
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+
11
+ import { Avatar } from '../avatar';
12
+ import { VerifiedCheck } from '../icons/VerifiedCheck';
13
+ import { useTheme } from '../theme/use-theme';
14
+ import { borderRadius, fontSize, space } from '../styles/tokens';
15
+ import type { UserHoverCardProps } from './types';
16
+
17
+ const AVATAR_SIZE = 48;
18
+ const VERIFIED_SIZE = 16;
19
+
20
+ const UserHoverCardComponent: React.FC<UserHoverCardProps> = ({
21
+ avatar,
22
+ displayName,
23
+ username,
24
+ bio,
25
+ stats,
26
+ verified = false,
27
+ onPressProfile,
28
+ action,
29
+ style,
30
+ testID,
31
+ }) => {
32
+ const theme = useTheme();
33
+
34
+ const cardStyle = useMemo((): ViewStyle => {
35
+ const base: ViewStyle = {
36
+ backgroundColor: theme.colors.card,
37
+ borderRadius: borderRadius.lg,
38
+ borderWidth: StyleSheet.hairlineWidth,
39
+ borderColor: theme.colors.border,
40
+ padding: space.lg,
41
+ width: 280,
42
+ };
43
+
44
+ if (Platform.OS === 'web') {
45
+ base.boxShadow = `0px 8px 24px ${theme.colors.shadow}`;
46
+ } else {
47
+ base.shadowColor = theme.colors.shadow;
48
+ base.shadowOffset = { width: 0, height: 8 };
49
+ base.shadowOpacity = 0.18;
50
+ base.shadowRadius = 16;
51
+ base.elevation = 8;
52
+ }
53
+
54
+ return base;
55
+ }, [theme]);
56
+
57
+ const identity = (
58
+ <View style={styles.identityRow}>
59
+ <Avatar uri={avatar ?? undefined} name={displayName} size={AVATAR_SIZE} />
60
+ <View style={styles.identityText}>
61
+ <View style={styles.nameRow}>
62
+ <Text
63
+ numberOfLines={1}
64
+ style={[styles.displayName, { color: theme.colors.text }]}
65
+ >
66
+ {displayName}
67
+ </Text>
68
+ {verified && (
69
+ <VerifiedCheck
70
+ size="sm"
71
+ fill={theme.colors.primary}
72
+ width={VERIFIED_SIZE}
73
+ height={VERIFIED_SIZE}
74
+ style={styles.verifiedBadge}
75
+ />
76
+ )}
77
+ </View>
78
+ {username ? (
79
+ <Text
80
+ numberOfLines={1}
81
+ style={[styles.username, { color: theme.colors.textSecondary }]}
82
+ >
83
+ @{username}
84
+ </Text>
85
+ ) : null}
86
+ </View>
87
+ </View>
88
+ );
89
+
90
+ return (
91
+ <View style={[cardStyle, style]} testID={testID}>
92
+ <View style={styles.header}>
93
+ {onPressProfile ? (
94
+ <Pressable
95
+ onPress={onPressProfile}
96
+ accessibilityRole="button"
97
+ accessibilityLabel={
98
+ username ? `${displayName} (@${username})` : displayName
99
+ }
100
+ style={styles.identityPressable}
101
+ >
102
+ {identity}
103
+ </Pressable>
104
+ ) : (
105
+ <View style={styles.identityPressable}>{identity}</View>
106
+ )}
107
+ {action != null ? <View style={styles.action}>{action}</View> : null}
108
+ </View>
109
+
110
+ {bio ? (
111
+ <Text
112
+ numberOfLines={3}
113
+ style={[styles.bio, { color: theme.colors.text }]}
114
+ >
115
+ {bio}
116
+ </Text>
117
+ ) : null}
118
+
119
+ {stats && stats.length > 0 ? (
120
+ <View style={styles.statsRow}>
121
+ {stats.map((stat) => (
122
+ <View key={stat.label} style={styles.stat}>
123
+ <Text style={[styles.statValue, { color: theme.colors.text }]}>
124
+ {stat.value}
125
+ </Text>
126
+ <Text
127
+ style={[styles.statLabel, { color: theme.colors.textSecondary }]}
128
+ >
129
+ {stat.label}
130
+ </Text>
131
+ </View>
132
+ ))}
133
+ </View>
134
+ ) : null}
135
+ </View>
136
+ );
137
+ };
138
+
139
+ const styles = StyleSheet.create({
140
+ header: {
141
+ flexDirection: 'row',
142
+ alignItems: 'flex-start',
143
+ justifyContent: 'space-between',
144
+ gap: space.sm,
145
+ },
146
+ identityPressable: {
147
+ flexShrink: 1,
148
+ flexGrow: 1,
149
+ },
150
+ identityRow: {
151
+ flexDirection: 'row',
152
+ alignItems: 'center',
153
+ gap: space.sm,
154
+ },
155
+ identityText: {
156
+ flexShrink: 1,
157
+ flexGrow: 1,
158
+ },
159
+ nameRow: {
160
+ flexDirection: 'row',
161
+ alignItems: 'center',
162
+ gap: space.xs,
163
+ },
164
+ displayName: {
165
+ fontSize: fontSize.lg,
166
+ fontWeight: '700',
167
+ flexShrink: 1,
168
+ },
169
+ verifiedBadge: {
170
+ flexShrink: 0,
171
+ },
172
+ username: {
173
+ fontSize: fontSize.sm,
174
+ marginTop: space._2xs,
175
+ },
176
+ action: {
177
+ flexShrink: 0,
178
+ },
179
+ bio: {
180
+ fontSize: fontSize.sm,
181
+ lineHeight: fontSize.sm * 1.4,
182
+ marginTop: space.md,
183
+ },
184
+ statsRow: {
185
+ flexDirection: 'row',
186
+ gap: space.lg,
187
+ marginTop: space.md,
188
+ },
189
+ stat: {
190
+ flexDirection: 'row',
191
+ alignItems: 'baseline',
192
+ gap: space.xs,
193
+ },
194
+ statValue: {
195
+ fontSize: fontSize.sm,
196
+ fontWeight: '700',
197
+ },
198
+ statLabel: {
199
+ fontSize: fontSize.sm,
200
+ },
201
+ });
202
+
203
+ export const UserHoverCard = memo(UserHoverCardComponent);
204
+ UserHoverCard.displayName = 'UserHoverCard';
@@ -0,0 +1,2 @@
1
+ export { UserHoverCard } from './UserHoverCard';
2
+ export type { UserHoverCardProps, UserHoverCardStat } from './types';
@@ -0,0 +1,38 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+
4
+ /** A single labeled metric rendered in the {@link UserHoverCardProps.stats} row. */
5
+ export interface UserHoverCardStat {
6
+ /** Human-readable label, e.g. `"Followers"`. */
7
+ label: string;
8
+ /** Pre-formatted value, e.g. `"1.2K"` or `42`. */
9
+ value: string | number;
10
+ }
11
+
12
+ export interface UserHoverCardProps {
13
+ /**
14
+ * Avatar image source — a URL string, or null/undefined to fall back to the
15
+ * deterministic name-based placeholder rendered by {@link Avatar}.
16
+ */
17
+ avatar?: string | null;
18
+ /** Canonical, already-resolved display name (an API contract — render directly). */
19
+ displayName: string;
20
+ /** Handle without the leading `@` (the component prefixes it). */
21
+ username?: string;
22
+ /** Optional short biography; truncated to a few lines. */
23
+ bio?: string;
24
+ /** Optional metrics row (e.g. Following / Followers). */
25
+ stats?: UserHoverCardStat[];
26
+ /** Whether to show the verified badge next to the display name. */
27
+ verified?: boolean;
28
+ /** Makes the identity area (avatar + name + handle) pressable. */
29
+ onPressProfile?: () => void;
30
+ /**
31
+ * Slot for a consumer-injected action — typically the app's SDK FollowButton.
32
+ * Bloom does NOT implement follow logic; it only renders the card.
33
+ */
34
+ action?: ReactNode;
35
+ /** Container style override. */
36
+ style?: StyleProp<ViewStyle>;
37
+ testID?: string;
38
+ }