@ankhorage/zora 3.0.2 → 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 (76) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/dist/components/card/meta.d.ts +1 -1
  4. package/dist/components/text/Text.d.ts.map +1 -1
  5. package/dist/components/text/Text.js +2 -2
  6. package/dist/components/text/Text.js.map +1 -1
  7. package/dist/components/text/types.d.ts +2 -1
  8. package/dist/components/text/types.d.ts.map +1 -1
  9. package/dist/components/text/types.js.map +1 -1
  10. package/dist/foundation/meta.d.ts +4 -4
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/layout/screen/meta.d.ts +1 -1
  16. package/dist/layout/screen-section/meta.d.ts +1 -1
  17. package/dist/metadata/allowedChildren.d.ts +3 -3
  18. package/dist/metadata/allowedChildren.d.ts.map +1 -1
  19. package/dist/metadata/allowedChildren.js +1 -0
  20. package/dist/metadata/allowedChildren.js.map +1 -1
  21. package/dist/metadata/bindableComponentMeta.d.ts +485 -0
  22. package/dist/metadata/bindableComponentMeta.d.ts.map +1 -1
  23. package/dist/metadata/bindableComponentMeta.js +102 -0
  24. package/dist/metadata/bindableComponentMeta.js.map +1 -1
  25. package/dist/metadata/componentMeta.d.ts.map +1 -1
  26. package/dist/metadata/componentMeta.js +2 -0
  27. package/dist/metadata/componentMeta.js.map +1 -1
  28. package/dist/patterns/message-bubble/meta.d.ts +1 -1
  29. package/dist/patterns/notice/meta.d.ts +1 -1
  30. package/dist/patterns/panel/meta.d.ts +1 -1
  31. package/dist/patterns/post-card/meta.d.ts +1 -1
  32. package/dist/patterns/reader/ReaderSurface.d.ts +11 -0
  33. package/dist/patterns/reader/ReaderSurface.d.ts.map +1 -0
  34. package/dist/patterns/reader/ReaderSurface.js +95 -0
  35. package/dist/patterns/reader/ReaderSurface.js.map +1 -0
  36. package/dist/patterns/reader/index.d.ts +4 -0
  37. package/dist/patterns/reader/index.d.ts.map +1 -0
  38. package/dist/patterns/reader/index.js +3 -0
  39. package/dist/patterns/reader/index.js.map +1 -0
  40. package/dist/patterns/reader/meta.d.ts +328 -0
  41. package/dist/patterns/reader/meta.d.ts.map +1 -0
  42. package/dist/patterns/reader/meta.js +293 -0
  43. package/dist/patterns/reader/meta.js.map +1 -0
  44. package/dist/patterns/reader/resolveReaderProgress.d.ts +6 -0
  45. package/dist/patterns/reader/resolveReaderProgress.d.ts.map +1 -0
  46. package/dist/patterns/reader/resolveReaderProgress.js +17 -0
  47. package/dist/patterns/reader/resolveReaderProgress.js.map +1 -0
  48. package/dist/patterns/reader/types.d.ts +69 -0
  49. package/dist/patterns/reader/types.d.ts.map +1 -0
  50. package/dist/patterns/reader/types.js +2 -0
  51. package/dist/patterns/reader/types.js.map +1 -0
  52. package/dist/patterns/scanner/meta.d.ts +2 -2
  53. package/dist/registry.d.ts.map +1 -1
  54. package/dist/registry.js +2 -0
  55. package/dist/registry.js.map +1 -1
  56. package/dist/theme/useZoraTheme.d.ts +1 -1
  57. package/package.json +3 -3
  58. package/src/components/text/Text.tsx +2 -0
  59. package/src/components/text/types.ts +7 -1
  60. package/src/expo57Migration.test.ts +18 -14
  61. package/src/index.ts +14 -0
  62. package/src/interactionPolicy.test.ts +30 -0
  63. package/src/metadata/allowedChildren.ts +1 -0
  64. package/src/metadata/bindableComponentMeta.test.ts +30 -0
  65. package/src/metadata/bindableComponentMeta.ts +102 -0
  66. package/src/metadata/componentMeta.test.ts +36 -0
  67. package/src/metadata/componentMeta.ts +2 -0
  68. package/src/patterns/reader/ReaderSurface.test.ts +46 -0
  69. package/src/patterns/reader/ReaderSurface.tsx +348 -0
  70. package/src/patterns/reader/index.ts +15 -0
  71. package/src/patterns/reader/meta.ts +296 -0
  72. package/src/patterns/reader/resolveReaderProgress.test.ts +31 -0
  73. package/src/patterns/reader/resolveReaderProgress.ts +29 -0
  74. package/src/patterns/reader/types.ts +80 -0
  75. package/src/registry.ts +2 -0
  76. package/src/showcaseCoverage.test.ts +1 -0
@@ -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,
@@ -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';