@ankhorage/expo-runtime 3.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @ankhorage/expo-runtime
2
2
 
3
+ ## 3.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 979cb5b: Pin the audited EPUB/PDF renderer foundations and document the isolated DOM, content-safety, and deterministic fixture contract for the reader adapter.
8
+ - 162fb2a: Update Ankhorage dependencies: `@ankhorage/contracts`.
9
+
3
10
  ## 3.1.0
4
11
 
5
12
  ### Minor Changes
@@ -0,0 +1,9 @@
1
+ export declare function resolveReaderKeyboardIntent(input: {
2
+ canGoNext: boolean;
3
+ canGoPrevious: boolean;
4
+ direction: 'ltr' | 'rtl';
5
+ hasModifier?: boolean;
6
+ isInteractiveTarget?: boolean;
7
+ key: string;
8
+ }): 'next' | 'previous' | null;
9
+ //# sourceMappingURL=resolveReaderKeyboardIntent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveReaderKeyboardIntent.d.ts","sourceRoot":"","sources":["../../../src/reader/navigation/resolveReaderKeyboardIntent.ts"],"names":[],"mappings":"AAAA,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IACjD,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,MAAM,GAAG,UAAU,GAAG,IAAI,CAY7B"}
@@ -0,0 +1,19 @@
1
+ export function resolveReaderKeyboardIntent(input) {
2
+ if (input.hasModifier || input.isInteractiveTarget)
3
+ return null;
4
+ let intent = null;
5
+ if (input.key === 'PageUp')
6
+ intent = 'previous';
7
+ if (input.key === 'PageDown')
8
+ intent = 'next';
9
+ if (input.key === 'ArrowLeft')
10
+ intent = input.direction === 'ltr' ? 'previous' : 'next';
11
+ if (input.key === 'ArrowRight')
12
+ intent = input.direction === 'ltr' ? 'next' : 'previous';
13
+ if (intent === 'next')
14
+ return input.canGoNext ? intent : null;
15
+ if (intent === 'previous')
16
+ return input.canGoPrevious ? intent : null;
17
+ return null;
18
+ }
19
+ //# sourceMappingURL=resolveReaderKeyboardIntent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveReaderKeyboardIntent.js","sourceRoot":"","sources":["../../../src/reader/navigation/resolveReaderKeyboardIntent.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,2BAA2B,CAAC,KAO3C;IACC,IAAI,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,mBAAmB;QAAE,OAAO,IAAI,CAAC;IAEhE,IAAI,MAAM,GAA+B,IAAI,CAAC;IAC9C,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ;QAAE,MAAM,GAAG,UAAU,CAAC;IAChD,IAAI,KAAK,CAAC,GAAG,KAAK,UAAU;QAAE,MAAM,GAAG,MAAM,CAAC;IAC9C,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW;QAAE,MAAM,GAAG,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;IACxF,IAAI,KAAK,CAAC,GAAG,KAAK,YAAY;QAAE,MAAM,GAAG,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;IAEzF,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9D,IAAI,MAAM,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["export function resolveReaderKeyboardIntent(input: {\n canGoNext: boolean;\n canGoPrevious: boolean;\n direction: 'ltr' | 'rtl';\n hasModifier?: boolean;\n isInteractiveTarget?: boolean;\n key: string;\n}): 'next' | 'previous' | null {\n if (input.hasModifier || input.isInteractiveTarget) return null;\n\n let intent: 'next' | 'previous' | null = null;\n if (input.key === 'PageUp') intent = 'previous';\n if (input.key === 'PageDown') intent = 'next';\n if (input.key === 'ArrowLeft') intent = input.direction === 'ltr' ? 'previous' : 'next';\n if (input.key === 'ArrowRight') intent = input.direction === 'ltr' ? 'next' : 'previous';\n\n if (intent === 'next') return input.canGoNext ? intent : null;\n if (intent === 'previous') return input.canGoPrevious ? intent : null;\n return null;\n}\n"]}
@@ -0,0 +1,11 @@
1
+ export declare function resolveReaderNavigationIntent(input: {
2
+ canGoNext: boolean;
3
+ canGoPrevious: boolean;
4
+ deltaX: number;
5
+ deltaY: number;
6
+ direction: 'ltr' | 'rtl';
7
+ hasSelection?: boolean;
8
+ isInteractiveTarget?: boolean;
9
+ viewportWidth: number;
10
+ }): 'next' | 'previous' | null;
11
+ //# sourceMappingURL=resolveReaderNavigationIntent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveReaderNavigationIntent.d.ts","sourceRoot":"","sources":["../../../src/reader/navigation/resolveReaderNavigationIntent.ts"],"names":[],"mappings":"AAAA,wBAAgB,6BAA6B,CAAC,KAAK,EAAE;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,MAAM,GAAG,UAAU,GAAG,IAAI,CAa7B"}
@@ -0,0 +1,16 @@
1
+ export function resolveReaderNavigationIntent(input) {
2
+ if (input.hasSelection || input.isInteractiveTarget)
3
+ return null;
4
+ const horizontalDistance = Math.abs(input.deltaX);
5
+ const verticalDistance = Math.abs(input.deltaY);
6
+ const distanceThreshold = Math.max(48, input.viewportWidth * 0.15);
7
+ if (horizontalDistance <= verticalDistance * 1.25)
8
+ return null;
9
+ if (horizontalDistance <= distanceThreshold)
10
+ return null;
11
+ const movesForward = input.direction === 'ltr' ? input.deltaX < 0 : input.deltaX > 0;
12
+ if (movesForward)
13
+ return input.canGoNext ? 'next' : null;
14
+ return input.canGoPrevious ? 'previous' : null;
15
+ }
16
+ //# sourceMappingURL=resolveReaderNavigationIntent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveReaderNavigationIntent.js","sourceRoot":"","sources":["../../../src/reader/navigation/resolveReaderNavigationIntent.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,6BAA6B,CAAC,KAS7C;IACC,IAAI,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,mBAAmB;QAAE,OAAO,IAAI,CAAC;IAEjE,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAEnE,IAAI,kBAAkB,IAAI,gBAAgB,GAAG,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/D,IAAI,kBAAkB,IAAI,iBAAiB;QAAE,OAAO,IAAI,CAAC;IAEzD,MAAM,YAAY,GAAG,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IACrF,IAAI,YAAY;QAAE,OAAO,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IACzD,OAAO,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;AACjD,CAAC","sourcesContent":["export function resolveReaderNavigationIntent(input: {\n canGoNext: boolean;\n canGoPrevious: boolean;\n deltaX: number;\n deltaY: number;\n direction: 'ltr' | 'rtl';\n hasSelection?: boolean;\n isInteractiveTarget?: boolean;\n viewportWidth: number;\n}): 'next' | 'previous' | null {\n if (input.hasSelection || input.isInteractiveTarget) return null;\n\n const horizontalDistance = Math.abs(input.deltaX);\n const verticalDistance = Math.abs(input.deltaY);\n const distanceThreshold = Math.max(48, input.viewportWidth * 0.15);\n\n if (horizontalDistance <= verticalDistance * 1.25) return null;\n if (horizontalDistance <= distanceThreshold) return null;\n\n const movesForward = input.direction === 'ltr' ? input.deltaX < 0 : input.deltaX > 0;\n if (movesForward) return input.canGoNext ? 'next' : null;\n return input.canGoPrevious ? 'previous' : null;\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/expo-runtime",
3
3
  "type": "module",
