@ankhorage/zora 2.13.2 → 3.0.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 (106) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -1
  3. package/dist/components/action-sheet/ActionSheetItem.d.ts.map +1 -1
  4. package/dist/components/action-sheet/ActionSheetItem.js +1 -1
  5. package/dist/components/action-sheet/ActionSheetItem.js.map +1 -1
  6. package/dist/components/app-bar/AppBar.js.map +1 -1
  7. package/dist/components/avatar/Avatar.d.ts.map +1 -1
  8. package/dist/components/avatar/Avatar.js +1 -1
  9. package/dist/components/avatar/Avatar.js.map +1 -1
  10. package/dist/components/breadcrumbs/Breadcrumbs.d.ts.map +1 -1
  11. package/dist/components/breadcrumbs/Breadcrumbs.js +1 -1
  12. package/dist/components/breadcrumbs/Breadcrumbs.js.map +1 -1
  13. package/dist/components/chip/Chip.d.ts.map +1 -1
  14. package/dist/components/chip/Chip.js +1 -1
  15. package/dist/components/chip/Chip.js.map +1 -1
  16. package/dist/components/gradient/Gradient.d.ts +2 -2
  17. package/dist/components/gradient/Gradient.d.ts.map +1 -1
  18. package/dist/components/gradient/Gradient.js +12 -7
  19. package/dist/components/gradient/Gradient.js.map +1 -1
  20. package/dist/components/gradient/GradientRendererContext.d.ts +11 -0
  21. package/dist/components/gradient/GradientRendererContext.d.ts.map +1 -0
  22. package/dist/components/gradient/GradientRendererContext.js +19 -0
  23. package/dist/components/gradient/GradientRendererContext.js.map +1 -0
  24. package/dist/components/gradient/index.d.ts +2 -1
  25. package/dist/components/gradient/index.d.ts.map +1 -1
  26. package/dist/components/gradient/index.js +1 -0
  27. package/dist/components/gradient/index.js.map +1 -1
  28. package/dist/components/gradient/types.d.ts +14 -0
  29. package/dist/components/gradient/types.d.ts.map +1 -1
  30. package/dist/components/gradient/types.js.map +1 -1
  31. package/dist/components/icon/Icon.d.ts +1 -2
  32. package/dist/components/icon/Icon.d.ts.map +1 -1
  33. package/dist/components/icon/Icon.js.map +1 -1
  34. package/dist/components/input/Input.d.ts.map +1 -1
  35. package/dist/components/input/Input.js +1 -1
  36. package/dist/components/input/Input.js.map +1 -1
  37. package/dist/components/menu/Menu.d.ts.map +1 -1
  38. package/dist/components/menu/Menu.js +1 -1
  39. package/dist/components/menu/Menu.js.map +1 -1
  40. package/dist/components/metric-card/MetricCard.d.ts.map +1 -1
  41. package/dist/components/metric-card/MetricCard.js +1 -1
  42. package/dist/components/metric-card/MetricCard.js.map +1 -1
  43. package/dist/components/select/Select.js +1 -1
  44. package/dist/components/select/Select.js.map +1 -1
  45. package/dist/components/textarea/Textarea.d.ts.map +1 -1
  46. package/dist/components/textarea/Textarea.js +1 -1
  47. package/dist/components/textarea/Textarea.js.map +1 -1
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/internal/resolveZoraNavigationItems.d.ts +2 -2
  53. package/dist/internal/resolveZoraNavigationItems.d.ts.map +1 -1
  54. package/dist/internal/resolveZoraNavigationItems.js +4 -3
  55. package/dist/internal/resolveZoraNavigationItems.js.map +1 -1
  56. package/dist/layout/app-shell/AppShell.d.ts.map +1 -1
  57. package/dist/layout/app-shell/AppShell.js +2 -4
  58. package/dist/layout/app-shell/AppShell.js.map +1 -1
  59. package/dist/patterns/auth/oauthProviders.d.ts +9 -0
  60. package/dist/patterns/auth/oauthProviders.d.ts.map +1 -1
  61. package/dist/patterns/auth/oauthProviders.js +9 -9
  62. package/dist/patterns/auth/oauthProviders.js.map +1 -1
  63. package/dist/patterns/auth/types.d.ts +2 -6
  64. package/dist/patterns/auth/types.d.ts.map +1 -1
  65. package/dist/patterns/auth/types.js.map +1 -1
  66. package/dist/patterns/scanner/BarcodeScannerView.js +1 -1
  67. package/dist/patterns/scanner/BarcodeScannerView.js.map +1 -1
  68. package/dist/patterns/timeline/Timeline.d.ts.map +1 -1
  69. package/dist/patterns/timeline/Timeline.js +1 -1
  70. package/dist/patterns/timeline/Timeline.js.map +1 -1
  71. package/dist/patterns/zora-tab-bar/ZoraTabBar.d.ts.map +1 -1
  72. package/dist/patterns/zora-tab-bar/ZoraTabBar.js +1 -1
  73. package/dist/patterns/zora-tab-bar/ZoraTabBar.js.map +1 -1
  74. package/package.json +34 -29
  75. package/src/components/action-sheet/ActionSheetItem.tsx +1 -6
  76. package/src/components/app-bar/AppBar.tsx +3 -3
  77. package/src/components/avatar/Avatar.tsx +1 -8
  78. package/src/components/breadcrumbs/Breadcrumbs.tsx +1 -2
  79. package/src/components/chip/Chip.tsx +1 -8
  80. package/src/components/gradient/Gradient.tsx +16 -12
  81. package/src/components/gradient/GradientRendererContext.tsx +27 -0
  82. package/src/components/gradient/index.ts +4 -0
  83. package/src/components/gradient/types.ts +17 -0
  84. package/src/components/icon/Icon.tsx +1 -1
  85. package/src/components/input/Input.tsx +2 -12
  86. package/src/components/menu/Menu.tsx +1 -8
  87. package/src/components/metric-card/MetricCard.tsx +1 -8
  88. package/src/components/select/Select.tsx +1 -1
  89. package/src/components/textarea/Textarea.tsx +2 -12
  90. package/src/expo57Migration.test.ts +125 -0
  91. package/src/index.ts +4 -1
  92. package/src/interactionPolicy.test.ts +1 -1
  93. package/src/internal/resolveZoraNavigationItems.test.ts +11 -0
  94. package/src/internal/resolveZoraNavigationItems.ts +7 -6
  95. package/src/layout/app-shell/AppShell.tsx +2 -6
  96. package/src/metadata/componentMeta.test.ts +2 -1
  97. package/src/nativePeerContract.test.ts +5 -3
  98. package/src/patterns/auth/oauthProviders.test.ts +25 -0
  99. package/src/patterns/auth/oauthProviders.ts +9 -9
  100. package/src/patterns/auth/types.ts +2 -6
  101. package/src/patterns/scanner/BarcodeScannerView.tsx +1 -1
  102. package/src/patterns/timeline/Timeline.tsx +1 -2
  103. package/src/patterns/zora-tab-bar/ZoraTabBar.tsx +1 -2
  104. package/src/platformAcceptance.test.ts +14 -0
  105. package/src/registry.test.ts +5 -1
  106. package/src/theme/themeScopeStructure.test.ts +1 -1
