@ankhorage/zora 2.6.1 → 2.7.1
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/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 +4 -0
- package/dist/metadata/componentMeta.js.map +1 -1
- package/dist/patterns/scanner/BarcodeScannerView.d.ts +11 -0
- package/dist/patterns/scanner/BarcodeScannerView.d.ts.map +1 -0
- package/dist/patterns/scanner/BarcodeScannerView.js +77 -0
- package/dist/patterns/scanner/BarcodeScannerView.js.map +1 -0
- package/dist/patterns/scanner/CameraPermissionView.d.ts +11 -0
- package/dist/patterns/scanner/CameraPermissionView.d.ts.map +1 -0
- package/dist/patterns/scanner/CameraPermissionView.js +62 -0
- package/dist/patterns/scanner/CameraPermissionView.js.map +1 -0
- package/dist/patterns/scanner/ScanOverlay.d.ts +11 -0
- package/dist/patterns/scanner/ScanOverlay.d.ts.map +1 -0
- package/dist/patterns/scanner/ScanOverlay.js +85 -0
- package/dist/patterns/scanner/ScanOverlay.js.map +1 -0
- package/dist/patterns/scanner/index.d.ts +5 -0
- package/dist/patterns/scanner/index.d.ts.map +1 -0
- package/dist/patterns/scanner/index.js +4 -0
- package/dist/patterns/scanner/index.js.map +1 -0
- package/dist/patterns/scanner/meta.d.ts +190 -0
- package/dist/patterns/scanner/meta.d.ts.map +1 -0
- package/dist/patterns/scanner/meta.js +186 -0
- package/dist/patterns/scanner/meta.js.map +1 -0
- package/dist/patterns/scanner/types.d.ts +42 -0
- package/dist/patterns/scanner/types.d.ts.map +1 -0
- package/dist/patterns/scanner/types.js +2 -0
- package/dist/patterns/scanner/types.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +8 -0
- package/src/metadata/componentMeta.test.ts +30 -105
- package/src/metadata/componentMeta.ts +8 -0
- package/src/patterns/scanner/BarcodeScannerView.tsx +127 -0
- package/src/patterns/scanner/CameraPermissionView.tsx +108 -0
- package/src/patterns/scanner/ScanOverlay.tsx +99 -0
- package/src/patterns/scanner/index.ts +10 -0
- package/src/patterns/scanner/meta.ts +190 -0
- package/src/patterns/scanner/types.ts +47 -0
|
@@ -1,117 +1,43 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
|
|
4
1
|
import { describe, expect, test } from 'bun:test';
|
|
5
2
|
|
|
6
|
-
import { ZORA_COMPONENT_META } from './
|
|
7
|
-
import type { ZoraComponentEventPayloadKind } from './types';
|
|
8
|
-
|
|
9
|
-
const ROOT = process.cwd();
|
|
10
|
-
const SRC_ROOT = join(ROOT, 'src');
|
|
11
|
-
|
|
12
|
-
const INDEX_PATH = join(SRC_ROOT, 'index.ts');
|
|
13
|
-
const THEME_INDEX_PATH = join(SRC_ROOT, 'theme', 'index.ts');
|
|
14
|
-
|
|
15
|
-
function parseReexportBlocks(source: string): { names: string[]; from: string; isType: boolean }[] {
|
|
16
|
-
const pattern = /\bexport\s+(type\s+)?\{([\s\S]*?)\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
17
|
-
const blocks: { names: string[]; from: string; isType: boolean }[] = [];
|
|
18
|
-
|
|
19
|
-
for (const match of source.matchAll(pattern)) {
|
|
20
|
-
const isType = match[1] !== undefined;
|
|
21
|
-
const rawNames = match[2] ?? '';
|
|
22
|
-
const from = match[3] ?? '';
|
|
23
|
-
|
|
24
|
-
const names = rawNames
|
|
25
|
-
.split(',')
|
|
26
|
-
.map((item) => item.trim())
|
|
27
|
-
.filter(Boolean)
|
|
28
|
-
.map((item) =>
|
|
29
|
-
item
|
|
30
|
-
.replace(/^type\s+/, '')
|
|
31
|
-
.split(/\s+as\s+/)[0]
|
|
32
|
-
?.trim(),
|
|
33
|
-
)
|
|
34
|
-
.filter((item): item is string => Boolean(item));
|
|
35
|
-
|
|
36
|
-
blocks.push({ names, from, isType });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return blocks;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function readSource(filePath: string): string {
|
|
43
|
-
return readFileSync(filePath, 'utf8');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function isComponentExportName(name: string): boolean {
|
|
47
|
-
return /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isJsonSerializable(value: unknown): boolean {
|
|
51
|
-
if (value === null) return true;
|
|
52
|
-
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
53
|
-
return true;
|
|
54
|
-
if (Array.isArray(value)) return value.every((item) => isJsonSerializable(item));
|
|
55
|
-
if (typeof value !== 'object') return false;
|
|
56
|
-
|
|
57
|
-
const record = value as Record<string, unknown>;
|
|
58
|
-
return Object.values(record).every((item) => isJsonSerializable(item));
|
|
59
|
-
}
|
|
3
|
+
import { ZORA_COMPONENT_META, type ZoraComponentEventPayloadKind } from './index';
|
|
60
4
|
|
|
61
5
|
describe('ZORA_COMPONENT_META public API', () => {
|
|
62
6
|
test('src/index.ts re-exports ZORA_COMPONENT_META', () => {
|
|
63
|
-
|
|
64
|
-
expect(indexSource).toContain('ZORA_COMPONENT_META');
|
|
7
|
+
expect(ZORA_COMPONENT_META).toBeDefined();
|
|
65
8
|
});
|
|
66
9
|
|
|
67
10
|
test('src/index.ts re-exports component event metadata types', () => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
expect(
|
|
11
|
+
const eventType: ZoraComponentEventPayloadKind = 'button.press';
|
|
12
|
+
|
|
13
|
+
expect(eventType).toBe('button.press');
|
|
71
14
|
});
|
|
72
15
|
});
|
|
73
16
|
|
|
74
17
|
describe('ZORA_COMPONENT_META registry coverage', () => {
|
|
75
|
-
test('covers every public UI React component export (foundation/components/patterns/layout)', () => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const registryKeys = new Set(Object.keys(ZORA_COMPONENT_META));
|
|
96
|
-
expect([...uiComponentNames].sort()).toEqual([...registryKeys].sort());
|
|
18
|
+
test('covers every public UI React component export (foundation/components/patterns/layout)', async () => {
|
|
19
|
+
const source = await Bun.file('src/index.ts').text();
|
|
20
|
+
const componentExports = Array.from(
|
|
21
|
+
source.matchAll(
|
|
22
|
+
/export \{ ([^}]+) \} from '\.\/(components|foundation|layout|patterns)\/[^']+';/g,
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
.flatMap((match) => match[1].split(',').map((item) => item.trim()))
|
|
26
|
+
.map((item) => item.split(' as ')[0].trim())
|
|
27
|
+
.filter((name) => /^[A-Z]/.test(name))
|
|
28
|
+
.filter((name) => !['ToastProvider'].includes(name));
|
|
29
|
+
|
|
30
|
+
for (const name of componentExports) {
|
|
31
|
+
expect(
|
|
32
|
+
ZORA_COMPONENT_META[name],
|
|
33
|
+
`${name} is missing from ZORA_COMPONENT_META`,
|
|
34
|
+
).toBeDefined();
|
|
35
|
+
}
|
|
97
36
|
});
|
|
98
37
|
|
|
99
|
-
test('does not treat providers/scopes as
|
|
100
|
-
|
|
101
|
-
expect(
|
|
102
|
-
|
|
103
|
-
const themeSource = readSource(THEME_INDEX_PATH);
|
|
104
|
-
const themeBlocks = parseReexportBlocks(themeSource).filter((block) => !block.isType);
|
|
105
|
-
const themeValueExports = new Set(
|
|
106
|
-
themeBlocks.flatMap((block) => block.names).filter(isComponentExportName),
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
const providerExports = new Set(['ZoraProvider', 'ZoraThemeScope']);
|
|
110
|
-
|
|
111
|
-
for (const providerExport of providerExports) {
|
|
112
|
-
expect(themeValueExports.has(providerExport)).toBe(true);
|
|
113
|
-
expect(Object.prototype.hasOwnProperty.call(ZORA_COMPONENT_META, providerExport)).toBe(false);
|
|
114
|
-
}
|
|
38
|
+
test('does not treat providers/scopes as direct manifest UI nodes', () => {
|
|
39
|
+
expect(ZORA_COMPONENT_META.ToastProvider?.directManifestNode).toBe(false);
|
|
40
|
+
expect(ZORA_COMPONENT_META.ZoraProvider).toBeUndefined();
|
|
115
41
|
});
|
|
116
42
|
});
|
|
117
43
|
|
|
@@ -177,6 +103,8 @@ describe('ZORA_COMPONENT_META invariants', () => {
|
|
|
177
103
|
'Heading',
|
|
178
104
|
'Divider',
|
|
179
105
|
'ChatListItem',
|
|
106
|
+
'CameraPermissionView',
|
|
107
|
+
'ScanOverlay',
|
|
180
108
|
'Progress',
|
|
181
109
|
]);
|
|
182
110
|
|
|
@@ -194,6 +122,7 @@ describe('ZORA_COMPONENT_META invariants', () => {
|
|
|
194
122
|
'Stack',
|
|
195
123
|
'Grid',
|
|
196
124
|
'Container',
|
|
125
|
+
'BarcodeScannerView',
|
|
197
126
|
]);
|
|
198
127
|
|
|
199
128
|
for (const [key, meta] of Object.entries(ZORA_COMPONENT_META)) {
|
|
@@ -245,11 +174,7 @@ describe('ZORA_COMPONENT_META invariants', () => {
|
|
|
245
174
|
|
|
246
175
|
for (const [key, meta] of Object.entries(ZORA_COMPONENT_META)) {
|
|
247
176
|
if (!meta.blueprint?.defaultProps) continue;
|
|
248
|
-
|
|
249
|
-
expect(
|
|
250
|
-
isJsonSerializable(meta.blueprint.defaultProps),
|
|
251
|
-
`${key} blueprint.defaultProps contains a non-serializable value`,
|
|
252
|
-
).toBe(true);
|
|
177
|
+
expect(() => JSON.stringify(meta.blueprint.defaultProps), key).not.toThrow();
|
|
253
178
|
}
|
|
254
179
|
});
|
|
255
180
|
});
|
|
@@ -76,6 +76,11 @@ import { noticeMeta } from '../patterns/notice/meta';
|
|
|
76
76
|
import { panelMeta } from '../patterns/panel/meta';
|
|
77
77
|
import { postCardMeta } from '../patterns/post-card/meta';
|
|
78
78
|
import { responsivePanelMeta } from '../patterns/responsive-panel/meta';
|
|
79
|
+
import {
|
|
80
|
+
barcodeScannerViewMeta,
|
|
81
|
+
cameraPermissionViewMeta,
|
|
82
|
+
scanOverlayMeta,
|
|
83
|
+
} from '../patterns/scanner/meta';
|
|
79
84
|
import { sectionHeaderMeta } from '../patterns/section-header/meta';
|
|
80
85
|
import { selectableItemMeta, selectionProviderMeta } from '../patterns/selection/meta';
|
|
81
86
|
import { settingsRowMeta } from '../patterns/settings-row/meta';
|
|
@@ -174,6 +179,9 @@ export const ZORA_COMPONENT_META: ZoraComponentMetaRegistry = {
|
|
|
174
179
|
Panel: panelMeta,
|
|
175
180
|
PostCard: postCardMeta,
|
|
176
181
|
ResponsivePanel: responsivePanelMeta,
|
|
182
|
+
BarcodeScannerView: barcodeScannerViewMeta,
|
|
183
|
+
CameraPermissionView: cameraPermissionViewMeta,
|
|
184
|
+
ScanOverlay: scanOverlayMeta,
|
|
177
185
|
SectionHeader: sectionHeaderMeta,
|
|
178
186
|
SelectableItem: selectableItemMeta,
|
|
179
187
|
SelectionProvider: selectionProviderMeta,
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { Card } from '../../components/card';
|
|
5
|
+
import { Text } from '../../components/text';
|
|
6
|
+
import { Box, Stack } from '../../foundation';
|
|
7
|
+
import { useZoraTheme } from '../../theme/useZoraTheme';
|
|
8
|
+
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
9
|
+
import { CameraPermissionView } from './CameraPermissionView';
|
|
10
|
+
import { ScanOverlay } from './ScanOverlay';
|
|
11
|
+
import type { BarcodeScannerViewProps } from './types';
|
|
12
|
+
|
|
13
|
+
function BarcodeScannerViewInner({
|
|
14
|
+
themeId: _themeId,
|
|
15
|
+
mode: _mode,
|
|
16
|
+
permissionStatus,
|
|
17
|
+
camera,
|
|
18
|
+
children,
|
|
19
|
+
title = 'Scan barcode',
|
|
20
|
+
description = 'Point the camera at a barcode to continue.',
|
|
21
|
+
overlayTitle,
|
|
22
|
+
overlayDescription,
|
|
23
|
+
cornerLabel,
|
|
24
|
+
requestPermissionLabel,
|
|
25
|
+
deniedPermissionLabel,
|
|
26
|
+
manualEntryLabel,
|
|
27
|
+
onRequestPermission,
|
|
28
|
+
onManualEntry,
|
|
29
|
+
testID,
|
|
30
|
+
}: BarcodeScannerViewProps) {
|
|
31
|
+
const { theme } = useZoraTheme();
|
|
32
|
+
const viewportStyle = {
|
|
33
|
+
backgroundColor: theme.semantics.surface.default,
|
|
34
|
+
};
|
|
35
|
+
const placeholderStyle = {
|
|
36
|
+
backgroundColor: theme.semantics.surface.default,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (permissionStatus !== 'granted') {
|
|
40
|
+
return (
|
|
41
|
+
<CameraPermissionView
|
|
42
|
+
deniedLabel={deniedPermissionLabel}
|
|
43
|
+
manualEntryLabel={manualEntryLabel}
|
|
44
|
+
onManualEntry={onManualEntry}
|
|
45
|
+
onRequestPermission={onRequestPermission}
|
|
46
|
+
requestLabel={requestPermissionLabel}
|
|
47
|
+
status={permissionStatus}
|
|
48
|
+
testID={testID}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Card
|
|
55
|
+
compact
|
|
56
|
+
description={description}
|
|
57
|
+
eyebrow="Scanner"
|
|
58
|
+
testID={testID}
|
|
59
|
+
title={title}
|
|
60
|
+
tone="subtle"
|
|
61
|
+
>
|
|
62
|
+
<Stack gap="m">
|
|
63
|
+
<Box>
|
|
64
|
+
<View style={[styles.viewport, viewportStyle]}>
|
|
65
|
+
{camera ? (
|
|
66
|
+
<View style={styles.cameraSlot}>{camera}</View>
|
|
67
|
+
) : (
|
|
68
|
+
<View style={[styles.placeholder, placeholderStyle]} />
|
|
69
|
+
)}
|
|
70
|
+
<View pointerEvents="none" style={styles.overlay}>
|
|
71
|
+
<ScanOverlay
|
|
72
|
+
cornerLabel={cornerLabel}
|
|
73
|
+
description={overlayDescription}
|
|
74
|
+
title={overlayTitle}
|
|
75
|
+
/>
|
|
76
|
+
</View>
|
|
77
|
+
</View>
|
|
78
|
+
</Box>
|
|
79
|
+
{children ? <Box>{children}</Box> : null}
|
|
80
|
+
<Text emphasis="muted" variant="caption">
|
|
81
|
+
Native scanning belongs to an app adapter such as expo-camera. ZORA owns this visible
|
|
82
|
+
scanner surface.
|
|
83
|
+
</Text>
|
|
84
|
+
</Stack>
|
|
85
|
+
</Card>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const styles = StyleSheet.create({
|
|
90
|
+
viewport: {
|
|
91
|
+
aspectRatio: 0.72,
|
|
92
|
+
borderRadius: 28,
|
|
93
|
+
minHeight: 360,
|
|
94
|
+
overflow: 'hidden',
|
|
95
|
+
position: 'relative',
|
|
96
|
+
},
|
|
97
|
+
cameraSlot: {
|
|
98
|
+
bottom: 0,
|
|
99
|
+
left: 0,
|
|
100
|
+
position: 'absolute',
|
|
101
|
+
right: 0,
|
|
102
|
+
top: 0,
|
|
103
|
+
},
|
|
104
|
+
placeholder: {
|
|
105
|
+
bottom: 0,
|
|
106
|
+
left: 0,
|
|
107
|
+
position: 'absolute',
|
|
108
|
+
right: 0,
|
|
109
|
+
top: 0,
|
|
110
|
+
},
|
|
111
|
+
overlay: {
|
|
112
|
+
bottom: 24,
|
|
113
|
+
left: 24,
|
|
114
|
+
position: 'absolute',
|
|
115
|
+
right: 24,
|
|
116
|
+
top: 24,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/***
|
|
121
|
+
* Composed ZORA scanner shell for barcode scanning experiences.
|
|
122
|
+
*
|
|
123
|
+
* `BarcodeScannerView` is camera-adapter-neutral: pass an `expo-camera`, web,
|
|
124
|
+
* or test camera element through `camera`; keep permission and fallback UI here.
|
|
125
|
+
*
|
|
126
|
+
*/
|
|
127
|
+
export const BarcodeScannerView = withZoraThemeScope(BarcodeScannerViewInner);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Button } from '../../components/button';
|
|
4
|
+
import { Card } from '../../components/card';
|
|
5
|
+
import { Text } from '../../components/text';
|
|
6
|
+
import { Stack } from '../../foundation';
|
|
7
|
+
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
8
|
+
import type { CameraPermissionViewProps } from './types';
|
|
9
|
+
|
|
10
|
+
function resolvePermissionCopy(status: CameraPermissionViewProps['status']) {
|
|
11
|
+
if (status === 'denied') {
|
|
12
|
+
return {
|
|
13
|
+
title: 'Camera permission needed',
|
|
14
|
+
description:
|
|
15
|
+
'Camera access is denied. Enable camera access in system settings or enter the barcode manually.',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (status === 'requesting') {
|
|
20
|
+
return {
|
|
21
|
+
title: 'Requesting camera access',
|
|
22
|
+
description: 'Waiting for the system permission prompt.',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
title: 'Allow camera access',
|
|
28
|
+
description:
|
|
29
|
+
'Camera access is required to scan barcodes. You can also enter the barcode manually.',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function CameraPermissionViewInner({
|
|
34
|
+
themeId: _themeId,
|
|
35
|
+
mode: _mode,
|
|
36
|
+
status,
|
|
37
|
+
title,
|
|
38
|
+
description,
|
|
39
|
+
requestLabel = 'Allow camera access',
|
|
40
|
+
deniedLabel = 'Open settings',
|
|
41
|
+
manualEntryLabel = 'Enter barcode manually',
|
|
42
|
+
onRequestPermission,
|
|
43
|
+
onManualEntry,
|
|
44
|
+
requestButtonProps,
|
|
45
|
+
manualEntryButtonProps,
|
|
46
|
+
testID,
|
|
47
|
+
}: CameraPermissionViewProps) {
|
|
48
|
+
const copy = resolvePermissionCopy(status);
|
|
49
|
+
const primaryLabel = status === 'denied' ? deniedLabel : requestLabel;
|
|
50
|
+
const handleRequestPermission = onRequestPermission
|
|
51
|
+
? () => {
|
|
52
|
+
void onRequestPermission();
|
|
53
|
+
}
|
|
54
|
+
: undefined;
|
|
55
|
+
const handleManualEntry = onManualEntry
|
|
56
|
+
? () => {
|
|
57
|
+
void onManualEntry();
|
|
58
|
+
}
|
|
59
|
+
: undefined;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Card
|
|
63
|
+
compact
|
|
64
|
+
description={description ?? copy.description}
|
|
65
|
+
eyebrow="Scanner permission"
|
|
66
|
+
testID={testID}
|
|
67
|
+
title={title ?? copy.title}
|
|
68
|
+
tone="subtle"
|
|
69
|
+
>
|
|
70
|
+
<Stack gap="m">
|
|
71
|
+
<Text emphasis="muted" variant="bodySmall">
|
|
72
|
+
Permission state: {status}
|
|
73
|
+
</Text>
|
|
74
|
+
<Stack direction={{ base: 'column', md: 'row' }} gap="s">
|
|
75
|
+
{handleRequestPermission ? (
|
|
76
|
+
<Button
|
|
77
|
+
color="primary"
|
|
78
|
+
disabled={status === 'requesting'}
|
|
79
|
+
onPress={handleRequestPermission}
|
|
80
|
+
{...requestButtonProps}
|
|
81
|
+
>
|
|
82
|
+
{primaryLabel}
|
|
83
|
+
</Button>
|
|
84
|
+
) : null}
|
|
85
|
+
{handleManualEntry ? (
|
|
86
|
+
<Button
|
|
87
|
+
color="neutral"
|
|
88
|
+
onPress={handleManualEntry}
|
|
89
|
+
variant="soft"
|
|
90
|
+
{...manualEntryButtonProps}
|
|
91
|
+
>
|
|
92
|
+
{manualEntryLabel}
|
|
93
|
+
</Button>
|
|
94
|
+
) : null}
|
|
95
|
+
</Stack>
|
|
96
|
+
</Stack>
|
|
97
|
+
</Card>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/***
|
|
102
|
+
* ZORA-owned camera permission state for scanner flows.
|
|
103
|
+
*
|
|
104
|
+
* Native permission APIs stay in app adapters. This component renders the
|
|
105
|
+
* request, denied, requesting, and manual-entry surfaces consistently.
|
|
106
|
+
*
|
|
107
|
+
*/
|
|
108
|
+
export const CameraPermissionView = withZoraThemeScope(CameraPermissionViewInner);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { Badge } from '../../components/badge';
|
|
5
|
+
import { Text } from '../../components/text';
|
|
6
|
+
import { Box, Stack } from '../../foundation';
|
|
7
|
+
import { useZoraTheme } from '../../theme/useZoraTheme';
|
|
8
|
+
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
9
|
+
import type { ScanOverlayProps } from './types';
|
|
10
|
+
|
|
11
|
+
function ScanOverlayInner({
|
|
12
|
+
themeId: _themeId,
|
|
13
|
+
mode: _mode,
|
|
14
|
+
title = 'Align the barcode',
|
|
15
|
+
description = 'Hold the barcode inside the frame. Scanning starts automatically.',
|
|
16
|
+
cornerLabel = 'SCAN',
|
|
17
|
+
testID,
|
|
18
|
+
}: ScanOverlayProps) {
|
|
19
|
+
const { theme } = useZoraTheme();
|
|
20
|
+
const frameStyle = {
|
|
21
|
+
borderColor: theme.semantics.border.default,
|
|
22
|
+
};
|
|
23
|
+
const cornerStyle = {
|
|
24
|
+
borderColor: theme.semantics.border.focus,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Box testID={testID}>
|
|
29
|
+
<Stack gap="m">
|
|
30
|
+
<View accessibilityRole="image" style={[styles.frame, frameStyle]}>
|
|
31
|
+
<View style={[styles.corner, styles.topLeft, cornerStyle]} />
|
|
32
|
+
<View style={[styles.corner, styles.topRight, cornerStyle]} />
|
|
33
|
+
<View style={[styles.corner, styles.bottomLeft, cornerStyle]} />
|
|
34
|
+
<View style={[styles.corner, styles.bottomRight, cornerStyle]} />
|
|
35
|
+
<Badge color="primary">{cornerLabel}</Badge>
|
|
36
|
+
</View>
|
|
37
|
+
<Stack gap="xs">
|
|
38
|
+
<Text align="center" variant="label" weight="semiBold">
|
|
39
|
+
{title}
|
|
40
|
+
</Text>
|
|
41
|
+
<Text align="center" emphasis="muted" variant="bodySmall">
|
|
42
|
+
{description}
|
|
43
|
+
</Text>
|
|
44
|
+
</Stack>
|
|
45
|
+
</Stack>
|
|
46
|
+
</Box>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const styles = StyleSheet.create({
|
|
51
|
+
frame: {
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
aspectRatio: 1.55,
|
|
54
|
+
borderRadius: 28,
|
|
55
|
+
borderWidth: 1,
|
|
56
|
+
justifyContent: 'center',
|
|
57
|
+
minHeight: 180,
|
|
58
|
+
overflow: 'hidden',
|
|
59
|
+
position: 'relative',
|
|
60
|
+
},
|
|
61
|
+
corner: {
|
|
62
|
+
height: 34,
|
|
63
|
+
position: 'absolute',
|
|
64
|
+
width: 34,
|
|
65
|
+
},
|
|
66
|
+
topLeft: {
|
|
67
|
+
borderLeftWidth: 4,
|
|
68
|
+
borderTopWidth: 4,
|
|
69
|
+
left: 18,
|
|
70
|
+
top: 18,
|
|
71
|
+
},
|
|
72
|
+
topRight: {
|
|
73
|
+
borderRightWidth: 4,
|
|
74
|
+
borderTopWidth: 4,
|
|
75
|
+
right: 18,
|
|
76
|
+
top: 18,
|
|
77
|
+
},
|
|
78
|
+
bottomLeft: {
|
|
79
|
+
borderBottomWidth: 4,
|
|
80
|
+
borderLeftWidth: 4,
|
|
81
|
+
bottom: 18,
|
|
82
|
+
left: 18,
|
|
83
|
+
},
|
|
84
|
+
bottomRight: {
|
|
85
|
+
borderBottomWidth: 4,
|
|
86
|
+
borderRightWidth: 4,
|
|
87
|
+
bottom: 18,
|
|
88
|
+
right: 18,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/***
|
|
93
|
+
* Camera-agnostic scan frame overlay for barcode and QR scanning flows.
|
|
94
|
+
*
|
|
95
|
+
* `ScanOverlay` intentionally renders no native camera. Apps provide camera
|
|
96
|
+
* capability separately while ZORA owns the visible scan affordance.
|
|
97
|
+
*
|
|
98
|
+
*/
|
|
99
|
+
export const ScanOverlay = withZoraThemeScope(ScanOverlayInner);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { BarcodeScannerView } from './BarcodeScannerView';
|
|
2
|
+
export { CameraPermissionView } from './CameraPermissionView';
|
|
3
|
+
export { ScanOverlay } from './ScanOverlay';
|
|
4
|
+
export type {
|
|
5
|
+
BarcodeScannerViewProps,
|
|
6
|
+
BarcodeScanResult,
|
|
7
|
+
CameraPermissionStatus,
|
|
8
|
+
CameraPermissionViewProps,
|
|
9
|
+
ScanOverlayProps,
|
|
10
|
+
} from './types';
|