@ankhorage/zora 5.0.0 → 7.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 (185) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +14 -10
  3. package/dist/components/button/resolveButtonThemeRecipe.d.ts +1 -1
  4. package/dist/components/date-picker/DatePicker.d.ts.map +1 -1
  5. package/dist/components/date-picker/DatePicker.js +92 -86
  6. package/dist/components/date-picker/DatePicker.js.map +1 -1
  7. package/dist/components/date-picker/meta.d.ts +1 -1
  8. package/dist/components/date-picker/meta.js +1 -1
  9. package/dist/components/date-picker/meta.js.map +1 -1
  10. package/dist/components/heading/resolveHeadingThemeRecipe.d.ts +1 -1
  11. package/dist/components/image/Image.d.ts +1 -5
  12. package/dist/components/image/Image.d.ts.map +1 -1
  13. package/dist/components/image/Image.js +79 -8
  14. package/dist/components/image/Image.js.map +1 -1
  15. package/dist/components/image/types.d.ts +4 -2
  16. package/dist/components/image/types.d.ts.map +1 -1
  17. package/dist/components/image/types.js.map +1 -1
  18. package/dist/components/text/resolveTextThemeRecipe.d.ts +1 -1
  19. package/dist/components/time-picker/TimePicker.d.ts +1 -3
  20. package/dist/components/time-picker/TimePicker.d.ts.map +1 -1
  21. package/dist/components/time-picker/TimePicker.js +55 -32
  22. package/dist/components/time-picker/TimePicker.js.map +1 -1
  23. package/dist/components/time-picker/meta.d.ts +1 -1
  24. package/dist/components/time-picker/meta.js +1 -1
  25. package/dist/components/time-picker/meta.js.map +1 -1
  26. package/dist/features/bottom-sheet/public.d.ts +3 -0
  27. package/dist/features/bottom-sheet/public.d.ts.map +1 -0
  28. package/dist/features/bottom-sheet/public.js +2 -0
  29. package/dist/features/bottom-sheet/public.js.map +1 -0
  30. package/dist/features/uploader/adapters/inbound/Uploader.d.ts +5 -0
  31. package/dist/features/uploader/adapters/inbound/Uploader.d.ts.map +1 -0
  32. package/dist/features/uploader/adapters/inbound/Uploader.js +274 -0
  33. package/dist/features/uploader/adapters/inbound/Uploader.js.map +1 -0
  34. package/dist/features/uploader/adapters/outbound/pickDocumentOrFileAsync.d.ts +5 -0
  35. package/dist/features/uploader/adapters/outbound/pickDocumentOrFileAsync.d.ts.map +1 -0
  36. package/dist/features/uploader/adapters/outbound/pickDocumentOrFileAsync.js +38 -0
  37. package/dist/features/uploader/adapters/outbound/pickDocumentOrFileAsync.js.map +1 -0
  38. package/dist/features/uploader/adapters/outbound/pickImageOrVideoAsync.d.ts +5 -0
  39. package/dist/features/uploader/adapters/outbound/pickImageOrVideoAsync.d.ts.map +1 -0
  40. package/dist/features/uploader/adapters/outbound/pickImageOrVideoAsync.js +24 -0
  41. package/dist/features/uploader/adapters/outbound/pickImageOrVideoAsync.js.map +1 -0
  42. package/dist/features/uploader/application/ports/outbound/UploadPickerPort.d.ts +9 -0
  43. package/dist/features/uploader/application/ports/outbound/UploadPickerPort.d.ts.map +1 -0
  44. package/dist/features/uploader/application/ports/outbound/UploadPickerPort.js +2 -0
  45. package/dist/features/uploader/application/ports/outbound/UploadPickerPort.js.map +1 -0
  46. package/dist/features/uploader/application/use-cases/pickUploadAssetAsync.d.ts +5 -0
  47. package/dist/features/uploader/application/use-cases/pickUploadAssetAsync.d.ts.map +1 -0
  48. package/dist/features/uploader/application/use-cases/pickUploadAssetAsync.js +6 -0
  49. package/dist/features/uploader/application/use-cases/pickUploadAssetAsync.js.map +1 -0
  50. package/dist/features/uploader/application/use-cases/validateUploadAsset.d.ts +4 -0
  51. package/dist/features/uploader/application/use-cases/validateUploadAsset.d.ts.map +1 -0
  52. package/dist/features/uploader/application/use-cases/validateUploadAsset.js +77 -0
  53. package/dist/features/uploader/application/use-cases/validateUploadAsset.js.map +1 -0
  54. package/dist/features/uploader/composition/createUploadPicker.d.ts +4 -0
  55. package/dist/features/uploader/composition/createUploadPicker.d.ts.map +1 -0
  56. package/dist/features/uploader/composition/createUploadPicker.js +12 -0
  57. package/dist/features/uploader/composition/createUploadPicker.js.map +1 -0
  58. package/dist/features/uploader/meta.d.ts +10 -0
  59. package/dist/features/uploader/meta.d.ts.map +1 -0
  60. package/dist/features/uploader/meta.js +10 -0
  61. package/dist/features/uploader/meta.js.map +1 -0
  62. package/dist/features/uploader/public.d.ts +4 -0
  63. package/dist/features/uploader/public.d.ts.map +1 -0
  64. package/dist/features/uploader/public.js +3 -0
  65. package/dist/features/uploader/public.js.map +1 -0
  66. package/dist/index.d.ts +2 -6
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +1 -3
  69. package/dist/index.js.map +1 -1
  70. package/dist/metadata/componentMeta.d.ts.map +1 -1
  71. package/dist/metadata/componentMeta.js +2 -7
  72. package/dist/metadata/componentMeta.js.map +1 -1
  73. package/dist/registry.d.ts.map +1 -1
  74. package/dist/registry.js +2 -7
  75. package/dist/registry.js.map +1 -1
  76. package/dist/theme/ZoraProvider.d.ts +1 -1
  77. package/dist/theme/ZoraProvider.d.ts.map +1 -1
  78. package/dist/theme/ZoraProvider.js +3 -2
  79. package/dist/theme/ZoraProvider.js.map +1 -1
  80. package/dist/theme/useZoraTheme.d.ts +1 -1
  81. package/dist/types/upload.d.ts +53 -0
  82. package/dist/types/upload.d.ts.map +1 -0
  83. package/dist/types/upload.js +2 -0
  84. package/dist/types/upload.js.map +1 -0
  85. package/package.json +25 -3
  86. package/src/components/date-picker/DatePicker.tsx +197 -129
  87. package/src/components/date-picker/meta.ts +1 -1
  88. package/src/components/image/Image.tsx +136 -8
  89. package/src/components/image/types.ts +6 -2
  90. package/src/components/time-picker/TimePicker.tsx +147 -60
  91. package/src/components/time-picker/meta.ts +1 -1
  92. package/src/expo57Migration.test.ts +56 -0
  93. package/src/features/bottom-sheet/public.test.ts +39 -0
  94. package/src/features/bottom-sheet/public.ts +5 -0
  95. package/src/features/uploader/adapters/inbound/Uploader.tsx +403 -0
  96. package/src/features/uploader/adapters/outbound/pickDocumentOrFileAsync.ts +50 -0
  97. package/src/features/uploader/adapters/outbound/pickImageOrVideoAsync.ts +30 -0
  98. package/src/features/uploader/application/ports/outbound/UploadPickerPort.ts +10 -0
  99. package/src/features/uploader/application/use-cases/pickUploadAssetAsync.test.ts +48 -0
  100. package/src/features/uploader/application/use-cases/pickUploadAssetAsync.ts +14 -0
  101. package/src/features/uploader/application/use-cases/validateUploadAsset.test.ts +51 -0
  102. package/src/features/uploader/application/use-cases/validateUploadAsset.ts +111 -0
  103. package/src/features/uploader/composition/createUploadPicker.ts +14 -0
  104. package/src/features/uploader/meta.ts +11 -0
  105. package/src/features/uploader/public.test.ts +34 -0
  106. package/src/features/uploader/public.ts +10 -0
  107. package/src/index.ts +9 -14
  108. package/src/metadata/componentMeta.ts +2 -7
  109. package/src/platformAcceptance.test.ts +1 -1
  110. package/src/registry.test.ts +1 -1
  111. package/src/registry.ts +2 -7
  112. package/src/showcaseCoverage.test.ts +1 -2
  113. package/src/theme/ZoraProvider.tsx +3 -2
  114. package/src/types/upload.ts +62 -0
  115. package/dist/components/action-sheet/ActionSheet.d.ts +0 -7
  116. package/dist/components/action-sheet/ActionSheet.d.ts.map +0 -1
  117. package/dist/components/action-sheet/ActionSheet.js +0 -11
  118. package/dist/components/action-sheet/ActionSheet.js.map +0 -1
  119. package/dist/components/action-sheet/ActionSheetItem.d.ts +0 -7
  120. package/dist/components/action-sheet/ActionSheetItem.d.ts.map +0 -1
  121. package/dist/components/action-sheet/ActionSheetItem.js +0 -17
  122. package/dist/components/action-sheet/ActionSheetItem.js.map +0 -1
  123. package/dist/components/action-sheet/index.d.ts +0 -4
  124. package/dist/components/action-sheet/index.d.ts.map +0 -1
  125. package/dist/components/action-sheet/index.js +0 -3
  126. package/dist/components/action-sheet/index.js.map +0 -1
  127. package/dist/components/action-sheet/meta.d.ts +0 -17
  128. package/dist/components/action-sheet/meta.d.ts.map +0 -1
  129. package/dist/components/action-sheet/meta.js +0 -17
  130. package/dist/components/action-sheet/meta.js.map +0 -1
  131. package/dist/components/action-sheet/types.d.ts +0 -13
  132. package/dist/components/action-sheet/types.d.ts.map +0 -1
  133. package/dist/components/action-sheet/types.js +0 -2
  134. package/dist/components/action-sheet/types.js.map +0 -1
  135. package/dist/patterns/image-preview/ImagePreview.d.ts +0 -9
  136. package/dist/patterns/image-preview/ImagePreview.d.ts.map +0 -1
  137. package/dist/patterns/image-preview/ImagePreview.js +0 -46
  138. package/dist/patterns/image-preview/ImagePreview.js.map +0 -1
  139. package/dist/patterns/image-preview/index.d.ts +0 -3
  140. package/dist/patterns/image-preview/index.d.ts.map +0 -1
  141. package/dist/patterns/image-preview/index.js +0 -2
  142. package/dist/patterns/image-preview/index.js.map +0 -1
  143. package/dist/patterns/image-preview/meta.d.ts +0 -9
  144. package/dist/patterns/image-preview/meta.d.ts.map +0 -1
  145. package/dist/patterns/image-preview/meta.js +0 -9
  146. package/dist/patterns/image-preview/meta.js.map +0 -1
  147. package/dist/patterns/image-preview/types.d.ts +0 -36
  148. package/dist/patterns/image-preview/types.d.ts.map +0 -1
  149. package/dist/patterns/image-preview/types.js +0 -2
  150. package/dist/patterns/image-preview/types.js.map +0 -1
  151. package/dist/patterns/image-upload-field/ImageUploadField.d.ts +0 -9
  152. package/dist/patterns/image-upload-field/ImageUploadField.d.ts.map +0 -1
  153. package/dist/patterns/image-upload-field/ImageUploadField.js +0 -222
  154. package/dist/patterns/image-upload-field/ImageUploadField.js.map +0 -1
  155. package/dist/patterns/image-upload-field/index.d.ts +0 -3
  156. package/dist/patterns/image-upload-field/index.d.ts.map +0 -1
  157. package/dist/patterns/image-upload-field/index.js +0 -2
  158. package/dist/patterns/image-upload-field/index.js.map +0 -1
  159. package/dist/patterns/image-upload-field/meta.d.ts +0 -9
  160. package/dist/patterns/image-upload-field/meta.d.ts.map +0 -1
  161. package/dist/patterns/image-upload-field/meta.js +0 -9
  162. package/dist/patterns/image-upload-field/meta.js.map +0 -1
  163. package/dist/patterns/image-upload-field/types.d.ts +0 -35
  164. package/dist/patterns/image-upload-field/types.d.ts.map +0 -1
  165. package/dist/patterns/image-upload-field/types.js +0 -2
  166. package/dist/patterns/image-upload-field/types.js.map +0 -1
  167. package/dist/patterns/image-upload-field/uploadFlow.d.ts +0 -18
  168. package/dist/patterns/image-upload-field/uploadFlow.d.ts.map +0 -1
  169. package/dist/patterns/image-upload-field/uploadFlow.js +0 -106
  170. package/dist/patterns/image-upload-field/uploadFlow.js.map +0 -1
  171. package/src/components/action-sheet/ActionSheet.tsx +0 -14
  172. package/src/components/action-sheet/ActionSheetItem.tsx +0 -30
  173. package/src/components/action-sheet/index.ts +0 -3
  174. package/src/components/action-sheet/meta.ts +0 -19
  175. package/src/components/action-sheet/types.ts +0 -23
  176. package/src/patterns/image-preview/ImagePreview.tsx +0 -82
  177. package/src/patterns/image-preview/index.ts +0 -2
  178. package/src/patterns/image-preview/meta.ts +0 -10
  179. package/src/patterns/image-preview/types.ts +0 -41
  180. package/src/patterns/image-upload-field/ImageUploadField.tsx +0 -319
  181. package/src/patterns/image-upload-field/index.ts +0 -2
  182. package/src/patterns/image-upload-field/meta.ts +0 -10
  183. package/src/patterns/image-upload-field/types.ts +0 -41
  184. package/src/patterns/image-upload-field/uploadFlow.test.ts +0 -117
  185. package/src/patterns/image-upload-field/uploadFlow.ts +0 -145