@@ -0,0 +1,125 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ import { describe, expect, test } from 'bun:test';
5
+
6
+ const ROOT = process.cwd();
7
+ const EXPO_EXAMPLE_DIRS = [
8
+ 'examples/expo-showcase',
9
+ 'examples/food_drink/restaurant',
10
+ 'examples/shopping_commerce/marketplace',
11
+ 'examples/shopping_commerce/storefront',
12
+ 'examples/social_community/community-feed',
13
+ 'examples/social_community/photo-social',
14
+ 'examples/social_community/private-messaging',
15
+ 'examples/social_community/visual-discovery',
16
+ ] as const;
17
+ const RNVI_PLUGINS = [
18
+ '@react-native-vector-icons/fontawesome',
19
+ '@react-native-vector-icons/fontawesome5',
20
+ '@react-native-vector-icons/fontawesome6',
21
+ '@react-native-vector-icons/ionicons',
22
+ ] as const;
23
+
24
+ function readJson(path: string): Record<string, unknown> {
25
+ const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
26
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
27
+ throw new Error(`Expected an object in ${path}.`);
28
+ }
29
+ return value as Record<string, unknown>;
30
+ }
31
+
32
+ function readRecord(record: Record<string, unknown>, key: string): Record<string, unknown> {
33
+ const value = Reflect.get(record, key);
34
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
35
+ throw new Error(`Expected ${key} to be an object.`);
36
+ }
37
+ return value as Record<string, unknown>;
38
+ }
39
+
40
+ function readValue(record: Record<string, unknown>, key: string): unknown {
41
+ return Reflect.get(record, key);
42
+ }
43
+
44
+ function readExamplePackage(directory: string): Record<string, unknown> {
45
+ return readJson(join(ROOT, directory, 'package.json'));
46
+ }
47
+
48
+ describe('portable ZORA package boundary', () => {
49
+ test('targets the released RN 0.86 and Surface 3 contract without Expo runtime peers', () => {
50
+ const packageJson = readJson(join(ROOT, 'package.json'));
51
+ const dependencies = readRecord(packageJson, 'dependencies');
52
+ const peers = readRecord(packageJson, 'peerDependencies');
53
+ const development = readRecord(packageJson, 'devDependencies');
54
+
55
+ expect(readValue(dependencies, '@ankhorage/surface')).toBe('^3.0.0');
56
+ expect(readValue(dependencies, '@ankhorage/contracts')).toBe('^8.0.0');
57
+ expect(peers.react).toBe('19.2.3');
58
+ expect(readValue(peers, 'react-native')).toBe('0.86.2');
59
+ expect(readValue(peers, 'react-native-web')).toBe('~0.21.0');
60
+ expect(development.typescript).toBe('~6.0.3');
61
+
62
+ for (const expoPackage of ['@expo/vector-icons', 'expo-font', 'expo-linear-gradient']) {
63
+ expect(readValue(peers, expoPackage)).toBeUndefined();
64
+ expect(readValue(development, expoPackage)).toBeUndefined();
65
+ }
66
+ });
67
+
68
+ test('contains no Expo runtime import or removed RN 0.85 style API', () => {
69
+ const runtimeSource = readFileSync(
70
+ join(ROOT, 'src', 'components', 'gradient', 'Gradient.tsx'),
71
+ 'utf8',
72
+ );
73
+ const shellSource = readFileSync(
74
+ join(ROOT, 'src', 'layout', 'app-shell', 'AppShell.tsx'),
75
+ 'utf8',
76
+ );
77
+
78
+ expect(runtimeSource).not.toContain("from 'expo");
79
+ expect(shellSource).not.toContain('absoluteFillObject');
80
+ expect(shellSource).not.toMatch(/pointerEvents=/);
81
+ });
82
+ });
83
+
84
+ describe('Expo 57 example boundary', () => {
85
+ test('uses the canonical React, RN, RNW, TypeScript, and Expo baselines', () => {
86
+ for (const directory of EXPO_EXAMPLE_DIRS) {
87
+ const packageJson = readExamplePackage(directory);
88
+ const dependencies = readRecord(packageJson, 'dependencies');
89
+ const development = readRecord(packageJson, 'devDependencies');
90
+
91
+ expect(dependencies.expo).toBe('~57.0.15');
92
+ expect(dependencies.react).toBe('19.2.3');
93
+ expect(readValue(dependencies, 'react-native')).toBe('0.86.2');
94
+ expect(readValue(dependencies, 'react-native-web')).toBe('~0.21.0');
95
+ expect(development.typescript).toBe('~6.0.3');
96
+ expect(readValue(dependencies, '@expo/vector-icons')).toBeUndefined();
97
+ }
98
+ });
99
+
100
+ test('registers all scoped RNVI packages in each Expo app config', () => {
101
+ for (const directory of EXPO_EXAMPLE_DIRS) {
102
+ const appJson = readJson(join(ROOT, directory, 'app.json'));
103
+ const expo = readRecord(appJson, 'expo');
104
+ const { plugins } = expo;
105
+ expect(Array.isArray(plugins)).toBe(true);
106
+
107
+ for (const plugin of RNVI_PLUGINS) {
108
+ expect(plugins).toContain(plugin);
109
+ }
110
+ }
111
+ });
112
+
113
+ test('loads every icon font face exercised by Web acceptance', () => {
114
+ const webFontSource = readFileSync(
115
+ join(ROOT, 'examples', 'expo-showcase', 'useZoraIconFonts.web.ts'),
116
+ 'utf8',
117
+ );
118
+
119
+ expect(webFontSource).toContain('Ionicons.ttf');
120
+ expect(webFontSource).toContain('FontAwesome.ttf');
121
+ expect(webFontSource).toContain('FontAwesome5_Brands.ttf');
122
+ expect(webFontSource).toContain('FontAwesome5_Solid.ttf');
123
+ expect(webFontSource).toContain('FontAwesome6_Brands.ttf');
124
+ });
125
+ });
package/src/index.ts CHANGED
@@ -74,8 +74,11 @@ export type {
74
74
  GradientLocations,
75
75
  GradientPoint,
76
76
  GradientProps,
77
+ GradientRenderer,
78
+ GradientRendererProps,
79
+ GradientRendererProviderProps,
77
80
  } from './components/gradient';
