@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
@@ -0,0 +1,296 @@
1
+ import type { ZoraComponentMeta } from '../../metadata';
2
+ import { CONTAINER_ALLOWED_CHILDREN } from '../../metadata/allowedChildren';
3
+
4
+ const EMPTY_EVENT_FIELDS = [] as const;
5
+
6
+ export const readerSurfaceMeta = {
7
+ name: 'ReaderSurface',
8
+ category: 'pattern',
9
+ description:
10
+ 'Adapter-neutral EPUB and PDF reader shell with controlled chrome, progress, and navigation events.',
11
+ directManifestNode: true,
12
+ requirements: {
13
+ capabilities: [{ capability: 'ebookReader' }],
14
+ },
15
+ allowedChildren: [],
16
+ blueprint: {
17
+ label: 'Reader',
18
+ defaultProps: {
19
+ format: 'epub',
20
+ status: 'idle',
21
+ showChrome: true,
22
+ readerColorScheme: 'system',
23
+ fontScale: 1,
24
+ lineHeight: 'normal',
25
+ },
26
+ },
27
+ events: {
28
+ previousPage: {
29
+ label: 'Previous page',
30
+ eventType: 'reader.previousPage',
31
+ description: 'Requests navigation to the previous page.',
32
+ payloadFields: EMPTY_EVENT_FIELDS,
33
+ },
34
+ nextPage: {
35
+ label: 'Next page',
36
+ eventType: 'reader.nextPage',
37
+ description: 'Requests navigation to the next page.',
38
+ payloadFields: EMPTY_EVENT_FIELDS,
39
+ },
40
+ locationChange: {
41
+ label: 'Location change',
42
+ eventType: 'reader.locationChange',
43
+ description: 'Emitted by the adapter after the destination has been displayed.',
44
+ payloadFields: [
45
+ { path: 'format', type: 'string', label: 'Format' },
46
+ { path: 'locator', type: 'string', label: 'Opaque locator' },
47
+ { path: 'page', type: 'number', label: 'Page' },
48
+ { path: 'pageCount', type: 'number', label: 'Page count' },
49
+ { path: 'progression', type: 'number', label: 'Progression' },
50
+ { path: 'chapterId', type: 'string', label: 'Chapter ID' },
51
+ { path: 'chapterTitle', type: 'string', label: 'Chapter title' },
52
+ { path: 'trigger', type: 'string', label: 'Navigation trigger' },
53
+ ],
54
+ },
55
+ openContents: {
56
+ label: 'Open contents',
57
+ eventType: 'reader.openContents',
58
+ description: 'Requests the app-owned table of contents.',
59
+ payloadFields: EMPTY_EVENT_FIELDS,
60
+ },
61
+ openAppearance: {
62
+ label: 'Open appearance',
63
+ eventType: 'reader.openAppearance',
64
+ description: 'Requests the app-owned reader appearance controls.',
65
+ payloadFields: EMPTY_EVENT_FIELDS,
66
+ },
67
+ toggleHighlight: {
68
+ label: 'Toggle highlight',
69
+ eventType: 'reader.toggleHighlight',
70
+ description: 'Requests the app-owned highlight action.',
71
+ payloadFields: EMPTY_EVENT_FIELDS,
72
+ },
73
+ openExternalLink: {
74
+ label: 'Open external link',
75
+ eventType: 'reader.openExternalLink',
76
+ description: 'Emitted when publication content requests an external URL.',
77
+ payloadFields: [{ path: 'url', type: 'string', label: 'URL' }],
78
+ },
79
+ readerError: {
80
+ label: 'Reader error',
81
+ eventType: 'reader.error',
82
+ description: 'Emitted when the adapter rejects or cannot load a document.',
83
+ payloadFields: [
84
+ { path: 'code', type: 'string', label: 'Error code' },
85
+ { path: 'format', type: 'string', label: 'Format' },
86
+ { path: 'message', type: 'string', label: 'Message' },
87
+ ],
88
+ },
89
+ },
90
+ slots: {
91
+ viewport: {
92
+ label: 'Reader viewport adapter',
93
+ allowedChildren: [],
94
+ },
95
+ headerActions: {
96
+ label: 'Header actions',
97
+ allowedChildren: [...CONTAINER_ALLOWED_CHILDREN],
98
+ },
99
+ footerActions: {
100
+ label: 'Footer actions',
101
+ allowedChildren: [...CONTAINER_ALLOWED_CHILDREN],
102
+ },
103
+ },
104
+ props: {
105
+ source: {
106
+ type: 'media',
107
+ category: 'Document',
108
+ label: 'Source',
109
+ mediaKinds: ['file'],
110
+ },
111
+ format: {
112
+ type: 'enum',
113
+ category: 'Document',
114
+ label: 'Format',
115
+ enum: ['epub', 'pdf'],
116
+ default: 'epub',
117
+ },
118
+ location: {
119
+ type: 'string',
120
+ category: 'State',
121
+ label: 'Location',
122
+ },
123
+ status: {
124
+ type: 'enum',
125
+ category: 'State',
126
+ label: 'Status',
127
+ enum: ['idle', 'loading', 'ready', 'error'],
128
+ default: 'idle',
129
+ },
130
+ page: {
131
+ type: 'number',
132
+ category: 'State',
133
+ label: 'Page',
134
+ },
135
+ pageCount: {
136
+ type: 'number',
137
+ category: 'State',
138
+ label: 'Page count',
139
+ },
140
+ progress: {
141
+ type: 'number',
142
+ category: 'State',
143
+ label: 'Progress',
144
+ },
145
+ canGoPrevious: {
146
+ type: 'boolean',
147
+ category: 'State',
148
+ label: 'Can go previous',
149
+ },
150
+ canGoNext: {
151
+ type: 'boolean',
152
+ category: 'State',
153
+ label: 'Can go next',
154
+ },
155
+ title: {
156
+ type: 'string',
157
+ category: 'Content',
158
+ label: 'Title',
159
+ },
160
+ subtitle: {
161
+ type: 'string',
162
+ category: 'Content',
163
+ label: 'Subtitle',
164
+ },
165
+ chapterLabel: {
166
+ type: 'string',
167
+ category: 'Content',
168
+ label: 'Chapter label',
169
+ },
170
+ pageLabel: {
171
+ type: 'string',
172
+ category: 'Content',
173
+ label: 'Page label',
174
+ },
175
+ previousPageLabel: {
176
+ type: 'string',
177
+ category: 'Content',
178
+ label: 'Previous page label',
179
+ default: 'Previous page',
180
+ },
181
+ nextPageLabel: {
182
+ type: 'string',
183
+ category: 'Content',
184
+ label: 'Next page label',
185
+ default: 'Next page',
186
+ },
187
+ contentsLabel: {
188
+ type: 'string',
189
+ category: 'Content',
190
+ label: 'Contents label',
191
+ default: 'Table of contents',
192
+ },
193
+ appearanceLabel: {
194
+ type: 'string',
195
+ category: 'Content',
196
+ label: 'Appearance label',
197
+ default: 'Reading appearance',
198
+ },
199
+ highlightLabel: {
200
+ type: 'string',
201
+ category: 'Content',
202
+ label: 'Highlight label',
203
+ default: 'Toggle highlight',
204
+ },
205
+ loadingLabel: {
206
+ type: 'string',
207
+ category: 'Content',
208
+ label: 'Loading label',
209
+ default: 'Loading document…',
210
+ },
211
+ errorTitle: {
212
+ type: 'string',
213
+ category: 'Content',
214
+ label: 'Error title',
215
+ default: 'Unable to open this document.',
216
+ },
217
+ unavailableTitle: {
218
+ type: 'string',
219
+ category: 'Content',
220
+ label: 'Unavailable title',
221
+ default: 'Reader preview unavailable.',
222
+ },
223
+ showChrome: {
224
+ type: 'boolean',
225
+ category: 'Chrome',
226
+ label: 'Show chrome',
227
+ default: true,
228
+ },
229
+ readerColorScheme: {
230
+ type: 'enum',
231
+ category: 'Appearance',
232
+ label: 'Color scheme',
233
+ enum: ['system', 'light', 'dark', 'sepia'],
234
+ default: 'system',
235
+ },
236
+ fontScale: {
237
+ type: 'number',
238
+ category: 'Appearance',
239
+ label: 'Font scale',
240
+ default: 1,
241
+ },
242
+ lineHeight: {
243
+ type: 'enum',
244
+ category: 'Appearance',
245
+ label: 'Line height',
246
+ enum: ['compact', 'normal', 'relaxed'],
247
+ default: 'normal',
248
+ },
249
+ highlighted: {
250
+ type: 'boolean',
251
+ category: 'State',
252
+ label: 'Highlighted',
253
+ default: false,
254
+ },
255
+ onPreviousPage: {
256
+ type: 'action',
257
+ category: 'Events',
258
+ label: 'Previous page action',
259
+ },
260
+ onNextPage: {
261
+ type: 'action',
262
+ category: 'Events',
263
+ label: 'Next page action',
264
+ },
265
+ onLocationChange: {
266
+ type: 'action',
267
+ category: 'Events',
268
+ label: 'Location change action',
269
+ },
270
+ onOpenContents: {
271
+ type: 'action',
272
+ category: 'Events',
273
+ label: 'Open contents action',
274
+ },
275
+ onOpenAppearance: {
276
+ type: 'action',
277
+ category: 'Events',
278
+ label: 'Open appearance action',
279
+ },
280
+ onToggleHighlight: {
281
+ type: 'action',
282
+ category: 'Events',
283
+ label: 'Toggle highlight action',
284
+ },
285
+ onOpenExternalLink: {
286
+ type: 'action',
287
+ category: 'Events',
288
+ label: 'Open external link action',
289
+ },
290
+ onReaderError: {
291
+ type: 'action',
292
+ category: 'Events',
293
+ label: 'Reader error action',
294
+ },
295
+ },
296
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveReaderProgress } from './resolveReaderProgress';
4
+
5
+ describe('resolveReaderProgress', () => {
6
+ test('is exported from the package root', async () => {
7
+ const source = await Bun.file('src/index.ts').text();
8
+
9
+ expect(source).toContain('ReaderSurface, resolveReaderProgress');
10
+ });
11
+
12
+ test('clamps an explicit progress value', () => {
13
+ expect(resolveReaderProgress({ progress: -0.2 })).toBe(0);
14
+ expect(resolveReaderProgress({ progress: 0.35 })).toBe(0.35);
15
+ expect(resolveReaderProgress({ progress: 1.2 })).toBe(1);
16
+ });
17
+
18
+ test('derives progress from one-based page state', () => {
19
+ expect(resolveReaderProgress({ page: 1, pageCount: 5 })).toBe(0);
20
+ expect(resolveReaderProgress({ page: 3, pageCount: 5 })).toBe(0.5);
21
+ expect(resolveReaderProgress({ page: 5, pageCount: 5 })).toBe(1);
22
+ expect(resolveReaderProgress({ page: 1, pageCount: 1 })).toBe(1);
23
+ });
24
+
25
+ test('returns zero for incomplete or invalid page state', () => {
26
+ expect(resolveReaderProgress({})).toBe(0);
27
+ expect(resolveReaderProgress({ page: 1 })).toBe(0);
28
+ expect(resolveReaderProgress({ page: 1, pageCount: 0 })).toBe(0);
29
+ expect(resolveReaderProgress({ progress: Number.NaN })).toBe(0);
30
+ });
31
+ });
@@ -0,0 +1,29 @@
1
+ export function resolveReaderProgress({
2
+ progress,
3
+ page,
4
+ pageCount,
5
+ }: {
6
+ progress?: number;
7
+ page?: number;
8
+ pageCount?: number;
9
+ }): number {
10
+ if (progress !== undefined && Number.isFinite(progress)) {
11
+ return Math.min(1, Math.max(0, progress));
12
+ }
13
+
14
+ if (
15
+ page === undefined ||
16
+ pageCount === undefined ||
17
+ !Number.isFinite(page) ||
18
+ !Number.isFinite(pageCount) ||
19
+ pageCount < 1
20
+ ) {
21
+ return 0;
22
+ }
23
+
24
+ if (pageCount === 1) {
25
+ return 1;
26
+ }
27
+
28
+ return Math.min(1, Math.max(0, (page - 1) / (pageCount - 1)));
29
+ }
@@ -0,0 +1,80 @@
1
+ import type React from 'react';
2
+
3
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
4
+
5
+ export type ReaderDocumentFormat = 'epub' | 'pdf';
6
+
7
+ export type ReaderStatus = 'idle' | 'loading' | 'ready' | 'error';
8
+
9
+ export type ReaderNavigationTrigger =
10
+ 'swipe' | 'previousControl' | 'nextControl' | 'keyboard' | 'location';
11
+
12
+ export interface ReaderLocationChangeEvent {
13
+ format: ReaderDocumentFormat;
14
+ locator: string;
15
+ page: number;
16
+ pageCount?: number;
17
+ progression: number;
18
+ chapterId?: string;
19
+ chapterTitle?: string;
20
+ trigger: ReaderNavigationTrigger;
21
+ }
22
+
23
+ export type ReaderErrorCode =
24
+ 'invalid-document' | 'load-failed' | 'protected-document' | 'unsupported-format';
25
+
26
+ export interface ReaderErrorEvent {
27
+ code: ReaderErrorCode;
28
+ format?: ReaderDocumentFormat;
29
+ message: string;
30
+ }
31
+
32
+ export type ReaderColorScheme = 'system' | 'light' | 'dark' | 'sepia';
33
+
34
+ export type ReaderLineHeight = 'compact' | 'normal' | 'relaxed';
35
+
36
+ export type ReaderResolvedSource = string | number | { readonly uri: string };
37
+
38
+ export interface ReaderExternalLinkEvent {
39
+ url: string;
40
+ }
41
+
42
+ export interface ReaderSurfaceProps extends ZoraBaseProps {
43
+ source?: ReaderResolvedSource | null;
44
+ format: ReaderDocumentFormat;
45
+ location?: string;
46
+ status?: ReaderStatus;
47
+ page?: number;
48
+ pageCount?: number;
49
+ progress?: number;
50
+ canGoPrevious?: boolean;
51
+ canGoNext?: boolean;
52
+ title?: string;
53
+ subtitle?: string;
54
+ chapterLabel?: string;
55
+ pageLabel?: string;
56
+ previousPageLabel?: string;
57
+ nextPageLabel?: string;
58
+ contentsLabel?: string;
59
+ appearanceLabel?: string;
60
+ highlightLabel?: string;
61
+ loadingLabel?: string;
62
+ errorTitle?: string;
63
+ unavailableTitle?: string;
64
+ showChrome?: boolean;
65
+ readerColorScheme?: ReaderColorScheme;
66
+ fontScale?: number;
67
+ lineHeight?: ReaderLineHeight;
68
+ highlighted?: boolean;
69
+ viewport?: React.ReactNode;
70
+ headerActions?: React.ReactNode;
71
+ footerActions?: React.ReactNode;
72
+ onPreviousPage?: () => void | Promise<void>;
73
+ onNextPage?: () => void | Promise<void>;
74
+ onLocationChange?: (event: ReaderLocationChangeEvent) => void | Promise<void>;
75
+ onOpenContents?: () => void | Promise<void>;
76
+ onOpenAppearance?: () => void | Promise<void>;
77
+ onToggleHighlight?: () => void | Promise<void>;
78
+ onOpenExternalLink?: (event: ReaderExternalLinkEvent) => void | Promise<void>;
79
+ onReaderError?: (event: ReaderErrorEvent) => void | Promise<void>;
80
+ }
package/src/registry.ts CHANGED
@@ -85,6 +85,7 @@ import { Notice } from './patterns/notice';
85
85
  import { Panel } from './patterns/panel';
86
86
  import { PostCard } from './patterns/post-card';
87
87
  import { ProductCard } from './patterns/product-card';
88
+ import { ReaderSurface } from './patterns/reader';
88
89
  import { ResponsivePanel } from './patterns/responsive-panel';
89
90
  import { BarcodeScannerView, CameraPermissionView, ScanOverlay } from './patterns/scanner';
90
91
  import { SectionHeader } from './patterns/section-header';
@@ -220,6 +221,7 @@ const _ZORA_COMPONENT_REGISTRY = {
220
221
  PostCard,
221
222
  ProductCard,
222
223
  ResponsivePanel,
224
+ ReaderSurface,
223
225
  BarcodeScannerView,
224
226
  CameraPermissionView,
225
227
  ScanOverlay,
@@ -97,6 +97,7 @@ const REQUIRED_SHOWCASE_COVERAGE = {
97
97
  'Notice',
98
98
  'Panel',
99
99
  'ResponsivePanel',
100
+ 'ReaderSurface',
100
101
  'SectionHeader',
101
102
  'SettingsRow',
102
103
  'SwitchField',