@ankhorage/zora 2.6.0 → 2.7.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 (48) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/components/progress/meta.d.ts +38 -3
  4. package/dist/components/progress/meta.d.ts.map +1 -1
  5. package/dist/components/progress/meta.js +39 -3
  6. package/dist/components/progress/meta.js.map +1 -1
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/metadata/componentMeta.d.ts.map +1 -1
  12. package/dist/metadata/componentMeta.js +4 -0
  13. package/dist/metadata/componentMeta.js.map +1 -1
  14. package/dist/patterns/scanner/BarcodeScannerView.d.ts +11 -0
  15. package/dist/patterns/scanner/BarcodeScannerView.d.ts.map +1 -0
  16. package/dist/patterns/scanner/BarcodeScannerView.js +77 -0
  17. package/dist/patterns/scanner/BarcodeScannerView.js.map +1 -0
  18. package/dist/patterns/scanner/CameraPermissionView.d.ts +11 -0
  19. package/dist/patterns/scanner/CameraPermissionView.d.ts.map +1 -0
  20. package/dist/patterns/scanner/CameraPermissionView.js +62 -0
  21. package/dist/patterns/scanner/CameraPermissionView.js.map +1 -0
  22. package/dist/patterns/scanner/ScanOverlay.d.ts +11 -0
  23. package/dist/patterns/scanner/ScanOverlay.d.ts.map +1 -0
  24. package/dist/patterns/scanner/ScanOverlay.js +85 -0
  25. package/dist/patterns/scanner/ScanOverlay.js.map +1 -0
  26. package/dist/patterns/scanner/index.d.ts +5 -0
  27. package/dist/patterns/scanner/index.d.ts.map +1 -0
  28. package/dist/patterns/scanner/index.js +4 -0
  29. package/dist/patterns/scanner/index.js.map +1 -0
  30. package/dist/patterns/scanner/meta.d.ts +190 -0
  31. package/dist/patterns/scanner/meta.d.ts.map +1 -0
  32. package/dist/patterns/scanner/meta.js +186 -0
  33. package/dist/patterns/scanner/meta.js.map +1 -0
  34. package/dist/patterns/scanner/types.d.ts +42 -0
  35. package/dist/patterns/scanner/types.d.ts.map +1 -0
  36. package/dist/patterns/scanner/types.js +2 -0
  37. package/dist/patterns/scanner/types.js.map +1 -0
  38. package/package.json +2 -2
  39. package/src/components/progress/meta.ts +39 -3
  40. package/src/index.ts +8 -0
  41. package/src/metadata/componentMeta.test.ts +42 -105
  42. package/src/metadata/componentMeta.ts +8 -0
  43. package/src/patterns/scanner/BarcodeScannerView.tsx +127 -0
  44. package/src/patterns/scanner/CameraPermissionView.tsx +108 -0
  45. package/src/patterns/scanner/ScanOverlay.tsx +99 -0
  46. package/src/patterns/scanner/index.ts +10 -0
  47. package/src/patterns/scanner/meta.ts +190 -0
  48. package/src/patterns/scanner/types.ts +47 -0
@@ -1,10 +1,46 @@
1
+ import { ZORA_COLORS } from '../../internal/colorModel';
1
2
  import type { ZoraComponentMeta } from '../../metadata';
2
3
 
3
4
  export const progressMeta = {
4
5
  name: 'Progress',
5
6
  category: 'component',
6
- directManifestNode: false,
7
+ directManifestNode: true,
7
8
  allowedChildren: [],
8
- note: 'Feedback component; not represented as a manifest node in v1.',
9
- props: {},
9
+ blueprint: {
10
+ label: 'Progress',
11
+ defaultProps: {
12
+ value: 50,
13
+ max: 100,
14
+ color: 'primary',
15
+ size: 'm',
16
+ },
17
+ },
18
+ props: {
19
+ value: {
20
+ type: 'number',
21
+ category: 'State',
22
+ label: 'Value',
23
+ default: 50,
24
+ },
25
+ max: {
26
+ type: 'number',
27
+ category: 'State',
28
+ label: 'Max',
29
+ default: 100,
30
+ },
31
+ color: {
32
+ type: 'enum',
33
+ category: 'Style',
34
+ label: 'Color',
35
+ enum: [...ZORA_COLORS],
36
+ default: 'primary',
37
+ },
38
+ size: {
39
+ type: 'enum',
40
+ category: 'Style',
41
+ label: 'Size',
42
+ enum: ['s', 'm', 'l'],
43
+ default: 'm',
44
+ },
45
+ },
10
46
  } as const satisfies ZoraComponentMeta;
