@ankhorage/zora 2.7.2 → 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.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/components/heading/Heading.d.ts.map +1 -1
- package/dist/components/heading/Heading.js +27 -11
- package/dist/components/heading/Heading.js.map +1 -1
- package/dist/components/heading/types.d.ts +2 -1
- package/dist/components/heading/types.d.ts.map +1 -1
- package/dist/components/heading/types.js.map +1 -1
- package/dist/components/text/Text.d.ts +1 -1
- package/dist/components/text/Text.d.ts.map +1 -1
- package/dist/components/text/Text.js +16 -4
- package/dist/components/text/Text.js.map +1 -1
- package/dist/components/text/types.d.ts +2 -1
- package/dist/components/text/types.d.ts.map +1 -1
- package/dist/components/text/types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/metadata/componentMeta.d.ts.map +1 -1
- package/dist/metadata/componentMeta.js +2 -0
- package/dist/metadata/componentMeta.js.map +1 -1
- package/dist/metadata/types.d.ts +2 -1
- package/dist/metadata/types.d.ts.map +1 -1
- package/dist/metadata/types.js.map +1 -1
- package/dist/patterns/disclosure-section/DisclosureSection.d.ts.map +1 -1
- package/dist/patterns/disclosure-section/DisclosureSection.js +11 -2
- package/dist/patterns/disclosure-section/DisclosureSection.js.map +1 -1
- package/dist/patterns/product-card/ProductCard.d.ts +5 -0
- package/dist/patterns/product-card/ProductCard.d.ts.map +1 -0
- package/dist/patterns/product-card/ProductCard.js +70 -0
- package/dist/patterns/product-card/ProductCard.js.map +1 -0
- package/dist/patterns/product-card/index.d.ts +3 -0
- package/dist/patterns/product-card/index.d.ts.map +1 -0
- package/dist/patterns/product-card/index.js +2 -0
- package/dist/patterns/product-card/index.js.map +1 -0
- package/dist/patterns/product-card/meta.d.ts +110 -0
- package/dist/patterns/product-card/meta.d.ts.map +1 -0
- package/dist/patterns/product-card/meta.js +110 -0
- package/dist/patterns/product-card/meta.js.map +1 -0
- package/dist/patterns/product-card/types.d.ts +22 -0
- package/dist/patterns/product-card/types.d.ts.map +1 -0
- package/dist/patterns/product-card/types.js +2 -0
- package/dist/patterns/product-card/types.js.map +1 -0
- package/dist/patterns/scanner/meta.d.ts +13 -0
- package/dist/patterns/scanner/meta.d.ts.map +1 -1
- package/dist/patterns/scanner/meta.js +7 -0
- package/dist/patterns/scanner/meta.js.map +1 -1
- package/package.json +2 -2
- package/src/components/heading/Heading.tsx +28 -10
- package/src/components/heading/types.ts +2 -1
- package/src/components/text/Text.tsx +17 -3
- package/src/components/text/types.ts +2 -1
- package/src/index.ts +2 -0
- package/src/metadata/componentMeta.test.ts +21 -0
- package/src/metadata/componentMeta.ts +2 -0
- package/src/metadata/types.ts +2 -1
- package/src/patterns/disclosure-section/DisclosureSection.tsx +13 -3
- package/src/patterns/product-card/ProductCard.test.tsx +19 -0
- package/src/patterns/product-card/ProductCard.tsx +125 -0
- package/src/patterns/product-card/index.ts +2 -0
- package/src/patterns/product-card/meta.ts +111 -0
- package/src/patterns/product-card/types.ts +21 -0
- 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={
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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,
|
package/src/metadata/types.ts
CHANGED
|
@@ -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}
|
|
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,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: {
|