@@ -1,10 +1,10 @@
1
1
  import { Field } from '@ankhorage/surface';
2
2
  import React from 'react';
3
- import { ScrollView } from 'react-native';
3
+ import { ScrollView, StyleSheet } from 'react-native';
4
4
 
5
+ import { useBottomSheet } from '../../features/bottom-sheet/public';
5
6
  import { Stack } from '../../foundation';
6
7
  import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
7
- import { ActionSheet, ActionSheetItem } from '../action-sheet';
8
8
  import { Button } from '../button';
9
9
  import { Text } from '../text';
10
10
  import type { TimePickerProps } from './types';
@@ -12,6 +12,7 @@ import type { TimePickerProps } from './types';
12
12
  const MINUTES_PER_DAY = 24 * 60;
13
13
  const DEFAULT_STEP_MINUTES = 30;
14
14
 
15
+ /*** Render the field label and optional supporting description. */
15
16
  function renderLabel(label: React.ReactNode, description: React.ReactNode | undefined) {
16
17
  return (
17
18
  <Stack gap="xs">
@@ -27,39 +28,34 @@ function renderLabel(label: React.ReactNode, description: React.ReactNode | unde
27
28
  );
28
29
  }
29
30
 
31
+ /*** Parse one HH:mm value to minutes since midnight. */
30
32
  function parseTimeToMinutes(value: string | undefined): number | undefined {
31
- if (!value) {
32
- return undefined;
33
- }
34
-
33
+ if (!value) return undefined;
35
34
  const match = /^(\d{2}):(\d{2})$/.exec(value);
36
- if (!match) {
37
- return undefined;
38
- }
35
+ if (!match) return undefined;
39
36
 
40
37
  const hours = Number(match[1]);
41
38
  const minutes = Number(match[2]);
42
- if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
43
- return undefined;
44
- }
45
-
39
+ if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return undefined;
46
40
  return hours * 60 + minutes;
47
41
  }
