@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.
Files changed (88) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/components/card/meta.d.ts +1 -1
  4. package/dist/components/form/types.d.ts.map +1 -1
  5. package/dist/components/form/types.js.map +1 -1
  6. package/dist/components/media-card/types.d.ts.map +1 -1
  7. package/dist/components/media-card/types.js.map +1 -1
  8. package/dist/components/text/Text.d.ts.map +1 -1
  9. package/dist/components/text/Text.js +2 -2
  10. package/dist/components/text/Text.js.map +1 -1
  11. package/dist/components/text/types.d.ts +2 -1
  12. package/dist/components/text/types.d.ts.map +1 -1
  13. package/dist/components/text/types.js.map +1 -1
  14. package/dist/foundation/meta.d.ts +4 -4
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/layout/screen/meta.d.ts +1 -1
  20. package/dist/layout/screen-section/meta.d.ts +1 -1
  21. package/dist/metadata/allowedChildren.d.ts +3 -3
  22. package/dist/metadata/allowedChildren.d.ts.map +1 -1
  23. package/dist/metadata/allowedChildren.js +1 -0
  24. package/dist/metadata/allowedChildren.js.map +1 -1
  25. package/dist/metadata/bindableComponentMeta.d.ts +485 -0
  26. package/dist/metadata/bindableComponentMeta.d.ts.map +1 -1
  27. package/dist/metadata/bindableComponentMeta.js +102 -0
  28. package/dist/metadata/bindableComponentMeta.js.map +1 -1
  29. package/dist/metadata/componentMeta.d.ts.map +1 -1
  30. package/dist/metadata/componentMeta.js +2 -0
  31. package/dist/metadata/componentMeta.js.map +1 -1
  32. package/dist/metadata/themeRecipeTypes.d.ts.map +1 -1
  33. package/dist/metadata/themeRecipeTypes.js.map +1 -1
  34. package/dist/metadata/types.d.ts.map +1 -1
  35. package/dist/metadata/types.js.map +1 -1
  36. package/dist/patterns/message-bubble/meta.d.ts +1 -1
  37. package/dist/patterns/notice/meta.d.ts +1 -1
  38. package/dist/patterns/panel/meta.d.ts +1 -1
  39. package/dist/patterns/post-card/meta.d.ts +1 -1
  40. package/dist/patterns/reader/ReaderSurface.d.ts +11 -0
  41. package/dist/patterns/reader/ReaderSurface.d.ts.map +1 -0
  42. package/dist/patterns/reader/ReaderSurface.js +95 -0
  43. package/dist/patterns/reader/ReaderSurface.js.map +1 -0
  44. package/dist/patterns/reader/index.d.ts +4 -0
  45. package/dist/patterns/reader/index.d.ts.map +1 -0
  46. package/dist/patterns/reader/index.js +3 -0
  47. package/dist/patterns/reader/index.js.map +1 -0
  48. package/dist/patterns/reader/meta.d.ts +328 -0
  49. package/dist/patterns/reader/meta.d.ts.map +1 -0
  50. package/dist/patterns/reader/meta.js +293 -0
  51. package/dist/patterns/reader/meta.js.map +1 -0
  52. package/dist/patterns/reader/resolveReaderProgress.d.ts +6 -0
  53. package/dist/patterns/reader/resolveReaderProgress.d.ts.map +1 -0
  54. package/dist/patterns/reader/resolveReaderProgress.js +17 -0
  55. package/dist/patterns/reader/resolveReaderProgress.js.map +1 -0
  56. package/dist/patterns/reader/types.d.ts +69 -0
  57. package/dist/patterns/reader/types.d.ts.map +1 -0
  58. package/dist/patterns/reader/types.js +2 -0
  59. package/dist/patterns/reader/types.js.map +1 -0
  60. package/dist/patterns/scanner/meta.d.ts +2 -2
  61. package/dist/registry.d.ts.map +1 -1
  62. package/dist/registry.js +2 -0
  63. package/dist/registry.js.map +1 -1
  64. package/dist/theme/useZoraTheme.d.ts +1 -1
  65. package/package.json +11 -12
  66. package/src/components/form/types.ts +1 -2
  67. package/src/components/media-card/types.ts +1 -3
  68. package/src/components/text/Text.tsx +2 -0
  69. package/src/components/text/types.ts +7 -1
  70. package/src/expo57Migration.test.ts +18 -14
  71. package/src/index.ts +14 -0
  72. package/src/interactionPolicy.test.ts +30 -0
  73. package/src/metadata/allowedChildren.ts +1 -0
  74. package/src/metadata/bindableComponentMeta.test.ts +30 -0
  75. package/src/metadata/bindableComponentMeta.ts +102 -0
  76. package/src/metadata/componentMeta.test.ts +36 -0
  77. package/src/metadata/componentMeta.ts +2 -0
  78. package/src/metadata/themeRecipeTypes.ts +1 -3
  79. package/src/metadata/types.ts +1 -6
  80. package/src/patterns/reader/ReaderSurface.test.ts +46 -0
  81. package/src/patterns/reader/ReaderSurface.tsx +348 -0
  82. package/src/patterns/reader/index.ts +15 -0
  83. package/src/patterns/reader/meta.ts +296 -0
  84. package/src/patterns/reader/resolveReaderProgress.test.ts +31 -0
  85. package/src/patterns/reader/resolveReaderProgress.ts +29 -0
  86. package/src/patterns/reader/types.ts +80 -0
  87. package/src/registry.ts +2 -0
  88. 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
- | ZoraThemeRecipeTokenFieldMeta
37
- | ZoraThemeRecipeChoiceFieldMeta
38
- | ZoraThemeRecipeBooleanFieldMeta;
36
+ ZoraThemeRecipeTokenFieldMeta | ZoraThemeRecipeChoiceFieldMeta | ZoraThemeRecipeBooleanFieldMeta;
39
37
 
40
38
  export interface ZoraThemeRecipeMeta {
41
39
  readonly name: string;
@@ -53,12 +53,7 @@ export interface ZoraComponentI18nMeta {
53
53
 
54
54
  export type ZoraComponentEventPayloadKind = ComponentEventDtoKind | (string & {});
55
55
  export type ZoraComponentEventPayloadFieldType =
56
- | 'boolean'
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';