@ankhorage/zora 1.4.6 → 1.4.8

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 (50) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +143 -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 +2 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -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 +1 -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 +2 -0
  18. package/dist/metadata/componentMeta.js.map +1 -1
  19. package/dist/patterns/hero/Hero.d.ts.map +1 -1
  20. package/dist/patterns/hero/Hero.js +11 -15
  21. package/dist/patterns/hero/Hero.js.map +1 -1
  22. package/dist/patterns/notice/meta.d.ts +1 -1
  23. package/dist/patterns/panel/meta.d.ts +1 -1
  24. package/dist/patterns/post-card/PostCard.d.ts +4 -0
  25. package/dist/patterns/post-card/PostCard.d.ts.map +1 -0
  26. package/dist/patterns/post-card/PostCard.js +133 -0
  27. package/dist/patterns/post-card/PostCard.js.map +1 -0
  28. package/dist/patterns/post-card/index.d.ts +3 -0
  29. package/dist/patterns/post-card/index.d.ts.map +1 -0
  30. package/dist/patterns/post-card/index.js +2 -0
  31. package/dist/patterns/post-card/index.js.map +1 -0
  32. package/dist/patterns/post-card/meta.d.ts +64 -0
  33. package/dist/patterns/post-card/meta.d.ts.map +1 -0
  34. package/dist/patterns/post-card/meta.js +66 -0
  35. package/dist/patterns/post-card/meta.js.map +1 -0
  36. package/dist/patterns/post-card/types.d.ts +64 -0
  37. package/dist/patterns/post-card/types.d.ts.map +1 -0
  38. package/dist/patterns/post-card/types.js +2 -0
  39. package/dist/patterns/post-card/types.js.map +1 -0
  40. package/package.json +1 -1
  41. package/src/index.ts +9 -0
  42. package/src/metadata/allowedChildren.ts +1 -0
  43. package/src/metadata/componentMeta.test.ts +1 -0
  44. package/src/metadata/componentMeta.ts +2 -0
  45. package/src/patterns/hero/Hero.tsx +18 -18
  46. package/src/patterns/post-card/PostCard.test.tsx +11 -0
  47. package/src/patterns/post-card/PostCard.tsx +234 -0
  48. package/src/patterns/post-card/index.ts +9 -0
  49. package/src/patterns/post-card/meta.ts +68 -0
  50. package/src/patterns/post-card/types.ts +71 -0
