@ankhorage/zora 2.13.1 → 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 (121) hide show
  1. package/CHANGELOG.md +20 -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/layout/sidebar-layout/SidebarLayout.d.ts +3 -1
  60. package/dist/layout/sidebar-layout/SidebarLayout.d.ts.map +1 -1
  61. package/dist/layout/sidebar-layout/SidebarLayout.js +14 -6
  62. package/dist/layout/sidebar-layout/SidebarLayout.js.map +1 -1
  63. package/dist/layout/sidebar-layout/resolveSidebarLayoutSizing.d.ts +14 -0
  64. package/dist/layout/sidebar-layout/resolveSidebarLayoutSizing.d.ts.map +1 -0
  65. package/dist/layout/sidebar-layout/resolveSidebarLayoutSizing.js +16 -0
  66. package/dist/layout/sidebar-layout/resolveSidebarLayoutSizing.js.map +1 -0
  67. package/dist/layout/sidebar-layout/types.d.ts +2 -0
  68. package/dist/layout/sidebar-layout/types.d.ts.map +1 -1
  69. package/dist/layout/sidebar-layout/types.js.map +1 -1
  70. package/dist/patterns/auth/oauthProviders.d.ts +9 -0
  71. package/dist/patterns/auth/oauthProviders.d.ts.map +1 -1
  72. package/dist/patterns/auth/oauthProviders.js +9 -9
  73. package/dist/patterns/auth/oauthProviders.js.map +1 -1
  74. package/dist/patterns/auth/types.d.ts +2 -6
  75. package/dist/patterns/auth/types.d.ts.map +1 -1
  76. package/dist/patterns/auth/types.js.map +1 -1
  77. package/dist/patterns/scanner/BarcodeScannerView.js +1 -1
  78. package/dist/patterns/scanner/BarcodeScannerView.js.map +1 -1
  79. package/dist/patterns/timeline/Timeline.d.ts.map +1 -1
  80. package/dist/patterns/timeline/Timeline.js +1 -1
  81. package/dist/patterns/timeline/Timeline.js.map +1 -1
  82. package/dist/patterns/zora-tab-bar/ZoraTabBar.d.ts.map +1 -1
  83. package/dist/patterns/zora-tab-bar/ZoraTabBar.js +1 -1
  84. package/dist/patterns/zora-tab-bar/ZoraTabBar.js.map +1 -1
  85. package/package.json +34 -29
  86. package/src/components/action-sheet/ActionSheetItem.tsx +1 -6
  87. package/src/components/app-bar/AppBar.tsx +3 -3
  88. package/src/components/avatar/Avatar.tsx +1 -8
  89. package/src/components/breadcrumbs/Breadcrumbs.tsx +1 -2
  90. package/src/components/chip/Chip.tsx +1 -8
  91. package/src/components/gradient/Gradient.tsx +16 -12
  92. package/src/components/gradient/GradientRendererContext.tsx +27 -0
  93. package/src/components/gradient/index.ts +4 -0
  94. package/src/components/gradient/types.ts +17 -0
  95. package/src/components/icon/Icon.tsx +1 -1
  96. package/src/components/input/Input.tsx +2 -12
  97. package/src/components/menu/Menu.tsx +1 -8
  98. package/src/components/metric-card/MetricCard.tsx +1 -8
  99. package/src/components/select/Select.tsx +1 -1
  100. package/src/components/textarea/Textarea.tsx +2 -12
  101. package/src/expo57Migration.test.ts +125 -0
  102. package/src/index.ts +4 -1
  103. package/src/interactionPolicy.test.ts +1 -1
  104. package/src/internal/resolveZoraNavigationItems.test.ts +11 -0
  105. package/src/internal/resolveZoraNavigationItems.ts +7 -6
  106. package/src/layout/app-shell/AppShell.tsx +2 -6
  107. package/src/layout/sidebar-layout/SidebarLayout.test.ts +35 -0
  108. package/src/layout/sidebar-layout/SidebarLayout.tsx +17 -5
  109. package/src/layout/sidebar-layout/resolveSidebarLayoutSizing.ts +18 -0
  110. package/src/layout/sidebar-layout/types.ts +3 -0
  111. package/src/metadata/componentMeta.test.ts +2 -1
  112. package/src/nativePeerContract.test.ts +5 -3
  113. package/src/patterns/auth/oauthProviders.test.ts +25 -0
  114. package/src/patterns/auth/oauthProviders.ts +9 -9
  115. package/src/patterns/auth/types.ts +2 -6
  116. package/src/patterns/scanner/BarcodeScannerView.tsx +1 -1
  117. package/src/patterns/timeline/Timeline.tsx +1 -2
  118. package/src/patterns/zora-tab-bar/ZoraTabBar.tsx +1 -2
  119. package/src/platformAcceptance.test.ts +14 -0
  120. package/src/registry.test.ts +5 -1
  121. package/src/theme/themeScopeStructure.test.ts +1 -1