78
- export { Gradient } from './components/gradient';
81
+ export { Gradient, GradientRendererProvider } from './components/gradient';
79
82
  export type {
80
83
  HeadingAlign,
81
84
  HeadingColor,
@@ -203,7 +203,7 @@ describe('Select', () => {
203
203
  test('prevents pointer/touch interaction with Picker subtree while passive', () => {
204
204
  const source = readComponent('select', 'Select.tsx');
205
205
 
206
- expect(source).toMatch(/pointerEvents=\{passive \? 'none' : 'auto'\}/);
206
+ expect(source).toMatch(/pointerEvents:\s*passive \? 'none' : 'auto'/);
207
207
  expect(source).toMatch(
208
208
  /onValueChange=\{\s*\(\s*itemValue\s*\)\s*=>\s*\{\s*if\s*\(\s*!passive\s*\)/,
209
209
  );
@@ -60,6 +60,17 @@ describe('resolveRouteLabel', () => {
60
60
 
61
61
  expect(result).toEqual({ label: 'profile', source: 'name' });
62
62
  });
63
+
64
+ test('uses descriptor title instead of invoking a callable Expo tab label', () => {
65
+ const result = resolveRouteLabel({
66
+ route: { key: 'k4', name: 'home' },
67
+ metadata: undefined,
68
+ descriptor: { options: { tabBarLabel: () => 'callable', title: 'Home title' } },
69
+ kind: 'tab',
70
+ });
71
+
72
+ expect(result).toEqual({ label: 'Home title', source: 'descriptor' });
73
+ });
63
74
  });
64
75
 
65
76
  describe('resolveNavigationItems', () => {
@@ -8,8 +8,8 @@ import type { ZoraNavigationRouteMap } from '../components/navigation-list';
8
8
 
9
9
  interface ZoraNavigationDescriptorOptions {
10
10
  title?: string;
11
- tabBarLabel?: string | React.ReactNode;
12
- drawerLabel?: string | React.ReactNode;
11
+ tabBarLabel?: unknown;
12
+ drawerLabel?: unknown;
13
13
  }
14
14
 
15
15
  export interface ZoraNavigationDescriptor {
@@ -71,13 +71,14 @@ export function resolveRouteLabel({
71
71
  }
72
72
 
73
73
  const options = descriptor?.options;
74
+ const configuredLabel = kind === 'tab' ? options?.tabBarLabel : options?.drawerLabel;
74
75
  const fallback =
75
- kind === 'tab'
76
- ? (options?.tabBarLabel ?? options?.title)
77
- : (options?.drawerLabel ?? options?.title);
76
+ configuredLabel === undefined || typeof configuredLabel === 'function'
77
+ ? options?.title
78
+ : configuredLabel;
78
79
 
79
80
  if (fallback !== undefined) {
80
- return { label: fallback, source: 'descriptor' };
81
+ return { label: fallback as React.ReactNode, source: 'descriptor' };
81
82
  }
82
83
 
83
84
  return { label: route.name, source: 'name' };
@@ -24,11 +24,7 @@ function AppShellInner({
24
24
 
25
25
  {footer ? <View style={styles.slot}>{footer}</View> : null}
26
26
 
27
- {overlay ? (
28
- <View pointerEvents="box-none" style={styles.overlay}>
29
- {overlay}
30
- </View>
31
- ) : null}
27
+ {overlay ? <View style={[StyleSheet.absoluteFill, styles.overlay]}>{overlay}</View> : null}
32
28
  </View>
33
29
  </Box>
34
30
  );
@@ -63,7 +59,7 @@ const styles = StyleSheet.create({
63
59
  flexShrink: 0,
64
60
  },
65
61
  overlay: {
66
- ...StyleSheet.absoluteFillObject,
62
+ pointerEvents: 'box-none',
67
63
  zIndex: 10,
68
64
  },
69
65
  });
@@ -27,7 +27,7 @@ describe('ZORA_COMPONENT_META registry coverage', () => {
27
27
  .flatMap((match) => match[1].split(',').map((item) => item.trim()))
28
28
  .map((item) => item.split(' as ')[0].trim())
29
29
  .filter((name) => /^[A-Z][A-Za-z0-9]+$/.test(name))
30
- .filter((name) => !['ToastProvider'].includes(name));
30
+ .filter((name) => !['GradientRendererProvider', 'ToastProvider'].includes(name));
31
31
  componentExports.push('ThemeModeToggle');
32
32
 
33
33
  for (const name of componentExports) {
@@ -40,6 +40,7 @@ describe('ZORA_COMPONENT_META registry coverage', () => {
40
40
 
41
41
  test('does not treat providers/scopes as direct manifest UI nodes', () => {
42
42
  expect(ZORA_COMPONENT_META.ToastProvider?.directManifestNode).toBe(false);
43
+ expect(ZORA_COMPONENT_META.GradientRendererProvider).toBeUndefined();
43
44
  expect(ZORA_COMPONENT_META.ZoraProvider).toBeUndefined();
44
45
  });
45
46
  });
@@ -22,13 +22,15 @@ function isOptionalPeer(metadata: unknown, packageName: string): boolean {
22
22
  return isRecord(peerMetadata) && peerMetadata.optional === true;
23
23
  }
24
24
 
25
- test('root-entry native imports are required peers', async () => {
25
+ test('root-entry native imports are required portable peers', async () => {
26
26
  const packageJson = JSON.parse(
27
27
  await readFile(new URL('../package.json', import.meta.url), 'utf8'),
28
28
  ) as unknown;
29
29
  if (!isRecord(packageJson)) throw new Error('Expected package.json to contain an object.');
30
30
 
31
- const { peerDependenciesMeta } = packageJson;
32
- expect(isOptionalPeer(peerDependenciesMeta, 'expo-linear-gradient')).toBe(false);
31
+ const { peerDependencies, peerDependenciesMeta } = packageJson;
33
32
  expect(isOptionalPeer(peerDependenciesMeta, '@react-native-picker/picker')).toBe(false);
33
+ expect(isRecord(peerDependencies) && peerDependencies['expo-linear-gradient']).toBeUndefined();
34
+ expect(isRecord(peerDependencies) && peerDependencies['expo-font']).toBeUndefined();
35
+ expect(isRecord(peerDependencies) && peerDependencies['@expo/vector-icons']).toBeUndefined();
34
36
  });
@@ -23,6 +23,31 @@ describe('OAuth provider UI helpers', () => {
23
23
  expect(resolveOAuthProviderIcon('google')).toEqual({ provider: 'FontAwesome', name: 'google' });
24
24
  expect(resolveOAuthProviderIcon('github')).toEqual({ provider: 'FontAwesome', name: 'github' });
25
25
  expect(DEFAULT_OAUTH_PROVIDER_ICONS.apple).toEqual({ provider: 'FontAwesome', name: 'apple' });
26
+ expect(DEFAULT_OAUTH_PROVIDER_ICONS.microsoft).toEqual({
27
+ provider: 'FontAwesome5',
28
+ name: 'microsoft',
29
+ variant: 'brand',
30
+ });
31
+ expect(DEFAULT_OAUTH_PROVIDER_ICONS.discord).toEqual({
32
+ provider: 'FontAwesome5',
33
+ name: 'discord',
34
+ variant: 'brand',
35
+ });
36
+ expect(DEFAULT_OAUTH_PROVIDER_ICONS.gitlab).toEqual({
37
+ provider: 'FontAwesome5',
38
+ name: 'gitlab',
39
+ variant: 'brand',
40
+ });
41
+ expect(DEFAULT_OAUTH_PROVIDER_ICONS.zoom).toEqual({
42
+ provider: 'FontAwesome5',
43
+ name: 'video',
44
+ variant: 'solid',
45
+ });
46
+ expect(DEFAULT_OAUTH_PROVIDER_ICONS.x).toEqual({
47
+ provider: 'FontAwesome6',
48
+ name: 'x-twitter',
49
+ variant: 'brand',
50
+ });
26
51
  });
27
52
 
28
53
  it('returns undefined for custom provider icons', () => {
@@ -2,22 +2,22 @@ import type { OAuthProviderIconSpec } from './types';
2
2
 
3
3
  export const DEFAULT_OAUTH_PROVIDER_ICONS = {
4
4
  apple: { provider: 'FontAwesome', name: 'apple' },
5
- azure: { provider: 'FontAwesome5', name: 'microsoft' },
5
+ azure: { provider: 'FontAwesome5', name: 'microsoft', variant: 'brand' },
6
6
  bitbucket: { provider: 'FontAwesome', name: 'bitbucket' },
7
- discord: { provider: 'FontAwesome5', name: 'discord' },
7
+ discord: { provider: 'FontAwesome5', name: 'discord', variant: 'brand' },
8
8
  facebook: { provider: 'FontAwesome', name: 'facebook' },
9
- figma: { provider: 'FontAwesome5', name: 'figma' },
9
+ figma: { provider: 'FontAwesome5', name: 'figma', variant: 'brand' },
10
10
  github: { provider: 'FontAwesome', name: 'github' },
11
- gitlab: { provider: 'FontAwesome5', name: 'gitlab' },
11
+ gitlab: { provider: 'FontAwesome5', name: 'gitlab', variant: 'brand' },
12
12
  google: { provider: 'FontAwesome', name: 'google' },
13
13
  linkedin: { provider: 'FontAwesome', name: 'linkedin' },
14
- microsoft: { provider: 'FontAwesome5', name: 'microsoft' },
15
- slack: { provider: 'FontAwesome5', name: 'slack' },
14
+ microsoft: { provider: 'FontAwesome5', name: 'microsoft', variant: 'brand' },
15
+ slack: { provider: 'FontAwesome5', name: 'slack', variant: 'brand' },
16
16
  spotify: { provider: 'FontAwesome', name: 'spotify' },
17
- twitch: { provider: 'FontAwesome5', name: 'twitch' },
17
+ twitch: { provider: 'FontAwesome5', name: 'twitch', variant: 'brand' },
18
18
  twitter: { provider: 'FontAwesome', name: 'twitter' },
19
- x: { provider: 'FontAwesome6', name: 'x-twitter' },
20
- zoom: { provider: 'FontAwesome5', name: 'video' },
19
+ x: { provider: 'FontAwesome6', name: 'x-twitter', variant: 'brand' },
20
+ zoom: { provider: 'FontAwesome5', name: 'video', variant: 'solid' },
21
21
  } as const satisfies Record<string, OAuthProviderIconSpec>;
22
22
 
23
23
  const DEFAULT_OAUTH_PROVIDER_LABELS = {
@@ -1,3 +1,4 @@
1
+ import type { ButtonIconSpec } from '@ankhorage/surface';
1
2
  import type React from 'react';
2
3
 
3
4
  import type { ButtonProps } from '../../components/button';
@@ -67,12 +68,7 @@ export interface OtpFormProps extends AuthFormBaseProps {
67
68
  onResend?: () => void | Promise<void>;
68
69
  }
69
70
 
70
- export interface OAuthProviderIconSpec {
71
- name: string;
72
- provider?: string;
73
- size?: number | string;
74
- color?: string;
75
- }
71
+ export type OAuthProviderIconSpec = ButtonIconSpec;
76
72
 
77
73
  export interface OAuthProviderItem {
78
74
  id: string;
@@ -70,7 +70,7 @@ function BarcodeScannerViewInner({
70
70
  ) : (
71
71
  <View style={[styles.placeholder, placeholderStyle]} />
72
72
  )}
73
- <View pointerEvents="none" style={styles.overlay}>
73
+ <View style={[styles.overlay, { pointerEvents: 'none' }]}>
74
74
  <ScanOverlay
75
75
  cornerLabel={cornerLabel}
76
76
  description={overlayDescription}
@@ -69,9 +69,8 @@ function TimelineInner({
69
69
  >
70
70
  {item.icon ? (
71
71
  <Icon
72
+ {...item.icon}
72
73
  color={status === 'neutral' ? theme.semantics.content.muted : role.base}
73
- name={item.icon.name}
74
- provider={item.icon.provider}
75
74
  size={iconSize}
76
75
  />
77
76
  ) : null}
@@ -110,9 +110,8 @@ function ZoraTabBarInner({
110
110
  >
111
111
  {item.metadata?.icon ? (
112
112
  <Icon
113
+ {...item.metadata.icon}
113
114
  color={contentColor}
114
- name={item.metadata.icon.name}
115
- provider={item.metadata.icon.provider}
116
115
  size={compact ? theme.spacing.m : theme.spacing.l}
117
116
  />
118
117
  ) : null}
@@ -0,0 +1,14 @@
1
+ import { expect, test } from 'bun:test';
2
+
3
+ test('passes the isolated Surface 3 and RN Web 0.21 acceptance suite', () => {
4
+ const subprocess = Bun.spawnSync({
5
+ cmd: [process.execPath, 'test', 'test-fixtures/platformAcceptance.test.tsx'],
6
+ stderr: 'pipe',
7
+ stdout: 'pipe',
8
+ });
9
+ const output = `${subprocess.stdout.toString()}\n${subprocess.stderr.toString()}`;
10
+
11
+ expect(subprocess.exitCode, output).toBe(0);
12
+ expect(output).toContain('4 pass');
13
+ expect(output).toContain('0 fail');
14
+ });
@@ -2,7 +2,11 @@ import { describe, expect, test } from 'bun:test';
2
2
 
3
3
  import type { ZoraComponentRegistry } from './index';
4
4
 
5
- const NON_RUNTIME_COMPONENT_EXPORTS = new Set(['SelectionProvider', 'ToastProvider']);
5
+ const NON_RUNTIME_COMPONENT_EXPORTS = new Set([
6
+ 'GradientRendererProvider',
7
+ 'SelectionProvider',
8
+ 'ToastProvider',
9
+ ]);
6
10
  const RUNTIME_THEME_COMPONENT_EXPORTS = ['ThemeModeToggle'] as const;
7
11
  const REGISTRY_SNAPSHOT_PREFIX = 'ZORA_REGISTRY_SNAPSHOT:';
8
12
 
@@ -210,7 +210,7 @@ describe('theme scope structure', () => {
210
210
  for (const filePath of scopedPropTypeFiles) {
211
211
  const source = readFileSync(filePath, 'utf8');
212
212
  expect(source).toMatch(/ZoraBaseProps/);
213
- expect(source).toMatch(/extends\s+ZoraBaseProps/);
213
+ expect(source).toMatch(/extends\s+ZoraBaseProps|ZoraBaseProps\s*&|&\s*ZoraBaseProps/);
214
214
  }
215
215
  });
216
216