@ankhorage/zora 2.7.3 → 2.8.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 (64) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/dist/components/heading/Heading.d.ts.map +1 -1
  4. package/dist/components/heading/Heading.js +27 -11
  5. package/dist/components/heading/Heading.js.map +1 -1
  6. package/dist/components/heading/types.d.ts +2 -1
  7. package/dist/components/heading/types.d.ts.map +1 -1
  8. package/dist/components/heading/types.js.map +1 -1
  9. package/dist/components/text/Text.d.ts +1 -1
  10. package/dist/components/text/Text.d.ts.map +1 -1
  11. package/dist/components/text/Text.js +16 -4
  12. package/dist/components/text/Text.js.map +1 -1
  13. package/dist/components/text/types.d.ts +2 -1
  14. package/dist/components/text/types.d.ts.map +1 -1
  15. package/dist/components/text/types.js.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/metadata/componentMeta.d.ts.map +1 -1
  21. package/dist/metadata/componentMeta.js +2 -0
  22. package/dist/metadata/componentMeta.js.map +1 -1
  23. package/dist/metadata/types.d.ts +2 -1
  24. package/dist/metadata/types.d.ts.map +1 -1
  25. package/dist/metadata/types.js.map +1 -1
  26. package/dist/patterns/disclosure-section/DisclosureSection.d.ts.map +1 -1
  27. package/dist/patterns/disclosure-section/DisclosureSection.js +11 -2
  28. package/dist/patterns/disclosure-section/DisclosureSection.js.map +1 -1
  29. package/dist/patterns/product-card/ProductCard.d.ts +5 -0
  30. package/dist/patterns/product-card/ProductCard.d.ts.map +1 -0
  31. package/dist/patterns/product-card/ProductCard.js +70 -0
  32. package/dist/patterns/product-card/ProductCard.js.map +1 -0
  33. package/dist/patterns/product-card/index.d.ts +3 -0
  34. package/dist/patterns/product-card/index.d.ts.map +1 -0
  35. package/dist/patterns/product-card/index.js +2 -0
  36. package/dist/patterns/product-card/index.js.map +1 -0
  37. package/dist/patterns/product-card/meta.d.ts +110 -0
  38. package/dist/patterns/product-card/meta.d.ts.map +1 -0
  39. package/dist/patterns/product-card/meta.js +110 -0
  40. package/dist/patterns/product-card/meta.js.map +1 -0
  41. package/dist/patterns/product-card/types.d.ts +22 -0
  42. package/dist/patterns/product-card/types.d.ts.map +1 -0
  43. package/dist/patterns/product-card/types.js +2 -0
  44. package/dist/patterns/product-card/types.js.map +1 -0
  45. package/dist/patterns/scanner/meta.d.ts +13 -0
  46. package/dist/patterns/scanner/meta.d.ts.map +1 -1
  47. package/dist/patterns/scanner/meta.js +7 -0
  48. package/dist/patterns/scanner/meta.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/components/heading/Heading.tsx +28 -10
  51. package/src/components/heading/types.ts +2 -1
  52. package/src/components/text/Text.tsx +17 -3
  53. package/src/components/text/types.ts +2 -1
  54. package/src/index.ts +2 -0
  55. package/src/metadata/componentMeta.test.ts +21 -0
  56. package/src/metadata/componentMeta.ts +2 -0
  57. package/src/metadata/types.ts +2 -1
  58. package/src/patterns/disclosure-section/DisclosureSection.tsx +13 -3
  59. package/src/patterns/product-card/ProductCard.test.tsx +19 -0
  60. package/src/patterns/product-card/ProductCard.tsx +125 -0
  61. package/src/patterns/product-card/index.ts +2 -0
  62. package/src/patterns/product-card/meta.ts +111 -0
  63. package/src/patterns/product-card/types.ts +21 -0
  64. package/src/patterns/scanner/meta.ts +7 -0
@@ -1,12 +1,25 @@
1
1
  import { resolveResponsive, useResponsiveRuntime } from '@ankhorage/surface';
2
2
  import React from 'react';
3
- import { Text as ReactNativeText } from 'react-native';
3
+ import { Platform, Text as ReactNativeText, type TextStyle } from 'react-native';
4
4
 
5
5
  import { useZoraTheme } from '../../theme/useZoraTheme';
6
6
  import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
7
7
  import { resolveHeadingRecipe, resolveHeadingSizeFromLevel } from './resolveHeadingRecipe';
