@ankhorage/zora 1.4.8 → 1.4.10

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 (67) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +222 -0
  3. package/dist/components/card/meta.d.ts +1 -1
  4. package/dist/foundation/meta.d.ts +4 -4
  5. package/dist/index.d.ts +4 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/layout/auth-layout/meta.d.ts +1 -1
  10. package/dist/layout/page/meta.d.ts +1 -1
  11. package/dist/layout/page-section/meta.d.ts +1 -1
  12. package/dist/metadata/allowedChildren.d.ts +3 -3
  13. package/dist/metadata/allowedChildren.d.ts.map +1 -1
  14. package/dist/metadata/allowedChildren.js +2 -0
  15. package/dist/metadata/allowedChildren.js.map +1 -1
  16. package/dist/metadata/componentMeta.d.ts.map +1 -1
  17. package/dist/metadata/componentMeta.js +4 -0
  18. package/dist/metadata/componentMeta.js.map +1 -1
  19. package/dist/patterns/chat-list-item/ChatListItem.d.ts +4 -0
  20. package/dist/patterns/chat-list-item/ChatListItem.d.ts.map +1 -0
  21. package/dist/patterns/chat-list-item/ChatListItem.js +110 -0
  22. package/dist/patterns/chat-list-item/ChatListItem.js.map +1 -0
  23. package/dist/patterns/chat-list-item/index.d.ts +3 -0
  24. package/dist/patterns/chat-list-item/index.d.ts.map +1 -0
  25. package/dist/patterns/chat-list-item/index.js +2 -0
  26. package/dist/patterns/chat-list-item/index.js.map +1 -0
  27. package/dist/patterns/chat-list-item/meta.d.ts +74 -0
  28. package/dist/patterns/chat-list-item/meta.d.ts.map +1 -0
  29. package/dist/patterns/chat-list-item/meta.js +72 -0
  30. package/dist/patterns/chat-list-item/meta.js.map +1 -0
  31. package/dist/patterns/chat-list-item/types.d.ts +31 -0
  32. package/dist/patterns/chat-list-item/types.d.ts.map +1 -0
  33. package/dist/patterns/chat-list-item/types.js +2 -0
  34. package/dist/patterns/chat-list-item/types.js.map +1 -0
  35. package/dist/patterns/message-bubble/MessageBubble.d.ts +4 -0
  36. package/dist/patterns/message-bubble/MessageBubble.d.ts.map +1 -0
  37. package/dist/patterns/message-bubble/MessageBubble.js +126 -0
  38. package/dist/patterns/message-bubble/MessageBubble.js.map +1 -0
  39. package/dist/patterns/message-bubble/index.d.ts +3 -0
  40. package/dist/patterns/message-bubble/index.d.ts.map +1 -0
  41. package/dist/patterns/message-bubble/index.js +2 -0
  42. package/dist/patterns/message-bubble/index.js.map +1 -0
  43. package/dist/patterns/message-bubble/meta.d.ts +67 -0
  44. package/dist/patterns/message-bubble/meta.d.ts.map +1 -0
  45. package/dist/patterns/message-bubble/meta.js +66 -0
  46. package/dist/patterns/message-bubble/meta.js.map +1 -0
  47. package/dist/patterns/message-bubble/types.d.ts +40 -0
  48. package/dist/patterns/message-bubble/types.d.ts.map +1 -0
  49. package/dist/patterns/message-bubble/types.js +2 -0
  50. package/dist/patterns/message-bubble/types.js.map +1 -0
  51. package/dist/patterns/notice/meta.d.ts +1 -1
  52. package/dist/patterns/panel/meta.d.ts +1 -1
  53. package/dist/patterns/post-card/meta.d.ts +1 -1
  54. package/package.json +1 -1
  55. package/src/index.ts +10 -0
  56. package/src/metadata/allowedChildren.ts +2 -0
  57. package/src/metadata/componentMeta.test.ts +2 -0
  58. package/src/metadata/componentMeta.ts +4 -0
  59. package/src/patterns/chat-list-item/ChatListItem.test.tsx +11 -0
  60. package/src/patterns/chat-list-item/ChatListItem.tsx +219 -0
  61. package/src/patterns/chat-list-item/index.ts +2 -0
  62. package/src/patterns/chat-list-item/meta.ts +74 -0
  63. package/src/patterns/chat-list-item/types.ts +33 -0
  64. package/src/patterns/message-bubble/MessageBubble.tsx +261 -0
  65. package/src/patterns/message-bubble/index.ts +8 -0
  66. package/src/patterns/message-bubble/meta.ts +68 -0
  67. package/src/patterns/message-bubble/types.ts +43 -0