4
- "version": "3.1.0",
4
+ "version": "3.1.1",
5
5
  "description": "Declarative runtime integration for Expo apps: maps app capabilities to permissions, packages, config plugins, providers, and adapters",
6
6
  "homepage": "https://github.com/ankhorage/expo-runtime#readme",
7
7
  "bugs": {
@@ -25,7 +25,7 @@
25
25
  "runtime"
26
26
  ],
27
27
  "dependencies": {
28
- "@ankhorage/contracts": "^8.0.0"
28
+ "@ankhorage/contracts": "^8.0.1"
29
29
  },
30
30
  "files": [
31
31
  "dist",
@@ -127,11 +127,13 @@
127
127
  "peerDependencies": {
128
128
  "@ankhorage/permissions": "^0.2.3",
129
129
  "@ankhorage/zora": "^3.0.2",
130
+ "@readium/navigator": "2.8.2",
130
131
  "@react-native-vector-icons/fontawesome": "^13.1.3",
131
132
  "@react-native-vector-icons/fontawesome5": "^13.1.3",
132
133
  "@react-native-vector-icons/fontawesome6": "^13.1.3",
133
134
  "@react-native-vector-icons/ionicons": "^13.1.3",
134
135
  "@react-native-vector-icons/material-design-icons": "^13.1.3",
136
+ "@zip.js/zip.js": "2.9.0",
135
137
  "expo": "57.0.18",
136
138
  "expo-camera": "~57.0.4",
137
139
  "expo-constants": "~57.0.16",
@@ -139,6 +141,7 @@
139
141
  "expo-file-system": "~57.0.6",
140
142
  "expo-font": "~57.0.2",
141
143
  "expo-image-picker": "~57.0.14",
144
+ "pdfjs-dist": "6.3.289",
142
145
  "react": "19.2.3",
143
146
  "react-native": "0.86.3"
144
147
  },
@@ -149,6 +152,9 @@
149
152
  "@ankhorage/zora": {
150
153
  "optional": true
151
154
  },
155
+ "@readium/navigator": {
156
+ "optional": true
157
+ },
152
158
  "@react-native-vector-icons/fontawesome": {
153
159
  "optional": true
154
160
  },
@@ -164,6 +170,9 @@
164
170
  "@react-native-vector-icons/material-design-icons": {
165
171
  "optional": true
166
172
  },
173
+ "@zip.js/zip.js": {
174
+ "optional": true
175
+ },
167
176
  "expo": {
168
177
  "optional": true
169
178
  },
@@ -185,6 +194,9 @@
185
194
  "expo-image-picker": {
186
195
  "optional": true
187
196
  },
197
+ "pdfjs-dist": {
198
+ "optional": true
199
+ },
188
200
  "react": {
189
201
  "optional": true
190
202
  },
@@ -197,12 +209,14 @@
197
209
  "@ankhorage/paradox": "^0.1.24",
198
210
  "@ankhorage/permissions": "^0.2.3",
199
211
  "@ankhorage/zora": "^3.0.2",
212
+ "@readium/navigator": "2.8.2",
200
213
  "@react-native-picker/picker": "2.11.4",
201
214
  "@react-native-vector-icons/fontawesome": "^13.1.3",
202
215
  "@react-native-vector-icons/fontawesome5": "^13.1.3",
203
216
  "@react-native-vector-icons/fontawesome6": "^13.1.3",
204
217
  "@react-native-vector-icons/ionicons": "^13.1.3",
205
218
  "@react-native-vector-icons/material-design-icons": "^13.1.3",
219
+ "@zip.js/zip.js": "2.9.0",
206
220
  "@types/bun": "^1.4.0",
207
221
  "@types/node": "^24.13.3",
208
222
  "@types/pngjs": "^6.0.5",
@@ -220,6 +234,7 @@
220
234
  "react-native": "0.86.3",
221
235
  "react-native-safe-area-context": "~5.7.0",
222
236
  "react-native-web": "~0.21.0",
237
+ "pdfjs-dist": "6.3.289",
223
238
  "playwright-core": "^1.62.1",
224
239
  "pngjs": "^7.0.0",
225
240
  "typescript": "~6.0.3"
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveReaderKeyboardIntent } from './resolveReaderKeyboardIntent';
4
+
5
+ const defaultInput = {
6
+ canGoNext: true,
7
+ canGoPrevious: true,
8
+ direction: 'ltr' as const,
9
+ key: 'ArrowRight',
10
+ };
11
+
12
+ describe('resolveReaderKeyboardIntent', () => {
13
+ test('maps directional keys in LTR and RTL publications', () => {
14
+ expect(resolveReaderKeyboardIntent(defaultInput)).toBe('next');
15
+ expect(resolveReaderKeyboardIntent({ ...defaultInput, key: 'ArrowLeft' })).toBe('previous');
16
+ expect(resolveReaderKeyboardIntent({ ...defaultInput, direction: 'rtl' })).toBe('previous');
17
+ expect(
18
+ resolveReaderKeyboardIntent({ ...defaultInput, direction: 'rtl', key: 'ArrowLeft' }),
19
+ ).toBe('next');
20
+ });
21
+
22
+ test('keeps PageUp and PageDown tied to logical page order', () => {
23
+ expect(resolveReaderKeyboardIntent({ ...defaultInput, key: 'PageUp' })).toBe('previous');
24
+ expect(
25
+ resolveReaderKeyboardIntent({ ...defaultInput, direction: 'rtl', key: 'PageDown' }),
26
+ ).toBe('next');
27
+ });
28
+
29
+ test('ignores modified, interactive, unsupported, and boundary input', () => {
30
+ expect(resolveReaderKeyboardIntent({ ...defaultInput, hasModifier: true })).toBeNull();
31
+ expect(resolveReaderKeyboardIntent({ ...defaultInput, isInteractiveTarget: true })).toBeNull();
32
+ expect(resolveReaderKeyboardIntent({ ...defaultInput, key: 'Enter' })).toBeNull();
33
+ expect(resolveReaderKeyboardIntent({ ...defaultInput, canGoNext: false })).toBeNull();
34
+ expect(
35
+ resolveReaderKeyboardIntent({ ...defaultInput, canGoPrevious: false, key: 'ArrowLeft' }),
36
+ ).toBeNull();
37
+ });
38
+ });
@@ -0,0 +1,20 @@
1
+ export function resolveReaderKeyboardIntent(input: {
2
+ canGoNext: boolean;
3
+ canGoPrevious: boolean;
4
+ direction: 'ltr' | 'rtl';
5
+ hasModifier?: boolean;
6
+ isInteractiveTarget?: boolean;
7
+ key: string;
8
+ }): 'next' | 'previous' | null {
9
+ if (input.hasModifier || input.isInteractiveTarget) return null;
10
+
11
+ let intent: 'next' | 'previous' | null = null;
12
+ if (input.key === 'PageUp') intent = 'previous';
13
+ if (input.key === 'PageDown') intent = 'next';
14
+ if (input.key === 'ArrowLeft') intent = input.direction === 'ltr' ? 'previous' : 'next';
15
+ if (input.key === 'ArrowRight') intent = input.direction === 'ltr' ? 'next' : 'previous';
16
+
17
+ if (intent === 'next') return input.canGoNext ? intent : null;
18
+ if (intent === 'previous') return input.canGoPrevious ? intent : null;
19
+ return null;
20
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveReaderNavigationIntent } from './resolveReaderNavigationIntent';
4
+
5
+ const defaultInput = {
6
+ canGoNext: true,
7
+ canGoPrevious: true,
8
+ deltaX: -49,
9
+ deltaY: 0,
10
+ direction: 'ltr' as const,
11
+ viewportWidth: 300,
12
+ };
13
+
14
+ describe('resolveReaderNavigationIntent', () => {
15
+ test('maps one qualified LTR swipe to its logical page direction', () => {
16
+ expect(resolveReaderNavigationIntent(defaultInput)).toBe('next');
17
+ expect(resolveReaderNavigationIntent({ ...defaultInput, deltaX: 49 })).toBe('previous');
18
+ });
19
+
20
+ test('inverts horizontal navigation for RTL publications', () => {
21
+ expect(resolveReaderNavigationIntent({ ...defaultInput, direction: 'rtl' })).toBe('previous');
22
+ expect(resolveReaderNavigationIntent({ ...defaultInput, deltaX: 49, direction: 'rtl' })).toBe(
23
+ 'next',
24
+ );
25
+ });
26
+
27
+ test('requires horizontal intent and a strict responsive distance threshold', () => {
28
+ expect(resolveReaderNavigationIntent({ ...defaultInput, deltaX: -100, deltaY: 80 })).toBeNull();
29
+ expect(resolveReaderNavigationIntent({ ...defaultInput, deltaX: -48 })).toBeNull();
30
+ expect(
31
+ resolveReaderNavigationIntent({ ...defaultInput, deltaX: -60, viewportWidth: 400 }),
32
+ ).toBeNull();
33
+ expect(
34
+ resolveReaderNavigationIntent({ ...defaultInput, deltaX: -61, viewportWidth: 400 }),
35
+ ).toBe('next');
36
+ });
37
+
38
+ test('does not claim selection, interactive content, or a document boundary', () => {
39
+ expect(resolveReaderNavigationIntent({ ...defaultInput, hasSelection: true })).toBeNull();
40
+ expect(
41
+ resolveReaderNavigationIntent({ ...defaultInput, isInteractiveTarget: true }),
42
+ ).toBeNull();
43
+ expect(resolveReaderNavigationIntent({ ...defaultInput, canGoNext: false })).toBeNull();
44
+ expect(
45
+ resolveReaderNavigationIntent({ ...defaultInput, canGoPrevious: false, deltaX: 49 }),
46
+ ).toBeNull();
47
+ });
48
+ });
@@ -0,0 +1,23 @@
1
+ export function resolveReaderNavigationIntent(input: {
2
+ canGoNext: boolean;
3
+ canGoPrevious: boolean;
4
+ deltaX: number;
5
+ deltaY: number;
6
+ direction: 'ltr' | 'rtl';
7
+ hasSelection?: boolean;
8
+ isInteractiveTarget?: boolean;
9
+ viewportWidth: number;
10
+ }): 'next' | 'previous' | null {
11
+ if (input.hasSelection || input.isInteractiveTarget) return null;
12
+
13
+ const horizontalDistance = Math.abs(input.deltaX);
14
+ const verticalDistance = Math.abs(input.deltaY);
15
+ const distanceThreshold = Math.max(48, input.viewportWidth * 0.15);
16
+
17
+ if (horizontalDistance <= verticalDistance * 1.25) return null;
18
+ if (horizontalDistance <= distanceThreshold) return null;
19
+
20
+ const movesForward = input.direction === 'ltr' ? input.deltaX < 0 : input.deltaX > 0;
21
+ if (movesForward) return input.canGoNext ? 'next' : null;
22
+ return input.canGoPrevious ? 'previous' : null;
23
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ describe('reader renderer package boundary', () => {
4
+ test('provides the EPUB navigator and archive APIs required by the driver', async () => {
5
+ const [{ EpubNavigator }, { BlobReader, ZipReader }] = await Promise.all([
6
+ import('@readium/navigator'),
7
+ import('@zip.js/zip.js'),
8
+ ]);
9
+
10
+ expect(EpubNavigator).toBeFunction();
11
+ expect(BlobReader).toBeFunction();
12
+ expect(ZipReader).toBeFunction();
13
+ });
14
+
15
+ test('provides the PDF loading, worker, and protected-document APIs', async () => {
16
+ const { getDocument, PasswordException, PDFWorker } = await import('pdfjs-dist');
17
+
18
+ expect(getDocument).toBeFunction();
19
+ expect(PasswordException).toBeFunction();
20
+ expect(PDFWorker).toBeFunction();
21
+ });
22
+ });