package/src/index.ts CHANGED
@@ -317,6 +317,14 @@ export type {
317
317
  ResponsivePanelSize,
318
318
  } from './patterns/responsive-panel';
319
319
  export { ResponsivePanel } from './patterns/responsive-panel';
320
+ export type {
321
+ BarcodeScannerViewProps,
322
+ BarcodeScanResult,
323
+ CameraPermissionStatus,
324
+ CameraPermissionViewProps,
325
+ ScanOverlayProps,
326
+ } from './patterns/scanner';
327
+ export { BarcodeScannerView, CameraPermissionView, ScanOverlay } from './patterns/scanner';
320
328
  export type { SectionHeaderProps } from './patterns/section-header';
321
329
  export { SectionHeader } from './patterns/section-header';
322
330
  export type {
@@ -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 './componentMeta';
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
- const indexSource = readSource(INDEX_PATH);
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 indexSource = readSource(INDEX_PATH);
69
- expect(indexSource).toContain('ZoraComponentEventMeta');
70
- expect(indexSource).toContain('ZoraComponentEventPayloadKind');
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
- // Avoid importing the full src/index.ts barrel here; Bun has been observed to crash when executing
77
- // module graphs that touch react-native. We do drift checks via static source parsing instead.
78
- const indexSource = readSource(INDEX_PATH);
79
- const blocks = parseReexportBlocks(indexSource).filter((block) => !block.isType);
80
-
81
- const uiComponentNames = new Set(
82
- blocks
83
- .filter((block) => {
84
- return (
85
- block.from.startsWith('./foundation') ||
86
- block.from.startsWith('./components') ||
87
- block.from.startsWith('./patterns') ||
88
- block.from.startsWith('./layout')
89
- );
90
- })
91
- .flatMap((block) => block.names)
92
- .filter(isComponentExportName),
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 UI component registry entries', () => {
100
- const indexSource = readSource(INDEX_PATH);
101
- expect(indexSource).toContain("export * from './theme';");
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,9 @@ describe('ZORA_COMPONENT_META invariants', () => {
177
103
  'Heading',
178
104
  'Divider',
179
105
  'ChatListItem',
106
+ 'CameraPermissionView',
107
+ 'ScanOverlay',
108
+ 'Progress',
180
109
  ]);
181
110
 
182
111
  const expectedContainerNodes = new Set([
@@ -193,6 +122,7 @@ describe('ZORA_COMPONENT_META invariants', () => {
193
122
  'Stack',
194
123
  'Grid',
195
124
  'Container',
125
+ 'BarcodeScannerView',
196
126
  ]);
197
127
 
198
128
  for (const [key, meta] of Object.entries(ZORA_COMPONENT_META)) {
@@ -217,6 +147,17 @@ describe('ZORA_COMPONENT_META invariants', () => {
217
147
  }
218
148
  });
219
149
 
150
+ test('Progress is a direct manifest leaf with serializable defaults', () => {
151
+ expect(ZORA_COMPONENT_META.Progress.directManifestNode).toBe(true);
152
+ expect(ZORA_COMPONENT_META.Progress.allowedChildren).toEqual([]);
153
+ expect(ZORA_COMPONENT_META.Progress.blueprint?.defaultProps).toEqual({
154
+ value: 50,
155
+ max: 100,
156
+ color: 'primary',
157
+ size: 'm',
158
+ });
159
+ });
160
+
220
161
  test('non-direct manifest nodes include an explicit note', () => {
221
162
  for (const [key, meta] of Object.entries(ZORA_COMPONENT_META)) {
222
163
  if (meta.directManifestNode) continue;
@@ -233,11 +174,7 @@ describe('ZORA_COMPONENT_META invariants', () => {
233
174
 
234
175
  for (const [key, meta] of Object.entries(ZORA_COMPONENT_META)) {
235
176
  if (!meta.blueprint?.defaultProps) continue;
236
-
237
- expect(
238
- isJsonSerializable(meta.blueprint.defaultProps),
239
- `${key} blueprint.defaultProps contains a non-serializable value`,
240
- ).toBe(true);
177
+ expect(() => JSON.stringify(meta.blueprint.defaultProps), key).not.toThrow();
241
178
  }
242
179
  });
243
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';