@@ -0,0 +1,27 @@
1
+ import React, { createContext, use } from 'react';
2
+
3
+ import type { GradientRenderer, GradientRendererProviderProps } from './types';
4
+
5
+ const GradientRendererContext = createContext<GradientRenderer | null>(null);
6
+
7
+ /**
8
+ * Supplies the host-owned renderer used by `Gradient`.
9
+ *
10
+ * Expo apps can adapt `expo-linear-gradient` here, while standalone React
11
+ * Native hosts can inject another compatible implementation.
12
+ */
13
+ export function GradientRendererProvider({ children, renderer }: GradientRendererProviderProps) {
14
+ return <GradientRendererContext value={renderer}>{children}</GradientRendererContext>;
15
+ }
16
+
17
+ export function useGradientRenderer(): GradientRenderer {
18
+ const renderer = use(GradientRendererContext);
19
+
20
+ if (!renderer) {
21
+ throw new Error(
22
+ 'Gradient requires a renderer. Wrap the app in GradientRendererProvider with a platform-compatible gradient implementation.',
23
+ );
24
+ }
25
+
26
+ return renderer;
27
+ }
@@ -1,8 +1,12 @@
1
1
  export { Gradient } from './Gradient';
2
+ export { GradientRendererProvider } from './GradientRendererContext';
2
3
  export type {
3
4
  GradientColor,
4
5
  GradientColors,
5
6
  GradientLocations,
6
7
  GradientPoint,
7
8
  GradientProps,
9
+ GradientRenderer,
10
+ GradientRendererProps,
11
+ GradientRendererProviderProps,
8
12
  } from './types';
@@ -1,4 +1,5 @@
1
1
  import type React from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
2
3
 
3
4
  import type { BoxProps } from '../../foundation';
4
5
  import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
@@ -12,6 +13,22 @@ export interface GradientPoint {
12
13
  readonly y: number;
13
14
  }
14
15
 