48
42
 
43
+ /*** Format minutes since midnight as HH:mm. */
49
44
  function formatMinutes(value: number): string {
50
45
  const hours = Math.floor(value / 60);
51
46
  const minutes = value % 60;
52
47
  return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
53
48
  }
54
49
 
50
+ /*** Normalize the configured time step to a positive whole minute. */
55
51
  function resolveStepMinutes(stepMinutes: number | undefined): number {
56
52
  if (stepMinutes === undefined || !Number.isFinite(stepMinutes) || stepMinutes <= 0) {
57
53
  return DEFAULT_STEP_MINUTES;
58
54
  }
59
-
60
55
  return Math.max(1, Math.floor(stepMinutes));
61
56
  }
62
57
 
58
+ /*** Generate selectable HH:mm values within the configured range. */
63
59
  function generateTimeOptions({
64
60
  minTime,
65
61
  maxTime,
@@ -71,11 +67,8 @@ function generateTimeOptions({
71
67
  const options: string[] = [];
72
68
 
73
69
  for (let minute = 0; minute < MINUTES_PER_DAY; minute += step) {
74
- if (minute >= min && minute <= max) {
75
- options.push(formatMinutes(minute));
76
- }
70
+ if (minute >= min && minute <= max) options.push(formatMinutes(minute));
77
71
  }
78
-
79
72
  return options;
80
73
  }
81
74
 
@@ -97,18 +90,33 @@ function TimePickerInner({
97
90
  testID,
98
91
  interactionPolicy,
99
92
  }: TimePickerProps) {
93
+ const { dismiss, present } = useBottomSheet();
100
94
  const passive = interactionPolicy === 'passive';
101
- const [visible, setVisible] = React.useState(false);
102
- const options = React.useMemo(
103
- () => generateTimeOptions({ maxTime, minTime, stepMinutes }),
104
- [maxTime, minTime, stepMinutes],
105
- );
106
95
  const displayValue = value ? (formatTime ? formatTime(value) : value) : placeholder;
107
96
 
97
+ /*** Present the time options through the shared BottomSheet controller. */
108
98
  function openPicker() {
109
99
  if (passive) return;
110
-
111
- setVisible(true);
100
+ present({
101
+ content: (
102
+ <TimePickerSheet
103
+ description={description}
104
+ formatTime={formatTime}
105
+ interactionPolicy={interactionPolicy}
106
+ label={label}
107
+ maxTime={maxTime}
108
+ minTime={minTime}
109
+ onDismiss={dismiss}
110
+ onSelect={(time) => {
111
+ onValueChange?.(time);
112
+ dismiss();
113
+ }}
114
+ stepMinutes={stepMinutes}
115
+ testID={testID}
116
+ value={value}
117
+ />
118
+ ),
119
+ });
112
120
  }
113
121
 
114
122
  return (
@@ -130,45 +138,124 @@ function TimePickerInner({
130
138
  >
131
139
  {displayValue}
132
140
  </Button>
133
- <ActionSheet
134
- description={description}
135
- interactionPolicy={interactionPolicy}
136
- onDismiss={() => {
137
- if (passive) return;
138
-
139
- setVisible(false);
140
- }}
141
- testID={testID ? `${testID}-sheet` : undefined}
142
- title={label ?? 'Choose time'}
143
- visible={visible}
144
- >
145
- <ScrollView style={{ maxHeight: 360 }}>
146
- <Stack gap="xxs">
147
- {options.map((option) => (
148
- <ActionSheetItem
149
- interactionPolicy={interactionPolicy}
150
- key={option}
151
- label={formatTime ? formatTime(option) : option}
152
- onPress={() => {
153
- if (passive) return;
154
-
155
- onValueChange?.(option);
156
- setVisible(false);
157
- }}
158
- selected={option === value}
159
- testID={testID ? `${testID}-option-${option}` : undefined}
160
- />
161
- ))}
162
- </Stack>
163
- </ScrollView>
164
- </ActionSheet>
165
141
  </Field>
166
142
  );
167
143
  }
168
144
 
145
+ interface TimePickerSheetProps extends Pick<
146
+ TimePickerProps,
147
+ | 'description'
148
+ | 'formatTime'
149
+ | 'interactionPolicy'
150
+ | 'label'
151
+ | 'maxTime'
152
+ | 'minTime'
153
+ | 'stepMinutes'
154
+ | 'testID'
155
+ | 'value'
156
+ > {
157
+ onDismiss: () => void;
158
+ onSelect: (value: string) => void;
159
+ }
160
+
161
+ /*** Render time selection content inside the shared BottomSheet host. */
162
+ function TimePickerSheet({
163
+ description,
164
+ formatTime,
165
+ interactionPolicy,
166
+ label,
167
+ maxTime,
168
+ minTime,
169
+ onDismiss,
170
+ onSelect,
171
+ stepMinutes,
172
+ testID,
173
+ value,
174
+ }: TimePickerSheetProps) {
175
+ const options = React.useMemo(
176
+ () => generateTimeOptions({ maxTime, minTime, stepMinutes }),
177
+ [maxTime, minTime, stepMinutes],
178
+ );
179
+
180
+ return (
181
+ <Stack gap="m" p="m" testID={testID ? `${testID}-sheet` : undefined}>
182
+ <PickerSheetHeader description={description} title={label ?? 'Choose time'} />
183
+ <ScrollView style={styles.options}>
184
+ <Stack gap="xxs">
185
+ {options.map((option) => (
186
+ <TimePickerOption
187
+ formatTime={formatTime}
188
+ interactionPolicy={interactionPolicy}
189
+ key={option}
190
+ onSelect={onSelect}
191
+ option={option}
192
+ selected={option === value}
193
+ testID={testID}
194
+ />
195
+ ))}
196
+ </Stack>
197
+ </ScrollView>
198
+ <Button fullWidth interactionPolicy={interactionPolicy} onPress={onDismiss} variant="ghost">
199
+ Cancel
200
+ </Button>
201
+ </Stack>
202
+ );
203
+ }
204
+
205
+ /*** Render the shared title block used by the time sheet. */
206
+ function PickerSheetHeader({
207
+ description,
208
+ title,
209
+ }: {
210
+ description: React.ReactNode | undefined;
211
+ title: React.ReactNode;
212
+ }) {
213
+ return (
214
+ <Stack gap="xxs">
215
+ <Text align="center" variant="label" weight="semiBold">
216
+ {title}
217
+ </Text>
218
+ {description ? (
219
+ <Text align="center" emphasis="muted" variant="bodySmall">
220
+ {description}
221
+ </Text>
222
+ ) : null}
223
+ </Stack>
224
+ );
225
+ }
226
+
227
+ /*** Render one accessible selectable time option. */
228
+ function TimePickerOption({
229
+ formatTime,
230
+ interactionPolicy,
231
+ onSelect,
232
+ option,
233
+ selected,
234
+ testID,
235
+ }: Pick<TimePickerSheetProps, 'formatTime' | 'interactionPolicy' | 'testID'> & {
236
+ onSelect: (value: string) => void;
237
+ option: string;
238
+ selected: boolean;
239
+ }) {
240
+ return (
241
+ <Button
242
+ accessibilityState={{ selected }}
243
+ fullWidth
244
+ interactionPolicy={interactionPolicy}
245
+ onPress={() => onSelect(option)}
246
+ testID={testID ? `${testID}-option-${option}` : undefined}
247
+ variant={selected ? 'soft' : 'ghost'}
248
+ >
249
+ {formatTime ? formatTime(option) : option}
250
+ </Button>
251
+ );
252
+ }
253
+
254
+ const styles = StyleSheet.create({
255
+ options: { maxHeight: 360 },
256
+ });
257
+
169
258
  /***
170
- * Time input control with wheel selection and formatted display value.
171
- *
172
-
259
+ * Time input control with bottom-sheet selection and formatted display value.
173
260
  */
174
261
  export const TimePicker = withZoraThemeScope(TimePickerInner);
@@ -5,6 +5,6 @@ export const timePickerMeta = {
5
5
  category: 'component',
6
6
  directManifestNode: false,
7
7
  allowedChildren: [],
8
- note: 'Code-facing ActionSheet-backed time picker; not represented as a manifest node in v1.',
8
+ note: 'Code-facing BottomSheet-backed time picker; not represented as a manifest node in v1.',
9
9
  props: {},
10
10
  } as const satisfies ZoraComponentMeta;
@@ -14,6 +14,14 @@ const EXPO_EXAMPLE_DIRS = [
14
14
  'examples/social_community/private-messaging',
15
15
  'examples/social_community/visual-discovery',
16
16
  ] as const;
17
+ const ROUTER_EXAMPLE_DIRS = EXPO_EXAMPLE_DIRS.filter(
18
+ (directory) => directory !== 'examples/expo-showcase',
19
+ );
20
+ const ZORA_PROVIDER_HOSTS = [
21
+ ['examples/basic-app', 'App.tsx'],
22
+ ['examples/expo-showcase', 'App.tsx'],
23
+ ...ROUTER_EXAMPLE_DIRS.map((directory) => [directory, 'app', '_layout.tsx'] as const),
24
+ ] as const;
17
25
  const RNVI_PLUGINS = [
18
26
  '@react-native-vector-icons/fontawesome',
19
27
  '@react-native-vector-icons/fontawesome5',
@@ -63,6 +71,9 @@ describe('portable ZORA package boundary', () => {
63
71
  expect(readValue(peers, 'react-native-svg')).toBe('15.15.4');
64
72
  expect(readValue(development, 'react-native-svg')).toBe('15.15.4');
65
73
  expect(readValue(peers, 'react-native-web')).toMatch(TILDE_SEMVER_RANGE);
74
+ expect(readValue(peers, 'react-native-gesture-handler')).toMatch(TILDE_SEMVER_RANGE);
75
+ expect(readValue(peers, 'react-native-reanimated')).toMatch(EXACT_SEMVER_VERSION);
76
+ expect(readValue(peers, 'react-native-worklets')).toMatch(EXACT_SEMVER_VERSION);
66
77
  expect(development.typescript).toMatch(TILDE_SEMVER_RANGE);
67
78
 
68
79
  for (const expoPackage of ['@expo/vector-icons', 'expo-font', 'expo-linear-gradient']) {
@@ -101,6 +112,9 @@ describe('Expo 57 example boundary', () => {
101
112
  expect(expoVersion).toMatch(EXACT_SEMVER_VERSION);
102
113
  expect(dependencies.react).toMatch(EXACT_SEMVER_VERSION);
103
114
  expect(readValue(dependencies, 'react-native')).toMatch(EXACT_SEMVER_VERSION);
115
+ expect(readValue(dependencies, 'react-native-gesture-handler')).toMatch(TILDE_SEMVER_RANGE);
116
+ expect(readValue(dependencies, 'react-native-reanimated')).toMatch(EXACT_SEMVER_VERSION);
117
+ expect(readValue(dependencies, 'react-native-worklets')).toMatch(EXACT_SEMVER_VERSION);
104
118
  expect(readValue(dependencies, 'react-native-web')).toMatch(TILDE_SEMVER_RANGE);
105
119
  expect(development.typescript).toMatch(TILDE_SEMVER_RANGE);
106
120
  expect(readValue(dependencies, '@expo/vector-icons')).toBeUndefined();
@@ -117,6 +131,48 @@ describe('Expo 57 example boundary', () => {
117
131
  'utf8',
118
132
  );
119
133
  expect(scaffoldSource).toContain(`expo: '${expoVersion}',`);
134
+ expect(scaffoldSource).toContain("navigator: '^3.2.3'");
135
+ expect(scaffoldSource).toContain("from 'expo-router/unstable-native-tabs'");
136
+ expect(scaffoldSource).toContain('<GestureHandlerRootView style={{ flex: 1 }}>');
137
+ expect(scaffoldSource).not.toContain('ZoraTabBar');
138
+ });
139
+
140
+ test('uses Navigator native tabs in every router example', () => {
141
+ for (const directory of ROUTER_EXAMPLE_DIRS) {
142
+ const packageJson = readExamplePackage(directory);
143
+ const dependencies = readRecord(packageJson, 'dependencies');
144
+ const layoutSource = readFileSync(
145
+ join(ROOT, directory, 'app', '(tabs)', '_layout.tsx'),
146
+ 'utf8',
147
+ );
148
+
149
+ expect(readValue(dependencies, '@ankhorage/navigator')).toMatch(CARET_SEMVER_RANGE);
150
+ expect(layoutSource).toContain("from '@ankhorage/navigator/tabs/native-icons'");
151
+ expect(layoutSource).toContain("from 'expo-router/unstable-native-tabs'");
152
+ expect(layoutSource).toContain("Platform.OS === 'web'");
153
+ expect(layoutSource).toContain('Promise.resolve(null)');
154
+ expect(layoutSource).toContain('<NativeTabs>');
155
+ expect(layoutSource).not.toContain('ZoraTabBar');
156
+ expect(layoutSource).not.toContain('ZoraNavigationRouteMap');
157
+ }
158
+ });
159
+
160
+ test('keeps GestureHandlerRootView outside ZoraProvider in every host app', () => {
161
+ for (const sourcePath of ZORA_PROVIDER_HOSTS) {
162
+ const [directory] = sourcePath;
163
+ const source = readFileSync(join(ROOT, ...sourcePath), 'utf8');
164
+ const packageJson = readExamplePackage(directory);
165
+ const dependencies = readRecord(packageJson, 'dependencies');
166
+ const gestureRootPosition = source.indexOf('<GestureHandlerRootView');
167
+ const zoraProviderPosition = source.indexOf('<ZoraProvider');
168
+
169
+ expect(readValue(dependencies, 'react-native-gesture-handler')).toMatch(TILDE_SEMVER_RANGE);
170
+ expect(readValue(dependencies, 'react-native-reanimated')).toMatch(EXACT_SEMVER_VERSION);
171
+ expect(readValue(dependencies, 'react-native-worklets')).toMatch(EXACT_SEMVER_VERSION);
172
+ expect(source).toContain("from 'react-native-gesture-handler'");
173
+ expect(gestureRootPosition).toBeGreaterThan(-1);
174
+ expect(zoraProviderPosition).toBeGreaterThan(gestureRootPosition);
175
+ }
120
176
  });
121
177
 
122
178
  test('registers all scoped RNVI packages in each Expo app config', () => {
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ describe('ZORA BottomSheet public contract', () => {
4
+ test('publishes an explicit ZORA subpath backed by Surface', async () => {
5
+ const packageJson = (await Bun.file('package.json').json()) as {
6
+ dependencies: Readonly<Record<string, string>>;
7
+ exports: Readonly<Record<string, unknown>>;
8
+ peerDependencies: Readonly<Record<string, string>>;
9
+ };
10
+ const source = await Bun.file('src/features/bottom-sheet/public.ts').text();
11
+
12
+ expect(packageJson.exports['./bottom-sheet']).toBeDefined();
13
+ expect(packageJson.dependencies['@ankhorage/surface']).toMatch(/^\^4\./u);
14
+ expect(source).toContain("from '@ankhorage/surface/bottom-sheet'");
15
+ const {
16
+ 'react-native-gesture-handler': gestureHandlerPeer,
17
+ 'react-native-reanimated': reanimatedPeer,
18
+ 'react-native-worklets': workletsPeer,
19
+ } = packageJson.peerDependencies;
20
+ expect(gestureHandlerPeer).toBeDefined();
21
+ expect(reanimatedPeer).toBeDefined();
22
+ expect(workletsPeer).toBeDefined();
23
+ });
24
+
25
+ test('installs the shared provider and removes picker ActionSheet dependencies', async () => {
26
+ const [provider, datePicker, timePicker] = await Promise.all([
27
+ Bun.file('src/theme/ZoraProvider.tsx').text(),
28
+ Bun.file('src/components/date-picker/DatePicker.tsx').text(),
29
+ Bun.file('src/components/time-picker/TimePicker.tsx').text(),
30
+ ]);
31
+
32
+ expect(provider).toContain('<BottomSheetProvider>{children}</BottomSheetProvider>');
33
+ expect(provider).not.toContain('GestureHandlerRootView');
34
+ expect(datePicker).toContain('useBottomSheet()');
35
+ expect(timePicker).toContain('useBottomSheet()');
36
+ expect(datePicker).not.toContain("from '../action-sheet'");
37
+ expect(timePicker).not.toContain("from '../action-sheet'");
38
+ });
39
+ });
@@ -0,0 +1,5 @@
1
+ export type {
2
+ BottomSheetController,
3
+ BottomSheetPresentOptions,
4
+ } from '@ankhorage/surface/bottom-sheet';
5
+ export { useBottomSheet } from '@ankhorage/surface/bottom-sheet';