@@ -0,0 +1,33 @@
1
+ import type React from 'react';
2
+ import type { ImageSourcePropType } from 'react-native';
3
+
4
+ import type { AvatarShape, AvatarSize } from '../../components/avatar';
5
+ import type { ZoraTone } from '../../internal/recipes';
6
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
7
+
8
+ export interface ChatListAvatar {
9
+ source?: ImageSourcePropType;
10
+ name?: string;
11
+ initials?: string;
12
+ label?: string;
13
+ size?: AvatarSize;
14
+ shape?: AvatarShape;
15
+ tone?: ZoraTone;
16
+ }
17
+
18
+ export interface ChatListItemProps extends ZoraBaseProps {
19
+ title: React.ReactNode;
20
+ preview?: React.ReactNode;
21
+ meta?: React.ReactNode;
22
+ timestamp?: React.ReactNode;
23
+ avatar?: ChatListAvatar;
24
+ leading?: React.ReactNode;
25
+ trailing?: React.ReactNode;
26
+ unread?: boolean;
27
+ unreadCount?: React.ReactNode;
28
+ selected?: boolean;
29
+ disabled?: boolean;
30
+ compact?: boolean;
31
+ accessibilityLabel?: string;
32
+ onPress?: () => void;
33
+ }
@@ -0,0 +1,261 @@
1
+ import { ButtonBase } from '@ankhorage/surface';
2
+ import React from 'react';
3
+
4
+ import { Avatar } from '../../components/avatar';
5
+ import { Text } from '../../components/text';
6
+ import { Box, Inline, Stack } from '../../foundation';
7
+ import { useZoraTheme } from '../../theme/useZoraTheme';
8
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
9
+ import type {
10
+ MessageBubbleAvatar,
11
+ MessageBubbleDirection,
12
+ MessageBubbleProps,
13
+ MessageBubbleStatus,
14
+ } from './types';
15
+
16
+ function resolveAvatarName({
17
+ avatar,
18
+ authorName,
19
+ }: {
20
+ avatar: MessageBubbleAvatar | undefined;
21
+ authorName: React.ReactNode | undefined;
22
+ }): string | undefined {
23
+ if (avatar?.name) return avatar.name;
24
+ return typeof authorName === 'string' ? authorName : undefined;
25
+ }
26
+
27
+ function resolvePadding(compact: boolean) {
28
+ return compact ? { px: 'm' as const, py: 's' as const } : { px: 'm' as const, py: 'm' as const };
29
+ }
30
+
31
+ function resolveStatusLabel(status: MessageBubbleStatus): string {
32
+ switch (status) {
33
+ case 'sending':
34
+ return 'Sending';
35
+ case 'sent':
36
+ return 'Sent';
37
+ case 'delivered':
38
+ return 'Delivered';
39
+ case 'read':
40
+ return 'Read';
41
+ case 'failed':
42
+ return 'Failed';
43
+ default:
44
+ return status;
45
+ }
46
+ }
47
+
48
+ function isMessageBubbleStatus(
49
+ status: MessageBubbleProps['status'],
50
+ ): status is MessageBubbleStatus {
51
+ return (
52
+ status === 'sending' ||
53
+ status === 'sent' ||
54
+ status === 'delivered' ||
55
+ status === 'read' ||
56
+ status === 'failed'
57
+ );
58
+ }
59
+
60
+ function resolveStatus(status: MessageBubbleProps['status']) {
61
+ if (status == null) return null;
62
+ return isMessageBubbleStatus(status) ? resolveStatusLabel(status) : status;
63
+ }
64
+
65
+ function resolveBubbleStyles({
66
+ direction,
67
+ disabled,
68
+ hovered,
69
+ pressed,
70
+ selected,
71
+ theme,
72
+ }: {
73
+ direction: MessageBubbleDirection;
74
+ disabled: boolean;
75
+ hovered: boolean;
76
+ pressed: boolean;
77
+ selected: boolean;
78
+ theme: ReturnType<typeof useZoraTheme>['theme'];
79
+ }) {
80
+ const borderColor = selected ? theme.semantics.border.focus : theme.semantics.border.default;
81
+
82
+ if (direction === 'system') {
83
+ return {
84
+ bg: selected ? theme.semantics.neutral.surface : 'transparent',
85
+ borderColor,
86
+ borderWidth: selected ? 1 : 0,
87
+ opacity: disabled ? 0.72 : 1,
88
+ };
89
+ }
90
+
91
+ const interactiveBg = pressed
92
+ ? theme.semantics.neutral.surfaceActive
93
+ : hovered
94
+ ? theme.semantics.neutral.surfaceHover
95
+ : undefined;
96
+
97
+ return {
98
+ bg:
99
+ interactiveBg ??
100
+ (direction === 'outgoing' ? theme.semantics.neutral.surface : theme.semantics.surface.raised),
101
+ borderColor,
102
+ borderWidth: selected ? 1 : 0,
103
+ opacity: disabled ? 0.72 : 1,
104
+ };
105
+ }
106
+
107
+ function MessageAvatar({
108
+ avatar,
109
+ authorName,
110
+ compact,
111
+ }: {
112
+ avatar: MessageBubbleAvatar;
113
+ authorName: React.ReactNode | undefined;
114
+ compact: boolean;
115
+ }) {
116
+ const avatarName = resolveAvatarName({ avatar, authorName });
117
+
118
+ return (
119
+ <Avatar
120
+ initials={avatar.initials}
121
+ label={avatar.label ?? avatarName}
122
+ name={avatarName}
123
+ shape={avatar.shape}
124
+ size={avatar.size ?? (compact ? 'xs' : 's')}
125
+ source={avatar.source}
126
+ tone={avatar.tone}
127
+ />
128
+ );
129
+ }
130
+
131
+ function MessageBubbleInner({
132
+ themeId: _themeId,
133
+ mode: _mode,
134
+ testID,
135
+ direction = 'incoming',
136
+ text,
137
+ children,
138
+ author,
139
+ timestamp,
140
+ meta,
141
+ status,
142
+ leading,
143
+ trailing,
144
+ footer,
145
+ selected = false,
146
+ disabled = false,
147
+ compact = false,
148
+ accessibilityLabel,
149
+ onPress,
150
+ }: MessageBubbleProps) {
151
+ const { theme } = useZoraTheme();
152
+ const padding = resolvePadding(compact);
153
+ const isInteractive = Boolean(onPress);
154
+ const isOutgoing = direction === 'outgoing';
155
+ const isSystem = direction === 'system';
156
+ const renderedStatus = resolveStatus(status);
157
+ const authorName = author?.name;
158
+ const authorAvatar = author?.avatar;
159
+ const hasAuthorName = authorName != null && !isOutgoing && !isSystem;
160
+ const hasAvatar = authorAvatar != null && !isOutgoing && !isSystem;
161
+ const hasMetaRow = timestamp != null || meta != null || renderedStatus != null;
162
+
163
+ const renderBubble = ({ pressed, hovered }: { pressed: boolean; hovered: boolean }) => {
164
+ const styles = resolveBubbleStyles({ direction, disabled, hovered, pressed, selected, theme });
165
+
166
+ return (
167
+ <Box
168
+ bg={styles.bg}
169
+ borderColor={styles.borderColor}
170
+ borderWidth={styles.borderWidth}
171
+ px={padding.px}
172
+ py={padding.py}
173
+ radius={isSystem ? 'm' : 'l'}
174
+ style={{ maxWidth: compact ? 420 : 560, opacity: styles.opacity }}
175
+ >
176
+ <Stack align={isSystem ? 'center' : 'flex-start'} gap={compact ? 'xxs' : 'xs'}>
177
+ {hasAuthorName ? (
178
+ <Text tone="muted" variant="caption" weight="semiBold">
179
+ {authorName}
180
+ </Text>
181
+ ) : null}
182
+ {text != null ? (
183
+ <Text align={isSystem ? 'center' : undefined} tone={disabled ? 'muted' : 'default'}>
184
+ {text}
185
+ </Text>
186
+ ) : null}
187
+ {children != null ? <Box width="100%">{children}</Box> : null}
188
+ {hasMetaRow ? (
189
+ <Inline
190
+ align="center"
191
+ gap="xs"
192
+ justify={isOutgoing ? 'flex-end' : isSystem ? 'center' : 'flex-start'}
193
+ wrap="wrap"
194
+ >
195
+ {timestamp != null ? (
196
+ <Text tone="subtle" variant="caption">
197
+ {timestamp}
198
+ </Text>
199
+ ) : null}
200
+ {meta != null ? (
201
+ <Text tone="subtle" variant="caption">
202
+ {meta}
203
+ </Text>
204
+ ) : null}
205
+ {renderedStatus != null ? (
206
+ <Text tone={status === 'failed' ? 'danger' : 'subtle'} variant="caption">
207
+ {renderedStatus}
208
+ </Text>
209
+ ) : null}
210
+ </Inline>
211
+ ) : null}
212
+ </Stack>
213
+ </Box>
214
+ );
215
+ };
216
+
217
+ const bubbleContent = isInteractive ? (
218
+ <ButtonBase
219
+ accessibilityLabel={accessibilityLabel}
220
+ accessibilityRole="button"
221
+ accessibilityState={{ disabled, selected }}
222
+ disabled={disabled}
223
+ onPress={onPress}
224
+ radius={isSystem ? 'm' : 'l'}
225
+ testID={testID}
226
+ >
227
+ {(state) => renderBubble({ pressed: state.pressed, hovered: state.hovered })}
228
+ </ButtonBase>
229
+ ) : (
230
+ <Box testID={testID}>{renderBubble({ pressed: false, hovered: false })}</Box>
231
+ );
232
+
233
+ return (
234
+ <Stack gap="xs" style={{ width: '100%' }}>
235
+ <Inline
236
+ align="flex-end"
237
+ gap="s"
238
+ justify={isSystem ? 'center' : isOutgoing ? 'flex-end' : 'flex-start'}
239
+ wrap="nowrap"
240
+ >
241
+ {!isOutgoing && !isSystem ? (
242
+ <Box>
243
+ {leading ??
244
+ (hasAvatar ? (
245
+ <MessageAvatar avatar={authorAvatar} authorName={authorName} compact={compact} />
246
+ ) : null)}
247
+ </Box>
248
+ ) : null}
249
+ {isOutgoing && trailing ? <Box>{trailing}</Box> : null}
250
+ {bubbleContent}
251
+ {isOutgoing && leading ? <Box>{leading}</Box> : null}
252
+ {!isOutgoing && !isSystem && trailing ? <Box>{trailing}</Box> : null}
253
+ </Inline>
254
+ {footer != null ? (
255
+ <Box alignSelf={isOutgoing ? 'flex-end' : isSystem ? 'center' : 'flex-start'}>{footer}</Box>
256
+ ) : null}
257
+ </Stack>
258
+ );
259
+ }
260
+
261
+ export const MessageBubble = withZoraThemeScope(MessageBubbleInner);
@@ -0,0 +1,8 @@
1
+ export { MessageBubble } from './MessageBubble';
2
+ export type {
3
+ MessageBubbleAuthor,
4
+ MessageBubbleAvatar,
5
+ MessageBubbleDirection,
6
+ MessageBubbleProps,
7
+ MessageBubbleStatus,
8
+ } from './types';
@@ -0,0 +1,68 @@
1
+ import type { ZoraComponentMeta } from '../../metadata';
2
+ import { CONTAINER_ALLOWED_CHILDREN } from '../../metadata/allowedChildren';
3
+
4
+ export const messageBubbleMeta = {
5
+ name: 'MessageBubble',
6
+ category: 'pattern',
7
+ description:
8
+ 'Chat/message bubble with direction, author, text, meta, and delivery status presentation.',
9
+ directManifestNode: true,
10
+ allowedChildren: [...CONTAINER_ALLOWED_CHILDREN],
11
+ blueprint: {
12
+ label: 'Message bubble',
13
+ icon: { name: 'chatbubble-outline' },
14
+ defaultProps: {
15
+ direction: 'incoming',
16
+ text: 'Can you review the latest message pattern?',
17
+ timestamp: '10:41',
18
+ },
19
+ },
20
+ props: {
21
+ direction: {
22
+ type: 'enum',
23
+ category: 'Content',
24
+ label: 'Direction',
25
+ enum: ['incoming', 'outgoing', 'system'],
26
+ default: 'incoming',
27
+ },
28
+ text: {
29
+ type: 'string',
30
+ category: 'Content',
31
+ label: 'Text',
32
+ },
33
+ timestamp: {
34
+ type: 'string',
35
+ category: 'Content',
36
+ label: 'Timestamp',
37
+ },
38
+ meta: {
39
+ type: 'string',
40
+ category: 'Content',
41
+ label: 'Meta',
42
+ },
43
+ status: {
44
+ type: 'enum',
45
+ category: 'Content',
46
+ label: 'Status',
47
+ enum: ['sending', 'sent', 'delivered', 'read', 'failed'],
48
+ },
49
+ selected: {
50
+ type: 'boolean',
51
+ category: 'State',
52
+ label: 'Selected',
53
+ default: false,
54
+ },
55
+ disabled: {
56
+ type: 'boolean',
57
+ category: 'State',
58
+ label: 'Disabled',
59
+ default: false,
60
+ },
61
+ compact: {
62
+ type: 'boolean',
63
+ category: 'Layout',
64
+ label: 'Compact',
65
+ default: false,
66
+ },
67
+ },
68
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,43 @@
1
+ import type React from 'react';
2
+ import type { ImageSourcePropType } from 'react-native';
3
+
4
+ import type { AvatarShape, AvatarSize } from '../../components/avatar';
5
+ import type { ZoraTone } from '../../internal/recipes';
6
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
7
+
8
+ export type MessageBubbleDirection = 'incoming' | 'outgoing' | 'system';
9
+ export type MessageBubbleStatus = 'sending' | 'sent' | 'delivered' | 'read' | 'failed';
10
+ type MessageBubbleStatusContent = MessageBubbleStatus | Exclude<React.ReactNode, string>;
11
+
12
+ export interface MessageBubbleAvatar {
13
+ source?: ImageSourcePropType;
14
+ name?: string;
15
+ initials?: string;
16
+ label?: string;
17
+ size?: AvatarSize;
18
+ shape?: AvatarShape;
19
+ tone?: ZoraTone;
20
+ }
21
+
22
+ export interface MessageBubbleAuthor {
23
+ name?: React.ReactNode;
24
+ avatar?: MessageBubbleAvatar;
25
+ }
26
+
27
+ export interface MessageBubbleProps extends ZoraBaseProps {
28
+ direction?: MessageBubbleDirection;
29
+ text?: React.ReactNode;
30
+ children?: React.ReactNode;
31
+ author?: MessageBubbleAuthor;
32
+ timestamp?: React.ReactNode;
33
+ meta?: React.ReactNode;
34
+ status?: MessageBubbleStatusContent;
35
+ leading?: React.ReactNode;
36
+ trailing?: React.ReactNode;
37
+ footer?: React.ReactNode;
38
+ selected?: boolean;
39
+ disabled?: boolean;
40
+ compact?: boolean;
41
+ accessibilityLabel?: string;
42
+ onPress?: () => void;
43
+ }