16
+ export interface GradientRendererProps {
17
+ children?: React.ReactNode;
18
+ colors: GradientColors;
19
+ locations?: GradientLocations;
20
+ start?: GradientPoint;
21
+ end?: GradientPoint;
22
+ style?: StyleProp<ViewStyle>;
23
+ }
24
+
25
+ export type GradientRenderer = React.ComponentType<GradientRendererProps>;
26
+
27
+ export interface GradientRendererProviderProps {
28
+ children?: React.ReactNode;
29
+ renderer: GradientRenderer;
30
+ }
31
+
15
32
  export interface GradientProps extends ZoraBaseProps {
16
33
  children?: React.ReactNode;
17
34
  colors: GradientColors;
@@ -4,7 +4,7 @@ import React from 'react';
4
4
  import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
5
5
  import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
6
6
 
7
- export interface IconProps extends ZoraBaseProps, Omit<SurfaceIconProps, 'mode' | 'themeId'> {}
7
+ export type IconProps = ZoraBaseProps & SurfaceIconProps;
8
8
 
9
9
  function IconInner({
10
10
  themeId: _themeId,
@@ -30,12 +30,7 @@ function InputInner({
30
30
  interactionPolicy={interactionPolicy}
31
31
  leadingAccessory={
32
32
  leadingIcon ? (
33
- <Surface.Icon
34
- color={iconColor}
35
- name={leadingIcon.name}
36
- provider={leadingIcon.provider}
37
- size={iconSize}
38
- />
33
+ <Surface.Icon {...leadingIcon} color={iconColor} size={iconSize} />
39
34
  ) : undefined
40
35
  }
41
36
  readOnly={readOnly}
@@ -53,12 +48,7 @@ function InputInner({
53
48
  onPress={trailingAction.onPress}
54
49
  />
55
50
  ) : trailingIcon ? (
56
- <Surface.Icon
57
- color={iconColor}
58
- name={trailingIcon.name}
59
- provider={trailingIcon.provider}
60
- size={iconSize}
61
- />
51
+ <Surface.Icon {...trailingIcon} color={iconColor} size={iconSize} />
62
52
  ) : undefined
63
53
  }
64
54
  />
@@ -17,14 +17,7 @@ function renderActionLeading(action: MenuAction, iconColor: string) {
17
17
  return undefined;
18
18
  }
19
19
 
20
- return (
21
- <Icon
22
- color={iconColor}
23
- name={action.icon.name}
24
- provider={action.icon.provider}
25
- size={resolveIconSize('s')}
26
- />
27
- );
20
+ return <Icon {...action.icon} color={iconColor} size={resolveIconSize('s')} />;
28
21
  }
29
22
 
30
23
  function renderActionTrailing(action: MenuAction) {
@@ -45,14 +45,7 @@ function MetricCardInner({
45
45
  <Inline align="flex-start" gap="m" justify="space-between">
46
46
  <Stack flex={1} gap="xs">
47
47
  <Inline align="center" gap="xs" wrap="wrap">
48
- {icon ? (
49
- <Icon
50
- color={iconColor}
51
- name={icon.name}
52
- provider={icon.provider}
53
- size={resolveIconSize('s')}
54
- />
55
- ) : null}
48
+ {icon ? <Icon {...icon} color={iconColor} size={resolveIconSize('s')} /> : null}
56
49
  <Text emphasis="muted" variant="caption" weight="semiBold">
57
50
  {label}
58
51
  </Text>
@@ -30,7 +30,7 @@ function SelectInner<TValue extends string = string>({
30
30
  radius="m"
31
31
  testID={testID}
32
32
  >
33
- <View pointerEvents={passive ? 'none' : 'auto'}>
33
+ <View style={{ pointerEvents: passive ? 'none' : 'auto' }}>
34
34
  <Picker
35
35
  enabled={!disabled}
36
36
  onValueChange={(itemValue) => {
@@ -25,23 +25,13 @@ function TextareaInner({
25
25
  interactionPolicy={interactionPolicy}
26
26
  leadingAccessory={
27
27
  leadingIcon ? (
28
- <Surface.Icon
29
- color={iconColor}
30
- name={leadingIcon.name}
31
- provider={leadingIcon.provider}
32
- size={iconSize}
33
- />
28
+ <Surface.Icon {...leadingIcon} color={iconColor} size={iconSize} />
34
29
  ) : undefined
35
30
  }
36
31
  size={size}
37
32
  trailingAccessory={
38
33
  trailingIcon ? (
39
- <Surface.Icon
40
- color={iconColor}
41
- name={trailingIcon.name}
42
- provider={trailingIcon.provider}
43
- size={iconSize}
44
- />
34
+ <Surface.Icon {...trailingIcon} color={iconColor} size={iconSize} />
45
35
  ) : undefined
46
36
  }
47
37
  />
@@ -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
  });
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolveSidebarLayoutSizing } from './resolveSidebarLayoutSizing';
4
+
5
+ describe('SidebarLayout sizing contract', () => {
6
+ it('keeps content-flow sizing as the default', () => {
7
+ expect(resolveSidebarLayoutSizing()).toEqual({
8
+ root: {
9
+ align: 'flex-start',
10
+ flex: undefined,
11
+ minHeight: undefined,
12
+ minWidth: undefined,
13
+ },
14
+ child: {
15
+ minHeight: undefined,
16
+ minWidth: undefined,
17
+ },
18
+ });
19
+ });
20
+
21
+ it('fills a bounded parent for child-owned scroll areas', () => {
22
+ expect(resolveSidebarLayoutSizing('fill')).toEqual({
23
+ root: {
24
+ align: 'stretch',
25
+ flex: 1,
26
+ minHeight: 0,
27
+ minWidth: 0,
28
+ },
29
+ child: {
30
+ minHeight: 0,
31
+ minWidth: 0,
32
+ },
33
+ });
34
+ });
35
+ });
@@ -2,6 +2,7 @@ import React from 'react';
2
2
 
3
3
  import { Box, Stack } from '../../foundation';
4
4
  import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
5
+ import { resolveSidebarLayoutSizing } from './resolveSidebarLayoutSizing';
5
6
  import type { SidebarLayoutProps } from './types';
6
7
 
7
8
  function SidebarLayoutInner({
@@ -13,15 +14,24 @@ function SidebarLayoutInner({
13
14
  aside,
14
15
  sidebarWidth = 280,
15
16
  asideWidth = 280,
17
+ sizing,
16
18
  testID,
17
19
  }: SidebarLayoutProps) {
20
+ const layoutSizing = resolveSidebarLayoutSizing(sizing);
21
+
18
22
  return (
19
- <Stack direction={{ base: 'column', lg: 'row' }} gap="l" testID={testID} align="flex-start">
20
- <Box width={{ base: '100%', lg: sidebarWidth }}>{sidebar}</Box>
21
- <Box flex={1} width="100%">
23
+ <Stack {...layoutSizing.root} direction={{ base: 'column', lg: 'row' }} gap="l" testID={testID}>
24
+ <Box {...layoutSizing.child} width={{ base: '100%', lg: sidebarWidth }}>
25
+ {sidebar}
26
+ </Box>
27
+ <Box {...layoutSizing.child} flex={1} width="100%">
22
28
  {children}
23
29
  </Box>
24
- {aside ? <Box width={{ base: '100%', lg: asideWidth }}>{aside}</Box> : null}
30
+ {aside ? (
31
+ <Box {...layoutSizing.child} width={{ base: '100%', lg: asideWidth }}>
32
+ {aside}
33
+ </Box>
34
+ ) : null}
25
35
  </Stack>
26
36
  );
27
37
  }
@@ -29,6 +39,8 @@ function SidebarLayoutInner({
29
39
  /***
30
40
  * Responsive layout with a sidebar and main content area (and optional aside).
31
41
  *
32
-
42
+ * `sizing="content"` keeps normal content-flow behavior. Use `sizing="fill"` when the layout
43
+ * should fill an already bounded parent so sidebar/content children receive the available space
44
+ * for their own scrolling or gesture ownership.
33
45
  */
34
46
  export const SidebarLayout = withZoraThemeScope(SidebarLayoutInner);
@@ -0,0 +1,18 @@
1
+ import type { SidebarLayoutSizing } from './types';
2
+
3
+ export function resolveSidebarLayoutSizing(sizing: SidebarLayoutSizing = 'content') {
4
+ const fillsAvailableSpace = sizing === 'fill';
5
+
6
+ return {
7
+ root: {
8
+ align: fillsAvailableSpace ? 'stretch' : 'flex-start',
9
+ flex: fillsAvailableSpace ? 1 : undefined,
10
+ minHeight: fillsAvailableSpace ? 0 : undefined,
11
+ minWidth: fillsAvailableSpace ? 0 : undefined,
12
+ },
13
+ child: {
14
+ minHeight: fillsAvailableSpace ? 0 : undefined,
15
+ minWidth: fillsAvailableSpace ? 0 : undefined,
16
+ },
17
+ } as const;
18
+ }
@@ -2,10 +2,13 @@ import type React from 'react';
2
2
 
3
3
  import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
4
4
 
5
+ export type SidebarLayoutSizing = 'content' | 'fill';
6
+
5
7
  export interface SidebarLayoutProps extends ZoraBaseProps {
6
8
  sidebar: React.ReactNode;
7
9
  children?: React.ReactNode;
8
10
  aside?: React.ReactNode;
9
11
  sidebarWidth?: number;
10
12
  asideWidth?: number;
13
+ sizing?: SidebarLayoutSizing;
11
14
  }
@@ -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}