@@ -0,0 +1,11 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { ZORA_COMPONENT_META } from '../../metadata';
4
+
5
+ describe('PostCard', () => {
6
+ test('is registered as a public ZORA pattern', () => {
7
+ expect(ZORA_COMPONENT_META.PostCard?.name).toBe('PostCard');
8
+ expect(ZORA_COMPONENT_META.PostCard?.category).toBe('pattern');
9
+ expect(ZORA_COMPONENT_META.PostCard?.directManifestNode).toBe(true);
10
+ });
11
+ });
@@ -0,0 +1,234 @@
1
+ import React from 'react';
2
+ import { Image as ReactNativeImage } from 'react-native';
3
+
4
+ import { Avatar } from '../../components/avatar';
5
+ import { Button } from '../../components/button';
6
+ import { Card } from '../../components/card';
7
+ import { Text } from '../../components/text';
8
+ import { Box, Divider, Inline, Stack } from '../../foundation';
9
+ import { useZoraTheme } from '../../theme/useZoraTheme';
10
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
11
+ import type {
12
+ PostAction,
13
+ PostAuthor,
14
+ PostCardMedia,
15
+ PostCardProps,
16
+ PostCommentPreview,
17
+ } from './types';
18
+
19
+ function resolveAuthorName(author: PostAuthor): string | undefined {
20
+ return typeof author.name === 'string' ? author.name : author.avatar?.name;
21
+ }
22
+
23
+ function resolveMediaAspectRatio(aspectRatio: number | undefined): number {
24
+ if (aspectRatio === undefined || !Number.isFinite(aspectRatio) || aspectRatio <= 0) {
25
+ return 16 / 9;
26
+ }
27
+
28
+ return aspectRatio;
29
+ }
30
+
31
+ function isPostCardMediaList(
32
+ media: NonNullable<PostCardProps['media']>,
33
+ ): media is readonly PostCardMedia[] {
34
+ return Array.isArray(media);
35
+ }
36
+
37
+ function normalizeMedia(media: PostCardProps['media']): readonly PostCardMedia[] {
38
+ if (!media) {
39
+ return [];
40
+ }
41
+
42
+ if (isPostCardMediaList(media)) {
43
+ return media;
44
+ }
45
+
46
+ return [media];
47
+ }
48
+
49
+ function PostCardAuthor({ author, compact = false }: { author: PostAuthor; compact?: boolean }) {
50
+ const avatarName = resolveAuthorName(author);
51
+ const { avatar } = author;
52
+
53
+ return (
54
+ <Inline align="center" gap="s" wrap="nowrap">
55
+ <Avatar
56
+ initials={avatar?.initials}
57
+ label={avatar?.label ?? avatarName}
58
+ name={avatarName}
59
+ shape={avatar?.shape}
60
+ size={avatar?.size ?? (compact ? 's' : 'm')}
61
+ source={avatar?.source}
62
+ tone={avatar?.tone}
63
+ />
64
+ <Box flex={1}>
65
+ <Stack gap="xxs">
66
+ <Text variant="bodySmall" weight="semiBold">
67
+ {author.name}
68
+ </Text>
69
+ {author.subtitle ? (
70
+ <Text tone="muted" variant="caption">
71
+ {author.subtitle}
72
+ </Text>
73
+ ) : null}
74
+ </Stack>
75
+ </Box>
76
+ </Inline>
77
+ );
78
+ }
79
+
80
+ function PostCardMediaItem({ media }: { media: PostCardMedia }) {
81
+ const { theme } = useZoraTheme();
82
+ const aspectRatio = resolveMediaAspectRatio(media.aspectRatio);
83
+
84
+ if (!('source' in media)) {
85
+ return (
86
+ <Box radius="m" style={{ overflow: 'hidden' }}>
87
+ {media.children}
88
+ </Box>
89
+ );
90
+ }
91
+
92
+ return (
93
+ <Box bg={theme.semantics.neutral.surface} radius="m" style={{ overflow: 'hidden' }}>
94
+ <Box style={{ aspectRatio, width: '100%' }}>
95
+ <ReactNativeImage
96
+ accessibilityLabel={media.label}
97
+ source={media.source}
98
+ style={{ height: '100%', width: '100%' }}
99
+ />
100
+ </Box>
101
+ </Box>
102
+ );
103
+ }
104
+
105
+ function PostActionLabel({ action }: { action: PostAction }) {
106
+ if (!action.count) {
107
+ return <>{action.label}</>;
108
+ }
109
+
110
+ return (
111
+ <>
112
+ {action.label} {action.count}
113
+ </>
114
+ );
115
+ }
116
+
117
+ function PostCardActions({ actions }: { actions: readonly PostAction[] }) {
118
+ if (actions.length === 0) {
119
+ return null;
120
+ }
121
+
122
+ return (
123
+ <Inline align="center" gap="s" wrap="wrap">
124
+ {actions.map((action) => (
125
+ <Button
126
+ key={action.id}
127
+ disabled={action.disabled}
128
+ emphasis={action.selected ? 'soft' : 'ghost'}
129
+ leadingIcon={action.icon}
130
+ onPress={action.onPress}
131
+ size="s"
132
+ tone={action.selected ? 'primary' : 'neutral'}
133
+ >
134
+ <PostActionLabel action={action} />
135
+ </Button>
136
+ ))}
137
+ </Inline>
138
+ );
139
+ }
140
+
141
+ function PostCommentPreviewItem({ comment }: { comment: PostCommentPreview }) {
142
+ return (
143
+ <Inline align="flex-start" gap="s" wrap="nowrap">
144
+ {comment.author ? <PostCardAuthor author={comment.author} compact /> : null}
145
+ <Box flex={1}>
146
+ <Stack gap="xxs">
147
+ <Text variant="bodySmall">{comment.text}</Text>
148
+ {comment.meta ? (
149
+ <Text tone="subtle" variant="caption">
150
+ {comment.meta}
151
+ </Text>
152
+ ) : null}
153
+ {comment.action ? <Box>{comment.action}</Box> : null}
154
+ </Stack>
155
+ </Box>
156
+ </Inline>
157
+ );
158
+ }
159
+
160
+ function PostCardComments({ comments }: { comments: readonly PostCommentPreview[] }) {
161
+ if (comments.length === 0) {
162
+ return null;
163
+ }
164
+
165
+ return (
166
+ <Stack gap="s">
167
+ {comments.map((comment) => (
168
+ <PostCommentPreviewItem key={comment.id} comment={comment} />
169
+ ))}
170
+ </Stack>
171
+ );
172
+ }
173
+
174
+ function PostCardInner({
175
+ themeId: _themeId,
176
+ mode: _mode,
177
+ testID,
178
+ author,
179
+ text,
180
+ children,
181
+ media,
182
+ actions = [],
183
+ comments = [],
184
+ headerAction,
185
+ footer,
186
+ tone = 'default',
187
+ compact = false,
188
+ onPress,
189
+ }: PostCardProps) {
190
+ const mediaItems = normalizeMedia(media);
191
+ const gap = compact ? 's' : 'm';
192
+ const isInteractive = Boolean(onPress) && !headerAction;
193
+ const hasBody = text != null || children != null || mediaItems.length > 0;
194
+ const hasEngagement = actions.length > 0 || comments.length > 0;
195
+
196
+ return (
197
+ <Card
198
+ compact={compact}
199
+ onPress={isInteractive ? onPress : undefined}
200
+ testID={testID}
201
+ tone={tone}
202
+ >
203
+ <Stack gap={gap}>
204
+ <Inline align="center" gap="m" justify="space-between" wrap="nowrap">
205
+ <Box flex={1}>
206
+ <PostCardAuthor author={author} compact={compact} />
207
+ </Box>
208
+ {headerAction ? <Box>{headerAction}</Box> : null}
209
+ </Inline>
210
+
211
+ {hasBody ? (
212
+ <Stack gap={gap}>
213
+ {text ? <Text variant="body">{text}</Text> : null}
214
+ {children ? <Box>{children}</Box> : null}
215
+ {mediaItems.length > 0 ? (
216
+ <Stack gap="s">
217
+ {mediaItems.map((item, index) => (
218
+ <PostCardMediaItem key={`${index}`} media={item} />
219
+ ))}
220
+ </Stack>
221
+ ) : null}
222
+ </Stack>
223
+ ) : null}
224
+
225
+ {hasEngagement ? <Divider /> : null}
226
+ <PostCardActions actions={actions} />
227
+ <PostCardComments comments={comments} />
228
+ {footer ? <Box pt="xs">{footer}</Box> : null}
229
+ </Stack>
230
+ </Card>
231
+ );
232
+ }
233
+
234
+ export const PostCard = withZoraThemeScope(PostCardInner);
@@ -0,0 +1,9 @@
1
+ export { PostCard } from './PostCard';
2
+ export type {
3
+ PostAction,
4
+ PostAuthor,
5
+ PostAuthorAvatar,
6
+ PostCardMedia,
7
+ PostCardProps,
8
+ PostCommentPreview,
9
+ } from './types';
@@ -0,0 +1,68 @@
1
+ import type { ZoraComponentMeta } from '../../metadata';
2
+ import { CONTAINER_ALLOWED_CHILDREN } from '../../metadata/allowedChildren';
3
+
4
+ export const postCardMeta = {
5
+ name: 'PostCard',
6
+ category: 'pattern',
7
+ description:
8
+ 'Social/content post card with author identity, body, media, actions, and comment previews.',
9
+ directManifestNode: true,
10
+ allowedChildren: [...CONTAINER_ALLOWED_CHILDREN],
11
+ blueprint: {
12
+ label: 'Post card',
13
+ icon: { name: 'chatbubble-ellipses-outline' },
14
+ defaultProps: {
15
+ author: {
16
+ name: 'Ada Lovelace',
17
+ subtitle: '@ada · 2h',
18
+ avatar: {
19
+ name: 'Ada Lovelace',
20
+ },
21
+ },
22
+ text: 'Share an update, image, or announcement with a reusable ZORA PostCard.',
23
+ },
24
+ },
25
+ props: {
26
+ author: {
27
+ type: 'array',
28
+ category: 'Content',
29
+ label: 'Author',
30
+ itemSchema: [
31
+ {
32
+ key: 'name',
33
+ schema: {
34
+ type: 'string',
35
+ category: 'Content',
36
+ label: 'Name',
37
+ },
38
+ },
39
+ {
40
+ key: 'subtitle',
41
+ schema: {
42
+ type: 'string',
43
+ category: 'Content',
44
+ label: 'Subtitle',
45
+ },
46
+ },
47
+ ],
48
+ },
49
+ text: {
50
+ type: 'string',
51
+ category: 'Content',
52
+ label: 'Text',
53
+ },
54
+ compact: {
55
+ type: 'boolean',
56
+ category: 'Layout',
57
+ label: 'Compact',
58
+ default: false,
59
+ },
60
+ tone: {
61
+ type: 'enum',
62
+ category: 'Style',
63
+ label: 'Tone',
64
+ enum: ['default', 'subtle', 'outline'],
65
+ default: 'default',
66
+ },
67
+ },
68
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,71 @@
1
+ import type { ButtonIconSpec } from '@ankhorage/surface';
2
+ import type React from 'react';
3
+ import type { ImageSourcePropType } from 'react-native';
4
+
5
+ import type { AvatarShape, AvatarSize } from '../../components/avatar';
6
+ import type { ZoraCardTone, ZoraTone } from '../../internal/recipes';
7
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
8
+
9
+ export interface PostAuthorAvatar {
10
+ source?: ImageSourcePropType;
11
+ name?: string;
12
+ initials?: string;
13
+ label?: string;
14
+ size?: AvatarSize;
15
+ shape?: AvatarShape;
16
+ tone?: ZoraTone;
17
+ }
18
+
19
+ export interface PostAuthor {
20
+ name: React.ReactNode;
21
+ subtitle?: React.ReactNode;
22
+ avatar?: PostAuthorAvatar;
23
+ }
24
+
25
+ interface PostCardSourceMedia {
26
+ source: ImageSourcePropType;
27
+ label: string;
28
+ aspectRatio?: number;
29
+ children?: never;
30
+ }
31
+
32
+ interface PostCardCustomMedia {
33
+ children: React.ReactNode;
34
+ label?: string;
35
+ aspectRatio?: number;
36
+ source?: never;
37
+ }
38
+
39
+ export type PostCardMedia = PostCardSourceMedia | PostCardCustomMedia;
40
+
41
+ export interface PostAction {
42
+ id: string;
43
+ label: string;
44
+ icon?: ButtonIconSpec;
45
+ count?: React.ReactNode;
46
+ selected?: boolean;
47
+ disabled?: boolean;
48
+ onPress?: () => void;
49
+ }
50
+
51
+ export interface PostCommentPreview {
52
+ id: string;
53
+ author?: PostAuthor;
54
+ text: React.ReactNode;
55
+ meta?: React.ReactNode;
56
+ action?: React.ReactNode;
57
+ }
58
+
59
+ export interface PostCardProps extends ZoraBaseProps {
60
+ author: PostAuthor;
61
+ text?: React.ReactNode;
62
+ children?: React.ReactNode;
63
+ media?: PostCardMedia | readonly PostCardMedia[];
64
+ actions?: readonly PostAction[];
65
+ comments?: readonly PostCommentPreview[];
66
+ headerAction?: React.ReactNode;
67
+ footer?: React.ReactNode;
68
+ tone?: ZoraCardTone;
69
+ compact?: boolean;
70
+ onPress?: () => void;
71
+ }