8
8
  import type { HeadingProps } from './types';
9
9
 
10
+ const headingLayoutStyle = {
11
+ flexShrink: 1,
12
+ maxWidth: '100%',
13
+ minWidth: 0,
14
+ ...(Platform.OS === 'web'
15
+ ? {
16
+ overflowWrap: 'break-word',
17
+ whiteSpace: 'normal',
18
+ wordBreak: 'normal',
19
+ }
20
+ : null),
21
+ } as unknown as TextStyle;
22
+
10
23
  function resolveHeadingContent({
11
24
  children,
12
25
  text,
@@ -47,6 +60,7 @@ function HeadingInner({
47
60
  numberOfLines,
48
61
  ellipsizeMode,
49
62
  selectable,
63
+ style,
50
64
  accessibilityLabel,
51
65
  accessibilityHint,
52
66
  accessibilityRole = 'header',
@@ -76,15 +90,19 @@ function HeadingInner({
76
90
  numberOfLines={numberOfLines}
77
91
  selectable={selectable}
78
92
  testID={testID}
79
- style={resolveHeadingRecipe(theme, {
80
- align: resolvedAlign,
81
- italic,
82
- level,
83
- size: resolvedSize,
84
- color: resolvedColor,
85
- emphasis: resolvedEmphasis,
86
- weight: resolvedWeight,
87
- })}
93
+ style={[
94
+ headingLayoutStyle,
95
+ resolveHeadingRecipe(theme, {
96
+ align: resolvedAlign,
97
+ italic,
98
+ level,
99
+ size: resolvedSize,
100
+ color: resolvedColor,
101
+ emphasis: resolvedEmphasis,
102
+ weight: resolvedWeight,
103
+ }),
104
+ style,
105
+ ]}
88
106
  >
89
107
  {content}
90
108
  </ReactNativeText>
@@ -1,6 +1,6 @@
1
1
  import type { Responsive } from '@ankhorage/surface';
2
2
  import type React from 'react';
3
- import type { AccessibilityRole, TextStyle } from 'react-native';
3
+ import type { AccessibilityRole, StyleProp, TextStyle } from 'react-native';
4
4
 
5
5
  import type { ZoraColor, ZoraEmphasis } from '../../internal/recipes';
6
6
  import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
@@ -30,6 +30,7 @@ export interface HeadingProps extends ZoraBaseProps {
30
30
  numberOfLines?: number;
31
31
  ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';
32
32
  selectable?: boolean;
33
+ style?: StyleProp<TextStyle>;
33
34
  accessibilityLabel?: string;
34
35
  accessibilityHint?: string;
35
36
  accessibilityRole?: AccessibilityRole;
@@ -1,12 +1,25 @@
1
1
  import { resolveResponsive, useResponsiveRuntime } from '@ankhorage/surface';
2
2
  import React from 'react';
3
- import { Text as ReactNativeText } from 'react-native';
3
+ import { Platform, Text as ReactNativeText, type TextStyle } from 'react-native';
4
4
 
5
5
  import { useZoraTheme } from '../../theme/useZoraTheme';
6
6
  import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
7
7
  import { resolveTextStyle } from './resolveTextRecipe';
8
8
  import type { TextProps } from './types';
9
9
 
10
+ const textLayoutStyle = {
11
+ flexShrink: 1,
12
+ maxWidth: '100%',
13
+ minWidth: 0,
14
+ ...(Platform.OS === 'web'
15
+ ? {
16
+ overflowWrap: 'break-word',
17
+ whiteSpace: 'normal',
18
+ wordBreak: 'normal',
19
+ }
20
+ : null),
21
+ } as unknown as TextStyle;
22
+
10
23
  function resolveTextContent({
11
24
  children,
12
25
  text,
@@ -46,6 +59,7 @@ function TextInner({
46
59
  numberOfLines,
47
60
  ellipsizeMode,
48
61
  selectable,
62
+ style,
49
63
  accessibilityLabel,
50
64
  accessibilityHint,
51
65
  accessibilityRole,
@@ -81,7 +95,7 @@ function TextInner({
81
95
  numberOfLines={numberOfLines}
82
96
  selectable={selectable}
83
97
  testID={testID}
84
- style={resolvedStyle}
98
+ style={[textLayoutStyle, resolvedStyle, style]}
85
99
  >
86
100
  {content}
87
101
  </ReactNativeText>
@@ -94,7 +108,7 @@ function TextInner({
94
108
  * `Text` owns normal body, caption, label, code, and supporting-copy variants so
95
109
  * consumers do not need to import lower-level Surface typography directly.
96
110
  *
97
-
111
+
98
112
  * @example Muted supporting copy
99
113
  * ```tsx
100
114
  * <Text variant="bodySmall" emphasis="muted">Updated just now</Text>
@@ -1,6 +1,6 @@
1
1
  import type { Responsive } from '@ankhorage/surface';
2
2
  import type React from 'react';
3
- import type { AccessibilityRole, TextStyle } from 'react-native';
3
+ import type { AccessibilityRole, StyleProp, TextStyle } from 'react-native';
4
4
 
5
5
  import type { ZoraColor, ZoraEmphasis } from '../../internal/recipes';
6
6
  import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
@@ -27,6 +27,7 @@ export interface TextProps extends ZoraBaseProps {
27
27
  numberOfLines?: number;
28
28
  ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';
29
29
  selectable?: boolean;
30
+ style?: StyleProp<TextStyle>;
30
31
  accessibilityLabel?: string;
31
32
  accessibilityHint?: string;
32
33
  accessibilityRole?: AccessibilityRole;
package/src/index.ts CHANGED
@@ -308,6 +308,8 @@ export type {
308
308
  PostCardProps,
309
309
  } from './patterns/post-card';
310
310
  export { PostCard } from './patterns/post-card';
311
+ export type { ProductCardProps } from './patterns/product-card';
312
+ export { ProductCard } from './patterns/product-card';
311
313
  export type {
312
314
  ResponsivePanelDesktopMode,
313
315
  ResponsivePanelMobileMode,
@@ -70,6 +70,25 @@ describe('ZORA_COMPONENT_META event metadata', () => {
70
70
  });
71
71
  });
72
72
 
73
+ describe('ZORA_COMPONENT_META requirement metadata', () => {
74
+ test('declares camera permission metadata for camera permission UI', () => {
75
+ expect(ZORA_COMPONENT_META.CameraPermissionView.requirements).toEqual({
76
+ permissions: [{ permission: 'camera' }],
77
+ });
78
+ });
79
+
80
+ test('declares camera and barcode scanner metadata for barcode scanner UI', () => {
81
+ expect(ZORA_COMPONENT_META.BarcodeScannerView.requirements).toEqual({
82
+ permissions: [{ permission: 'camera' }],
83
+ capabilities: [{ capability: 'barcodeScanner' }],
84
+ });
85
+ });
86
+
87
+ test('does not declare runtime requirements for the camera-agnostic scan overlay', () => {
88
+ expect(ZORA_COMPONENT_META.ScanOverlay.requirements).toBeUndefined();
89
+ });
90
+ });
91
+
73
92
  describe('ZORA_COMPONENT_META invariants', () => {
74
93
  test('every key matches meta.name', () => {
75
94
  for (const [key, meta] of Object.entries(ZORA_COMPONENT_META)) {
@@ -105,6 +124,7 @@ describe('ZORA_COMPONENT_META invariants', () => {
105
124
  'ChatListItem',
106
125
  'CameraPermissionView',
107
126
  'ScanOverlay',
127
+ 'ProductCard',
108
128
  'Progress',
109
129
  ]);
110
130
 
@@ -119,6 +139,7 @@ describe('ZORA_COMPONENT_META invariants', () => {
119
139
  'PostCard',
120
140
  'MessageBubble',
121
141
  'Box',
142
+
122
143
  'Stack',
123
144
  'Grid',
124
145
  'Container',
@@ -75,6 +75,7 @@ import { messageBubbleMeta } from '../patterns/message-bubble/meta';
75
75
  import { noticeMeta } from '../patterns/notice/meta';
76
76
  import { panelMeta } from '../patterns/panel/meta';
77
77
  import { postCardMeta } from '../patterns/post-card/meta';
78
+ import { productCardMeta } from '../patterns/product-card/meta';
78
79
  import { responsivePanelMeta } from '../patterns/responsive-panel/meta';
79
80
  import {
80
81
  barcodeScannerViewMeta,
@@ -178,6 +179,7 @@ export const ZORA_COMPONENT_META: ZoraComponentMetaRegistry = {
178
179
  Notice: noticeMeta,
179
180
  Panel: panelMeta,
180
181
  PostCard: postCardMeta,
182
+ ProductCard: productCardMeta,
181
183
  ResponsivePanel: responsivePanelMeta,
182
184
  BarcodeScannerView: barcodeScannerViewMeta,
183
185
  CameraPermissionView: cameraPermissionViewMeta,
@@ -1,4 +1,4 @@
1
- import type { ComponentEventDtoKind } from '@ankhorage/contracts';
1
+ import type { ComponentEventDtoKind, ComponentRequirements } from '@ankhorage/contracts';
2
2
 
3
3
  export type ZoraComponentCategory = 'foundation' | 'component' | 'pattern' | 'layout';
4
4
 
@@ -90,6 +90,7 @@ export interface ZoraComponentMeta {
90
90
  description?: string;
91
91
  directManifestNode: boolean;
92
92
  allowedChildren: readonly string[];
93
+ requirements?: ComponentRequirements;
93
94
  blueprint?: ZoraComponentBlueprint;
94
95
  events?: Readonly<Record<string, ZoraComponentEventMeta>>;
95
96
  i18n?: ZoraComponentI18nMeta;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Platform, Pressable, type ViewStyle } from 'react-native';
2
+ import { Platform, Pressable, type TextStyle, type ViewStyle } from 'react-native';
3
3
 
4
4
  import { Heading } from '../../components/heading';
5
5
  import { IconButton } from '../../components/icon-button';
@@ -11,6 +11,13 @@ import type { DisclosureSectionProps } from './types';
11
11
 
12
12
  const triggerTextStyle: ViewStyle = {
13
13
  flex: 1,
14
+ flexBasis: 0,
15
+ minWidth: 0,
16
+ };
17
+
18
+ const triggerCopyStyle: TextStyle = {
19
+ flexShrink: 1,
20
+ maxWidth: '100%',
14
21
  minWidth: 0,
15
22
  };
16
23
 
@@ -22,6 +29,7 @@ const getTriggerStyle = (disabled?: boolean): ViewStyle =>
22
29
  ({
23
30
  alignSelf: 'stretch',
24
31
  flex: 1,
32
+ flexBasis: 0,
25
33
  justifyContent: 'center',
26
34
  minWidth: 0,
27
35
  ...(Platform.OS === 'web'
@@ -78,9 +86,11 @@ function DisclosureSectionInner({
78
86
  <Box style={triggerTextStyle}>
79
87
  <Stack gap="xs">
80
88
  {icon ? <Box pb="xs">{/* Surface icon spec would go here */}</Box> : null}
81
- <Heading level={4}>{title}</Heading>
89
+ <Heading level={4} style={triggerCopyStyle}>
90
+ {title}
91
+ </Heading>
82
92
  {description ? (
83
- <Text emphasis="muted" variant="bodySmall">
93
+ <Text emphasis="muted" variant="bodySmall" style={triggerCopyStyle}>
84
94
  {description}
85
95
  </Text>
86
96
  ) : null}
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { ZORA_COMPONENT_META } from '../../metadata';
4
+
5
+ describe('ProductCard', () => {
6
+ test('is registered as a public ZORA pattern', () => {
7
+ expect(ZORA_COMPONENT_META.ProductCard?.name).toBe('ProductCard');
8
+ expect(ZORA_COMPONENT_META.ProductCard?.category).toBe('pattern');
9
+ expect(ZORA_COMPONENT_META.ProductCard?.directManifestNode).toBe(true);
10
+ expect(ZORA_COMPONENT_META.ProductCard?.allowedChildren).toEqual([]);
11
+ expect(ZORA_COMPONENT_META.ProductCard?.requirements).toBeUndefined();
12
+ });
13
+
14
+ test('has correct event metadata', () => {
15
+ expect(ZORA_COMPONENT_META.ProductCard?.events?.press).toBeDefined();
16
+ expect(ZORA_COMPONENT_META.ProductCard?.events?.primaryAction).toBeDefined();
17
+ expect(ZORA_COMPONENT_META.ProductCard?.events?.secondaryAction).toBeDefined();
18
+ });
19
+ });
@@ -0,0 +1,125 @@
1
+ import React from 'react';
2
+
3
+ import { Badge } from '../../components/badge';
4
+ import { Button } from '../../components/button';
5
+ import { Card } from '../../components/card';
6
+ import { Heading } from '../../components/heading';
7
+ import { Image } from '../../components/image';
8
+ import { Text } from '../../components/text';
9
+ import { Divider, Inline, Stack } from '../../foundation';
10
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
11
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
12
+ import type { ProductCardProps } from './types';
13
+
14
+ function ProductCardInner({
15
+ title,
16
+ subtitle,
17
+ description,
18
+ brand,
19
+ vendor: _vendor,
20
+ imageUrl,
21
+ imageAlt,
22
+ price,
23
+ currency,
24
+ badges,
25
+ meta,
26
+ primaryActionLabel,
27
+ secondaryActionLabel,
28
+ onPress,
29
+ onPrimaryAction,
30
+ onSecondaryAction,
31
+ ...rest
32
+ }: ProductCardProps & ZoraBaseProps) {
33
+ const hasHeaderInfo = !!title || !!brand || !!subtitle;
34
+ const hasMeta = !!meta && meta.length > 0;
35
+ const hasActions = !!primaryActionLabel || !!secondaryActionLabel;
36
+ const isInteractive = Boolean(onPress);
37
+
38
+ const eyebrow = brand ?? _vendor;
39
+
40
+ return (
41
+ <Card onPress={isInteractive ? onPress : undefined} {...rest}>
42
+ <Stack gap="m">
43
+ {imageUrl ? (
44
+ <Image
45
+ accessibilityLabel={imageAlt ?? title}
46
+ source={{ uri: imageUrl }}
47
+ aspectRatio={1}
48
+ radius="m"
49
+ />
50
+ ) : null}
51
+
52
+ <Stack gap="s">
53
+ {hasHeaderInfo ? (
54
+ <Stack gap="xxs">
55
+ {eyebrow ? (
56
+ <Text variant="caption" weight="semiBold" emphasis="muted">
57
+ {eyebrow}
58
+ </Text>
59
+ ) : null}
60
+ <Heading level={4}>{title}</Heading>
61
+ {subtitle ? (
62
+ <Text variant="bodySmall" emphasis="muted">
63
+ {subtitle}
64
+ </Text>
65
+ ) : null}
66
+ </Stack>
67
+ ) : null}
68
+
69
+ {description ? <Text variant="bodySmall">{description}</Text> : null}
70
+
71
+ {badges && badges.length > 0 ? (
72
+ <Inline gap="xs" wrap="wrap">
73
+ {badges.map((badge, index) => (
74
+ <Badge key={`${badge}-${index}`}>{badge}</Badge>
75
+ ))}
76
+ </Inline>
77
+ ) : null}
78
+ </Stack>
79
+
80
+ {price ? (
81
+ <Text variant="bodySmall" weight="bold">
82
+ {currency ? `${currency} ` : ''}
83
+ {price}
84
+ </Text>
85
+ ) : null}
86
+
87
+ {hasMeta ? (
88
+ <Stack gap="xs">
89
+ <Divider />
90
+ {meta.map((item, index) => (
91
+ <Inline key={`${item.label}-${index}`} justify="space-between">
92
+ <Text variant="bodySmall" emphasis="muted">
93
+ {item.label}
94
+ </Text>
95
+ <Text variant="bodySmall" weight="semiBold">
96
+ {item.value}
97
+ </Text>
98
+ </Inline>
99
+ ))}
100
+ </Stack>
101
+ ) : null}
102
+
103
+ {hasActions ? (
104
+ <Stack gap="s">
105
+ <Divider />
106
+ <Inline gap="s">
107
+ {secondaryActionLabel ? (
108
+ <Button variant="ghost" onPress={onSecondaryAction} size="s">
109
+ {secondaryActionLabel}
110
+ </Button>
111
+ ) : null}
112
+ {primaryActionLabel ? (
113
+ <Button variant="solid" onPress={onPrimaryAction} size="s" flex={1}>
114
+ {primaryActionLabel}
115
+ </Button>
116
+ ) : null}
117
+ </Inline>
118
+ </Stack>
119
+ ) : null}
120
+ </Stack>
121
+ </Card>
122
+ );
123
+ }
124
+
125
+ export const ProductCard = withZoraThemeScope(ProductCardInner);
@@ -0,0 +1,2 @@
1
+ export { ProductCard } from './ProductCard';
2
+ export type { ProductCardProps } from './types';
@@ -0,0 +1,111 @@
1
+ import type { ZoraComponentMeta } from '../../metadata';
2
+
3
+ export const productCardMeta = {
4
+ name: 'ProductCard',
5
+ category: 'pattern',
6
+ description: 'A generic card for displaying product or article information.',
7
+ directManifestNode: true,
8
+ allowedChildren: [],
9
+ events: {
10
+ press: {
11
+ label: 'Press',
12
+ eventType: 'productCard.press',
13
+ description: 'Emitted when the product card is pressed.',
14
+ payloadFields: [],
15
+ },
16
+ primaryAction: {
17
+ label: 'Primary action',
18
+ eventType: 'productCard.primaryAction',
19
+ description: 'Emitted when the primary product card action is pressed.',
20
+ payloadFields: [],
21
+ },
22
+ secondaryAction: {
23
+ label: 'Secondary action',
24
+ eventType: 'productCard.secondaryAction',
25
+ description: 'Emitted when the secondary product card action is pressed.',
26
+ payloadFields: [],
27
+ },
28
+ },
29
+ props: {
30
+ title: {
31
+ type: 'string',
32
+ category: 'Content',
33
+ label: 'Title',
34
+ },
35
+ subtitle: {
36
+ type: 'string',
37
+ category: 'Content',
38
+ label: 'Subtitle',
39
+ },
40
+ description: {
41
+ type: 'string',
42
+ category: 'Content',
43
+ label: 'Description',
44
+ },
45
+ brand: {
46
+ type: 'string',
47
+ category: 'Content',
48
+ label: 'Brand',
49
+ },
50
+ vendor: {
51
+ type: 'string',
52
+ category: 'Content',
53
+ label: 'Vendor',
54
+ },
55
+ imageUrl: {
56
+ type: 'string',
57
+ category: 'Content',
58
+ label: 'Image URL',
59
+ },
60
+ imageAlt: {
61
+ type: 'string',
62
+ category: 'Content',
63
+ label: 'Image Alt Text',
64
+ },
65
+ price: {
66
+ type: 'string',
67
+ category: 'Content',
68
+ label: 'Price',
69
+ },
70
+ currency: {
71
+ type: 'string',
72
+ category: 'Content',
73
+ label: 'Currency',
74
+ },
75
+ badges: {
76
+ type: 'array',
77
+ category: 'Content',
78
+ label: 'Badges',
79
+ },
80
+ meta: {
81
+ type: 'array',
82
+ category: 'Content',
83
+ label: 'Meta',
84
+ },
85
+ primaryActionLabel: {
86
+ type: 'string',
87
+ category: 'Content',
88
+ label: 'Primary Action Label',
89
+ },
90
+ secondaryActionLabel: {
91
+ type: 'string',
92
+ category: 'Content',
93
+ label: 'Secondary Action Label',
94
+ },
95
+ onPress: {
96
+ type: 'action',
97
+ category: 'Events',
98
+ label: 'Press action',
99
+ },
100
+ onPrimaryAction: {
101
+ type: 'action',
102
+ category: 'Events',
103
+ label: 'Primary action',
104
+ },
105
+ onSecondaryAction: {
106
+ type: 'action',
107
+ category: 'Events',
108
+ label: 'Secondary action',
109
+ },
110
+ },
111
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,21 @@
1
+ export interface ProductCardProps {
2
+ title: string;
3
+ subtitle?: string;
4
+ description?: string;
5
+ brand?: string;
6
+ vendor?: string;
7
+ imageUrl?: string;
8
+ imageAlt?: string;
9
+ price?: string;
10
+ currency?: string;
11
+ badges?: readonly string[];
12
+ meta?: readonly {
13
+ label: string;
14
+ value: string;
15
+ }[];
16
+ primaryActionLabel?: string;
17
+ secondaryActionLabel?: string;
18
+ onPress?: () => void;
19
+ onPrimaryAction?: () => void;
20
+ onSecondaryAction?: () => void;
21
+ }
@@ -43,6 +43,9 @@ export const cameraPermissionViewMeta = {
43
43
  description: 'ZORA-owned camera permission state for scanner flows.',
44
44
  directManifestNode: true,
45
45
  allowedChildren: [],
46
+ requirements: {
47
+ permissions: [{ permission: 'camera' }],
48
+ },
46
49
  blueprint: {
47
50
  label: 'Camera permission',
48
51
  defaultProps: {
@@ -107,6 +110,10 @@ export const barcodeScannerViewMeta = {
107
110
  'Composed scanner shell with permission, camera slot, scan overlay, and manual entry affordance.',
108
111
  directManifestNode: true,
109
112
  allowedChildren: [...CONTAINER_ALLOWED_CHILDREN],
113
+ requirements: {
114
+ permissions: [{ permission: 'camera' }],
115
+ capabilities: [{ capability: 'barcodeScanner' }],
116
+ },
110
117
  blueprint: {
111
118
  label: 'Barcode scanner',
112
119
  defaultProps: {