@ankhorage/zora 3.0.1 → 3.1.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.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/components/card/meta.d.ts +1 -1
- package/dist/components/form/types.d.ts.map +1 -1
- package/dist/components/form/types.js.map +1 -1
- package/dist/components/media-card/types.d.ts.map +1 -1
- package/dist/components/media-card/types.js.map +1 -1
- package/dist/components/text/Text.d.ts.map +1 -1
- package/dist/components/text/Text.js +2 -2
- package/dist/components/text/Text.js.map +1 -1
- package/dist/components/text/types.d.ts +2 -1
- package/dist/components/text/types.d.ts.map +1 -1
- package/dist/components/text/types.js.map +1 -1
- package/dist/foundation/meta.d.ts +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/screen/meta.d.ts +1 -1
- package/dist/layout/screen-section/meta.d.ts +1 -1
- package/dist/metadata/allowedChildren.d.ts +3 -3
- package/dist/metadata/allowedChildren.d.ts.map +1 -1
- package/dist/metadata/allowedChildren.js +1 -0
- package/dist/metadata/allowedChildren.js.map +1 -1
- package/dist/metadata/bindableComponentMeta.d.ts +485 -0
- package/dist/metadata/bindableComponentMeta.d.ts.map +1 -1
- package/dist/metadata/bindableComponentMeta.js +102 -0
- package/dist/metadata/bindableComponentMeta.js.map +1 -1
- package/dist/metadata/componentMeta.d.ts.map +1 -1
- package/dist/metadata/componentMeta.js +2 -0
- package/dist/metadata/componentMeta.js.map +1 -1
- package/dist/metadata/themeRecipeTypes.d.ts.map +1 -1
- package/dist/metadata/themeRecipeTypes.js.map +1 -1
- package/dist/metadata/types.d.ts.map +1 -1
- package/dist/metadata/types.js.map +1 -1
- package/dist/patterns/message-bubble/meta.d.ts +1 -1
- package/dist/patterns/notice/meta.d.ts +1 -1
- package/dist/patterns/panel/meta.d.ts +1 -1
- package/dist/patterns/post-card/meta.d.ts +1 -1
- package/dist/patterns/reader/ReaderSurface.d.ts +11 -0
- package/dist/patterns/reader/ReaderSurface.d.ts.map +1 -0
- package/dist/patterns/reader/ReaderSurface.js +95 -0
- package/dist/patterns/reader/ReaderSurface.js.map +1 -0
- package/dist/patterns/reader/index.d.ts +4 -0
- package/dist/patterns/reader/index.d.ts.map +1 -0
- package/dist/patterns/reader/index.js +3 -0
- package/dist/patterns/reader/index.js.map +1 -0
- package/dist/patterns/reader/meta.d.ts +328 -0
- package/dist/patterns/reader/meta.d.ts.map +1 -0
- package/dist/patterns/reader/meta.js +293 -0
- package/dist/patterns/reader/meta.js.map +1 -0
- package/dist/patterns/reader/resolveReaderProgress.d.ts +6 -0
- package/dist/patterns/reader/resolveReaderProgress.d.ts.map +1 -0
- package/dist/patterns/reader/resolveReaderProgress.js +17 -0
- package/dist/patterns/reader/resolveReaderProgress.js.map +1 -0
- package/dist/patterns/reader/types.d.ts +69 -0
- package/dist/patterns/reader/types.d.ts.map +1 -0
- package/dist/patterns/reader/types.js +2 -0
- package/dist/patterns/reader/types.js.map +1 -0
- package/dist/patterns/scanner/meta.d.ts +2 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +2 -0
- package/dist/registry.js.map +1 -1
- package/dist/theme/useZoraTheme.d.ts +1 -1
- package/package.json +11 -12
- package/src/components/form/types.ts +1 -2
- package/src/components/media-card/types.ts +1 -3
- package/src/components/text/Text.tsx +2 -0
- package/src/components/text/types.ts +7 -1
- package/src/expo57Migration.test.ts +18 -14
- package/src/index.ts +14 -0
- package/src/interactionPolicy.test.ts +30 -0
- package/src/metadata/allowedChildren.ts +1 -0
- package/src/metadata/bindableComponentMeta.test.ts +30 -0
- package/src/metadata/bindableComponentMeta.ts +102 -0
- package/src/metadata/componentMeta.test.ts +36 -0
- package/src/metadata/componentMeta.ts +2 -0
- package/src/metadata/themeRecipeTypes.ts +1 -3
- package/src/metadata/types.ts +1 -6
- package/src/patterns/reader/ReaderSurface.test.ts +46 -0
- package/src/patterns/reader/ReaderSurface.tsx +348 -0
- package/src/patterns/reader/index.ts +15 -0
- package/src/patterns/reader/meta.ts +296 -0
- package/src/patterns/reader/resolveReaderProgress.test.ts +31 -0
- package/src/patterns/reader/resolveReaderProgress.ts +29 -0
- package/src/patterns/reader/types.ts +80 -0
- package/src/registry.ts +2 -0
- package/src/showcaseCoverage.test.ts +1 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { UiComponentMetaRegistry } from '@ankhorage/contracts';
|
|
2
2
|
|
|
3
3
|
import { imageMeta } from '../components/image/meta';
|
|
4
|
+
import { readerSurfaceMeta } from '../patterns/reader/meta';
|
|
4
5
|
|
|
5
6
|
export const ZORA_BINDABLE_COMPONENT_META = {
|
|
6
7
|
Text: {
|
|
@@ -462,6 +463,107 @@ export const ZORA_BINDABLE_COMPONENT_META = {
|
|
|
462
463
|
},
|
|
463
464
|
},
|
|
464
465
|
},
|
|
466
|
+
ReaderSurface: {
|
|
467
|
+
...readerSurfaceMeta,
|
|
468
|
+
bindings: {
|
|
469
|
+
props: {
|
|
470
|
+
location: {
|
|
471
|
+
label: 'Location',
|
|
472
|
+
description: 'Opaque adapter locator to restore.',
|
|
473
|
+
value: { type: 'string' },
|
|
474
|
+
acceptsFallback: true,
|
|
475
|
+
acceptsTransforms: true,
|
|
476
|
+
},
|
|
477
|
+
page: {
|
|
478
|
+
label: 'Page',
|
|
479
|
+
description: 'One-based visible page.',
|
|
480
|
+
value: { type: 'number' },
|
|
481
|
+
acceptsFallback: true,
|
|
482
|
+
acceptsTransforms: true,
|
|
483
|
+
},
|
|
484
|
+
pageCount: {
|
|
485
|
+
label: 'Page count',
|
|
486
|
+
description: 'Current number of logical pages.',
|
|
487
|
+
value: { type: 'number' },
|
|
488
|
+
acceptsFallback: true,
|
|
489
|
+
acceptsTransforms: true,
|
|
490
|
+
},
|
|
491
|
+
progress: {
|
|
492
|
+
label: 'Progress',
|
|
493
|
+
description: 'Reading progression clamped from zero to one.',
|
|
494
|
+
value: { type: 'number' },
|
|
495
|
+
acceptsFallback: true,
|
|
496
|
+
acceptsTransforms: true,
|
|
497
|
+
},
|
|
498
|
+
status: {
|
|
499
|
+
label: 'Status',
|
|
500
|
+
description: 'Current reader loading status.',
|
|
501
|
+
value: { type: 'string' },
|
|
502
|
+
acceptsFallback: true,
|
|
503
|
+
acceptsTransforms: true,
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
events: {
|
|
507
|
+
previousPage: {
|
|
508
|
+
label: 'Previous page',
|
|
509
|
+
description: 'Requests navigation to the previous page.',
|
|
510
|
+
payload: { eventType: 'reader.previousPage', fields: [] },
|
|
511
|
+
},
|
|
512
|
+
nextPage: {
|
|
513
|
+
label: 'Next page',
|
|
514
|
+
description: 'Requests navigation to the next page.',
|
|
515
|
+
payload: { eventType: 'reader.nextPage', fields: [] },
|
|
516
|
+
},
|
|
517
|
+
locationChange: {
|
|
518
|
+
label: 'Location change',
|
|
519
|
+
description: 'Runs after the adapter displays a destination.',
|
|
520
|
+
payload: {
|
|
521
|
+
eventType: 'reader.locationChange',
|
|
522
|
+
fields: [
|
|
523
|
+
{ path: 'format', type: 'string', label: 'Format' },
|
|
524
|
+
{ path: 'locator', type: 'string', label: 'Opaque locator' },
|
|
525
|
+
{ path: 'page', type: 'number', label: 'Page' },
|
|
526
|
+
{ path: 'pageCount', type: 'number', label: 'Page count' },
|
|
527
|
+
{ path: 'progression', type: 'number', label: 'Progression' },
|
|
528
|
+
{ path: 'chapterId', type: 'string', label: 'Chapter ID' },
|
|
529
|
+
{ path: 'chapterTitle', type: 'string', label: 'Chapter title' },
|
|
530
|
+
{ path: 'trigger', type: 'string', label: 'Navigation trigger' },
|
|
531
|
+
],
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
openContents: {
|
|
535
|
+
label: 'Open contents',
|
|
536
|
+
payload: { eventType: 'reader.openContents', fields: [] },
|
|
537
|
+
},
|
|
538
|
+
openAppearance: {
|
|
539
|
+
label: 'Open appearance',
|
|
540
|
+
payload: { eventType: 'reader.openAppearance', fields: [] },
|
|
541
|
+
},
|
|
542
|
+
toggleHighlight: {
|
|
543
|
+
label: 'Toggle highlight',
|
|
544
|
+
payload: { eventType: 'reader.toggleHighlight', fields: [] },
|
|
545
|
+
},
|
|
546
|
+
openExternalLink: {
|
|
547
|
+
label: 'Open external link',
|
|
548
|
+
payload: {
|
|
549
|
+
eventType: 'reader.openExternalLink',
|
|
550
|
+
fields: [{ path: 'url', type: 'string', label: 'URL' }],
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
readerError: {
|
|
554
|
+
label: 'Reader error',
|
|
555
|
+
payload: {
|
|
556
|
+
eventType: 'reader.error',
|
|
557
|
+
fields: [
|
|
558
|
+
{ path: 'code', type: 'string', label: 'Error code' },
|
|
559
|
+
{ path: 'format', type: 'string', label: 'Format' },
|
|
560
|
+
{ path: 'message', type: 'string', label: 'Message' },
|
|
561
|
+
],
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
465
567
|
} as const satisfies UiComponentMetaRegistry;
|
|
466
568
|
|
|
467
569
|
export type ZoraBindableComponentType = keyof typeof ZORA_BINDABLE_COMPONENT_META;
|
|
@@ -88,6 +88,12 @@ describe('ZORA_COMPONENT_META requirement metadata', () => {
|
|
|
88
88
|
});
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
test('declares ebook reader capability metadata for ReaderSurface', () => {
|
|
92
|
+
expect(ZORA_COMPONENT_META.ReaderSurface.requirements).toEqual({
|
|
93
|
+
capabilities: [{ capability: 'ebookReader' }],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
91
97
|
test('does not declare runtime requirements for the camera-agnostic scan overlay', () => {
|
|
92
98
|
expect(ZORA_COMPONENT_META.ScanOverlay.requirements).toBeUndefined();
|
|
93
99
|
});
|
|
@@ -132,6 +138,7 @@ describe('ZORA_COMPONENT_META invariants', () => {
|
|
|
132
138
|
'ScanOverlay',
|
|
133
139
|
'ProductCard',
|
|
134
140
|
'Progress',
|
|
141
|
+
'ReaderSurface',
|
|
135
142
|
]);
|
|
136
143
|
|
|
137
144
|
const expectedContainerNodes = new Set([
|
|
@@ -204,6 +211,35 @@ describe('ZORA_COMPONENT_META invariants', () => {
|
|
|
204
211
|
});
|
|
205
212
|
});
|
|
206
213
|
|
|
214
|
+
test('ReaderSurface exposes a safe file-media blueprint and normalized events', () => {
|
|
215
|
+
const reader = ZORA_COMPONENT_META.ReaderSurface;
|
|
216
|
+
|
|
217
|
+
expect(reader.directManifestNode).toBe(true);
|
|
218
|
+
expect(reader.allowedChildren).toEqual([]);
|
|
219
|
+
expect(reader.props.source).toMatchObject({ type: 'media', mediaKinds: ['file'] });
|
|
220
|
+
expect(reader.props.format.enum).toEqual(['epub', 'pdf']);
|
|
221
|
+
expect(reader.blueprint?.defaultProps).toEqual({
|
|
222
|
+
format: 'epub',
|
|
223
|
+
status: 'idle',
|
|
224
|
+
showChrome: true,
|
|
225
|
+
readerColorScheme: 'system',
|
|
226
|
+
fontScale: 1,
|
|
227
|
+
lineHeight: 'normal',
|
|
228
|
+
});
|
|
229
|
+
expect(reader.blueprint?.defaultProps).not.toHaveProperty('source');
|
|
230
|
+
expect(reader.events?.locationChange?.payloadFields?.map((field) => field.path)).toEqual([
|
|
231
|
+
'format',
|
|
232
|
+
'locator',
|
|
233
|
+
'page',
|
|
234
|
+
'pageCount',
|
|
235
|
+
'progression',
|
|
236
|
+
'chapterId',
|
|
237
|
+
'chapterTitle',
|
|
238
|
+
'trigger',
|
|
239
|
+
]);
|
|
240
|
+
expect(reader.events?.readerError?.eventType).toBe('reader.error');
|
|
241
|
+
});
|
|
242
|
+
|
|
207
243
|
test('non-direct manifest nodes include an explicit note', () => {
|
|
208
244
|
for (const [key, meta] of Object.entries(ZORA_COMPONENT_META)) {
|
|
209
245
|
if (meta.directManifestNode) continue;
|
|
@@ -76,6 +76,7 @@ import { noticeMeta } from '../patterns/notice/meta';
|
|
|
76
76
|
import { panelMeta } from '../patterns/panel/meta';
|
|
77
77
|
import { postCardMeta } from '../patterns/post-card/meta';
|
|
78
78
|
import { productCardMeta } from '../patterns/product-card/meta';
|
|
79
|
+
import { readerSurfaceMeta } from '../patterns/reader/meta';
|
|
79
80
|
import { responsivePanelMeta } from '../patterns/responsive-panel/meta';
|
|
80
81
|
import {
|
|
81
82
|
barcodeScannerViewMeta,
|
|
@@ -183,6 +184,7 @@ export const ZORA_COMPONENT_META: ZoraComponentMetaRegistry = {
|
|
|
183
184
|
PostCard: postCardMeta,
|
|
184
185
|
ProductCard: productCardMeta,
|
|
185
186
|
ResponsivePanel: responsivePanelMeta,
|
|
187
|
+
ReaderSurface: readerSurfaceMeta,
|
|
186
188
|
BarcodeScannerView: barcodeScannerViewMeta,
|
|
187
189
|
CameraPermissionView: cameraPermissionViewMeta,
|
|
188
190
|
ScanOverlay: scanOverlayMeta,
|
|
@@ -33,9 +33,7 @@ export interface ZoraThemeRecipeBooleanFieldMeta extends ZoraThemeRecipeFieldBas
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export type ZoraThemeRecipeFieldMeta =
|
|
36
|
-
|
|
|
37
|
-
| ZoraThemeRecipeChoiceFieldMeta
|
|
38
|
-
| ZoraThemeRecipeBooleanFieldMeta;
|
|
36
|
+
ZoraThemeRecipeTokenFieldMeta | ZoraThemeRecipeChoiceFieldMeta | ZoraThemeRecipeBooleanFieldMeta;
|
|
39
37
|
|
|
40
38
|
export interface ZoraThemeRecipeMeta {
|
|
41
39
|
readonly name: string;
|
package/src/metadata/types.ts
CHANGED
|
@@ -53,12 +53,7 @@ export interface ZoraComponentI18nMeta {
|
|
|
53
53
|
|
|
54
54
|
export type ZoraComponentEventPayloadKind = ComponentEventDtoKind | (string & {});
|
|
55
55
|
export type ZoraComponentEventPayloadFieldType =
|
|
56
|
-
| '
|
|
57
|
-
| 'number'
|
|
58
|
-
| 'object'
|
|
59
|
-
| 'record'
|
|
60
|
-
| 'string'
|
|
61
|
-
| 'unknown';
|
|
56
|
+
'boolean' | 'number' | 'object' | 'record' | 'string' | 'unknown';
|
|
62
57
|
|
|
63
58
|
export interface ZoraComponentEventPayloadFieldMeta {
|
|
64
59
|
readonly path: string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import type { ReaderErrorEvent, ReaderLocationChangeEvent, ReaderSurfaceProps } from '../../index';
|
|
4
|
+
|
|
5
|
+
describe('ReaderSurface public contract', () => {
|
|
6
|
+
test('exports the component and serializable adapter events from the package root', () => {
|
|
7
|
+
const props: ReaderSurfaceProps = { format: 'epub' };
|
|
8
|
+
const location: ReaderLocationChangeEvent = {
|
|
9
|
+
format: 'epub',
|
|
10
|
+
locator: 'epubcfi(/6/4!/4/2)',
|
|
11
|
+
page: 2,
|
|
12
|
+
pageCount: 8,
|
|
13
|
+
progression: 1 / 7,
|
|
14
|
+
chapterId: 'chapter-1',
|
|
15
|
+
chapterTitle: 'A beginning',
|
|
16
|
+
trigger: 'swipe',
|
|
17
|
+
};
|
|
18
|
+
const error: ReaderErrorEvent = {
|
|
19
|
+
code: 'protected-document',
|
|
20
|
+
format: 'pdf',
|
|
21
|
+
message: 'This document is protected.',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
expect(props.format).toBe('epub');
|
|
25
|
+
expect(() => JSON.stringify(location)).not.toThrow();
|
|
26
|
+
expect(() => JSON.stringify(error)).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('exports the reader API from the package root', async () => {
|
|
30
|
+
const source = await Bun.file('src/index.ts').text();
|
|
31
|
+
|
|
32
|
+
expect(source).toContain("from './patterns/reader';");
|
|
33
|
+
expect(source).toContain('ReaderSurface, resolveReaderProgress');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('keeps parsing, fetching, persistence, and platform APIs outside ZORA', async () => {
|
|
37
|
+
const source = await Bun.file('src/patterns/reader/ReaderSurface.tsx').text();
|
|
38
|
+
|
|
39
|
+
expect(source).not.toContain('expo-');
|
|
40
|
+
expect(source).not.toContain('pdfjs');
|
|
41
|
+
expect(source).not.toContain('epubjs');
|
|
42
|
+
expect(source).not.toContain('fetch(');
|
|
43
|
+
expect(source).not.toContain('AsyncStorage');
|
|
44
|
+
expect(source).toContain('accessibilityLiveRegion="polite"');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { AppBar } from '../../components/app-bar';
|
|
4
|
+
import { IconButton } from '../../components/icon-button';
|
|
5
|
+
import { Progress } from '../../components/progress';
|
|
6
|
+
import { Text } from '../../components/text';
|
|
7
|
+
import { Box, Center, Inline, Stack, Surface } from '../../foundation';
|
|
8
|
+
import { useZoraTheme } from '../../theme/useZoraTheme';
|
|
9
|
+
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
10
|
+
import { resolveReaderProgress } from './resolveReaderProgress';
|
|
11
|
+
import type { ReaderStatus, ReaderSurfaceProps } from './types';
|
|
12
|
+
|
|
13
|
+
function resolvePageLabel(page: number | undefined, pageCount: number | undefined): string | null {
|
|
14
|
+
if (page === undefined || !Number.isFinite(page)) return null;
|
|
15
|
+
if (pageCount === undefined || !Number.isFinite(pageCount)) return `Page ${page}`;
|
|
16
|
+
return `Page ${page} of ${pageCount}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveCanGoPrevious({
|
|
20
|
+
canGoPrevious,
|
|
21
|
+
page,
|
|
22
|
+
}: Pick<ReaderSurfaceProps, 'canGoPrevious' | 'page'>): boolean {
|
|
23
|
+
if (canGoPrevious !== undefined) return canGoPrevious;
|
|
24
|
+
return page === undefined || page > 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveCanGoNext({
|
|
28
|
+
canGoNext,
|
|
29
|
+
page,
|
|
30
|
+
pageCount,
|
|
31
|
+
}: Pick<ReaderSurfaceProps, 'canGoNext' | 'page' | 'pageCount'>): boolean {
|
|
32
|
+
if (canGoNext !== undefined) return canGoNext;
|
|
33
|
+
return page === undefined || pageCount === undefined || page < pageCount;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ReaderEmptyState({
|
|
37
|
+
status,
|
|
38
|
+
loadingLabel,
|
|
39
|
+
errorTitle,
|
|
40
|
+
unavailableTitle,
|
|
41
|
+
}: {
|
|
42
|
+
status: ReaderStatus;
|
|
43
|
+
loadingLabel: string;
|
|
44
|
+
errorTitle: string;
|
|
45
|
+
unavailableTitle: string;
|
|
46
|
+
}) {
|
|
47
|
+
const title =
|
|
48
|
+
status === 'loading' ? loadingLabel : status === 'error' ? errorTitle : unavailableTitle;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Center flex={1} minHeight={360} p="l">
|
|
52
|
+
<Text align="center" emphasis="muted" variant="bodySmall">
|
|
53
|
+
{title}
|
|
54
|
+
</Text>
|
|
55
|
+
</Center>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function invokeReaderAction(action: (() => void | Promise<void>) | undefined): void {
|
|
60
|
+
void action?.();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ReaderHeader({
|
|
64
|
+
interactionPolicy,
|
|
65
|
+
title,
|
|
66
|
+
subtitle,
|
|
67
|
+
contentsLabel,
|
|
68
|
+
appearanceLabel,
|
|
69
|
+
highlightLabel,
|
|
70
|
+
highlighted,
|
|
71
|
+
headerActions,
|
|
72
|
+
onOpenContents,
|
|
73
|
+
onOpenAppearance,
|
|
74
|
+
onToggleHighlight,
|
|
75
|
+
}: Pick<
|
|
76
|
+
ReaderSurfaceProps,
|
|
77
|
+
| 'interactionPolicy'
|
|
78
|
+
| 'title'
|
|
79
|
+
| 'subtitle'
|
|
80
|
+
| 'contentsLabel'
|
|
81
|
+
| 'appearanceLabel'
|
|
82
|
+
| 'highlightLabel'
|
|
83
|
+
| 'highlighted'
|
|
84
|
+
| 'headerActions'
|
|
85
|
+
| 'onOpenContents'
|
|
86
|
+
| 'onOpenAppearance'
|
|
87
|
+
| 'onToggleHighlight'
|
|
88
|
+
>) {
|
|
89
|
+
const actions =
|
|
90
|
+
onOpenContents || onOpenAppearance || onToggleHighlight || headerActions ? (
|
|
91
|
+
<Inline align="center" gap="xs" wrap="nowrap">
|
|
92
|
+
{onOpenContents ? (
|
|
93
|
+
<IconButton
|
|
94
|
+
icon={{ name: 'list-outline' }}
|
|
95
|
+
interactionPolicy={interactionPolicy}
|
|
96
|
+
label={contentsLabel ?? 'Table of contents'}
|
|
97
|
+
onPress={() => invokeReaderAction(onOpenContents)}
|
|
98
|
+
size="l"
|
|
99
|
+
/>
|
|
100
|
+
) : null}
|
|
101
|
+
{onOpenAppearance ? (
|
|
102
|
+
<IconButton
|
|
103
|
+
icon={{ name: 'text-outline' }}
|
|
104
|
+
interactionPolicy={interactionPolicy}
|
|
105
|
+
label={appearanceLabel ?? 'Reading appearance'}
|
|
106
|
+
onPress={() => invokeReaderAction(onOpenAppearance)}
|
|
107
|
+
size="l"
|
|
108
|
+
/>
|
|
109
|
+
) : null}
|
|
110
|
+
{onToggleHighlight ? (
|
|
111
|
+
<IconButton
|
|
112
|
+
color={highlighted ? 'primary' : 'neutral'}
|
|
113
|
+
icon={{ name: highlighted ? 'bookmark' : 'bookmark-outline' }}
|
|
114
|
+
interactionPolicy={interactionPolicy}
|
|
115
|
+
label={highlightLabel ?? 'Toggle highlight'}
|
|
116
|
+
onPress={() => invokeReaderAction(onToggleHighlight)}
|
|
117
|
+
size="l"
|
|
118
|
+
/>
|
|
119
|
+
) : null}
|
|
120
|
+
{headerActions}
|
|
121
|
+
</Inline>
|
|
122
|
+
) : undefined;
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<AppBar
|
|
126
|
+
actions={actions}
|
|
127
|
+
divider
|
|
128
|
+
interactionPolicy={interactionPolicy}
|
|
129
|
+
safeAreaTop
|
|
130
|
+
subtitle={subtitle}
|
|
131
|
+
title={title}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function ReaderFooter({
|
|
137
|
+
interactionPolicy,
|
|
138
|
+
status,
|
|
139
|
+
page,
|
|
140
|
+
pageCount,
|
|
141
|
+
progress,
|
|
142
|
+
canGoPrevious,
|
|
143
|
+
canGoNext,
|
|
144
|
+
chapterLabel,
|
|
145
|
+
pageLabel,
|
|
146
|
+
previousPageLabel,
|
|
147
|
+
nextPageLabel,
|
|
148
|
+
footerActions,
|
|
149
|
+
onPreviousPage,
|
|
150
|
+
onNextPage,
|
|
151
|
+
}: Pick<
|
|
152
|
+
ReaderSurfaceProps,
|
|
153
|
+
| 'interactionPolicy'
|
|
154
|
+
| 'status'
|
|
155
|
+
| 'page'
|
|
156
|
+
| 'pageCount'
|
|
157
|
+
| 'progress'
|
|
158
|
+
| 'canGoPrevious'
|
|
159
|
+
| 'canGoNext'
|
|
160
|
+
| 'chapterLabel'
|
|
161
|
+
| 'pageLabel'
|
|
162
|
+
| 'previousPageLabel'
|
|
163
|
+
| 'nextPageLabel'
|
|
164
|
+
| 'footerActions'
|
|
165
|
+
| 'onPreviousPage'
|
|
166
|
+
| 'onNextPage'
|
|
167
|
+
>) {
|
|
168
|
+
const { theme } = useZoraTheme();
|
|
169
|
+
const resolvedProgress = resolveReaderProgress({ progress, page, pageCount });
|
|
170
|
+
const resolvedPageLabel = pageLabel ?? resolvePageLabel(page, pageCount);
|
|
171
|
+
const previousEnabled =
|
|
172
|
+
status === 'ready' && Boolean(onPreviousPage) && resolveCanGoPrevious({ canGoPrevious, page });
|
|
173
|
+
const nextEnabled =
|
|
174
|
+
status === 'ready' && Boolean(onNextPage) && resolveCanGoNext({ canGoNext, page, pageCount });
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Stack
|
|
178
|
+
bg={theme.semantics.surface.raised}
|
|
179
|
+
borderColor={theme.semantics.neutral.divider}
|
|
180
|
+
borderWidth={1}
|
|
181
|
+
gap="s"
|
|
182
|
+
p="m"
|
|
183
|
+
>
|
|
184
|
+
<Progress value={resolvedProgress} max={1} size="s" />
|
|
185
|
+
<Stack align="center" direction="row" gap="m" justify="space-between" wrap="nowrap">
|
|
186
|
+
<IconButton
|
|
187
|
+
disabled={!previousEnabled}
|
|
188
|
+
icon={{ name: 'chevron-back' }}
|
|
189
|
+
interactionPolicy={interactionPolicy}
|
|
190
|
+
label={previousPageLabel ?? 'Previous page'}
|
|
191
|
+
onPress={() => invokeReaderAction(onPreviousPage)}
|
|
192
|
+
size="l"
|
|
193
|
+
/>
|
|
194
|
+
<Box flex={1} minWidth={0}>
|
|
195
|
+
<Stack align="center" gap="xs">
|
|
196
|
+
{chapterLabel ? (
|
|
197
|
+
<Text
|
|
198
|
+
align="center"
|
|
199
|
+
ellipsizeMode="tail"
|
|
200
|
+
numberOfLines={1}
|
|
201
|
+
variant="label"
|
|
202
|
+
weight="semiBold"
|
|
203
|
+
>
|
|
204
|
+
{chapterLabel}
|
|
205
|
+
</Text>
|
|
206
|
+
) : null}
|
|
207
|
+
{resolvedPageLabel ? (
|
|
208
|
+
<Text
|
|
209
|
+
accessibilityLiveRegion="polite"
|
|
210
|
+
align="center"
|
|
211
|
+
emphasis="muted"
|
|
212
|
+
variant="caption"
|
|
213
|
+
>
|
|
214
|
+
{resolvedPageLabel}
|
|
215
|
+
</Text>
|
|
216
|
+
) : null}
|
|
217
|
+
</Stack>
|
|
218
|
+
</Box>
|
|
219
|
+
<IconButton
|
|
220
|
+
disabled={!nextEnabled}
|
|
221
|
+
icon={{ name: 'chevron-forward' }}
|
|
222
|
+
interactionPolicy={interactionPolicy}
|
|
223
|
+
label={nextPageLabel ?? 'Next page'}
|
|
224
|
+
onPress={() => invokeReaderAction(onNextPage)}
|
|
225
|
+
size="l"
|
|
226
|
+
/>
|
|
227
|
+
</Stack>
|
|
228
|
+
{footerActions ? <Box>{footerActions}</Box> : null}
|
|
229
|
+
</Stack>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function ReaderSurfaceInner({
|
|
234
|
+
themeId: _themeId,
|
|
235
|
+
mode: _mode,
|
|
236
|
+
interactionPolicy,
|
|
237
|
+
source: _source,
|
|
238
|
+
format: _format,
|
|
239
|
+
location: _location,
|
|
240
|
+
status = 'idle',
|
|
241
|
+
page,
|
|
242
|
+
pageCount,
|
|
243
|
+
progress,
|
|
244
|
+
canGoPrevious,
|
|
245
|
+
canGoNext,
|
|
246
|
+
title,
|
|
247
|
+
subtitle,
|
|
248
|
+
chapterLabel,
|
|
249
|
+
pageLabel,
|
|
250
|
+
previousPageLabel = 'Previous page',
|
|
251
|
+
nextPageLabel = 'Next page',
|
|
252
|
+
contentsLabel = 'Table of contents',
|
|
253
|
+
appearanceLabel = 'Reading appearance',
|
|
254
|
+
highlightLabel = 'Toggle highlight',
|
|
255
|
+
loadingLabel = 'Loading document…',
|
|
256
|
+
errorTitle = 'Unable to open this document.',
|
|
257
|
+
unavailableTitle = 'Reader preview unavailable.',
|
|
258
|
+
showChrome = true,
|
|
259
|
+
readerColorScheme: _readerColorScheme = 'system',
|
|
260
|
+
fontScale: _fontScale = 1,
|
|
261
|
+
lineHeight: _lineHeight = 'normal',
|
|
262
|
+
highlighted = false,
|
|
263
|
+
viewport,
|
|
264
|
+
headerActions,
|
|
265
|
+
footerActions,
|
|
266
|
+
onPreviousPage,
|
|
267
|
+
onNextPage,
|
|
268
|
+
onLocationChange: _onLocationChange,
|
|
269
|
+
onOpenContents,
|
|
270
|
+
onOpenAppearance,
|
|
271
|
+
onToggleHighlight,
|
|
272
|
+
onOpenExternalLink: _onOpenExternalLink,
|
|
273
|
+
onReaderError: _onReaderError,
|
|
274
|
+
testID,
|
|
275
|
+
}: ReaderSurfaceProps) {
|
|
276
|
+
const { theme } = useZoraTheme();
|
|
277
|
+
|
|
278
|
+
const viewportContent =
|
|
279
|
+
viewport && status !== 'error' ? (
|
|
280
|
+
viewport
|
|
281
|
+
) : (
|
|
282
|
+
<ReaderEmptyState
|
|
283
|
+
errorTitle={errorTitle}
|
|
284
|
+
loadingLabel={loadingLabel}
|
|
285
|
+
status={status}
|
|
286
|
+
unavailableTitle={unavailableTitle}
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<Surface
|
|
292
|
+
radius="l"
|
|
293
|
+
testID={testID}
|
|
294
|
+
variant="default"
|
|
295
|
+
style={{ minHeight: 480, overflow: 'hidden' }}
|
|
296
|
+
>
|
|
297
|
+
<Stack flex={1} gap={0}>
|
|
298
|
+
{showChrome ? (
|
|
299
|
+
<ReaderHeader
|
|
300
|
+
appearanceLabel={appearanceLabel}
|
|
301
|
+
contentsLabel={contentsLabel}
|
|
302
|
+
headerActions={headerActions}
|
|
303
|
+
highlightLabel={highlightLabel}
|
|
304
|
+
highlighted={highlighted}
|
|
305
|
+
interactionPolicy={interactionPolicy}
|
|
306
|
+
onOpenAppearance={onOpenAppearance}
|
|
307
|
+
onOpenContents={onOpenContents}
|
|
308
|
+
onToggleHighlight={onToggleHighlight}
|
|
309
|
+
subtitle={subtitle}
|
|
310
|
+
title={title}
|
|
311
|
+
/>
|
|
312
|
+
) : null}
|
|
313
|
+
|
|
314
|
+
<Box bg={theme.semantics.surface.default} flex={1} minHeight={360} overflow="hidden">
|
|
315
|
+
{viewportContent}
|
|
316
|
+
</Box>
|
|
317
|
+
|
|
318
|
+
{showChrome ? (
|
|
319
|
+
<ReaderFooter
|
|
320
|
+
canGoNext={canGoNext}
|
|
321
|
+
canGoPrevious={canGoPrevious}
|
|
322
|
+
chapterLabel={chapterLabel}
|
|
323
|
+
footerActions={footerActions}
|
|
324
|
+
interactionPolicy={interactionPolicy}
|
|
325
|
+
nextPageLabel={nextPageLabel}
|
|
326
|
+
onNextPage={onNextPage}
|
|
327
|
+
onPreviousPage={onPreviousPage}
|
|
328
|
+
page={page}
|
|
329
|
+
pageCount={pageCount}
|
|
330
|
+
pageLabel={pageLabel}
|
|
331
|
+
previousPageLabel={previousPageLabel}
|
|
332
|
+
progress={progress}
|
|
333
|
+
status={status}
|
|
334
|
+
/>
|
|
335
|
+
) : null}
|
|
336
|
+
</Stack>
|
|
337
|
+
</Surface>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/***
|
|
342
|
+
* Adapter-neutral reader shell for EPUB and PDF experiences.
|
|
343
|
+
*
|
|
344
|
+
* Supply the actual renderer through `viewport`; ZORA owns only the reader
|
|
345
|
+
* chrome, controlled state, progress, accessible controls, and normalized
|
|
346
|
+
* adapter callbacks.
|
|
347
|
+
*/
|
|
348
|
+
export const ReaderSurface = withZoraThemeScope(ReaderSurfaceInner);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { ReaderSurface } from './ReaderSurface';
|
|
2
|
+
export { resolveReaderProgress } from './resolveReaderProgress';
|
|
3
|
+
export type {
|
|
4
|
+
ReaderColorScheme,
|
|
5
|
+
ReaderDocumentFormat,
|
|
6
|
+
ReaderErrorCode,
|
|
7
|
+
ReaderErrorEvent,
|
|
8
|
+
ReaderExternalLinkEvent,
|
|
9
|
+
ReaderLineHeight,
|
|
10
|
+
ReaderLocationChangeEvent,
|
|
11
|
+
ReaderNavigationTrigger,
|
|
12
|
+
ReaderResolvedSource,
|
|
13
|
+
ReaderStatus,
|
|
14
|
+
ReaderSurfaceProps,
|
|
15
|
+
} from './types';
|