@ankhorage/zora 1.0.10 → 1.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 (60) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +107 -0
  3. package/dist/components/image/Image.d.ts +4 -0
  4. package/dist/components/image/Image.d.ts.map +1 -0
  5. package/dist/components/image/Image.js +8 -0
  6. package/dist/components/image/Image.js.map +1 -0
  7. package/dist/components/image/index.d.ts +3 -0
  8. package/dist/components/image/index.d.ts.map +1 -0
  9. package/dist/components/image/index.js +2 -0
  10. package/dist/components/image/index.js.map +1 -0
  11. package/dist/components/image/types.d.ts +6 -0
  12. package/dist/components/image/types.d.ts.map +1 -0
  13. package/dist/components/image/types.js +2 -0
  14. package/dist/components/image/types.js.map +1 -0
  15. package/dist/index.d.ts +6 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/patterns/image-preview/ImagePreview.d.ts +4 -0
  20. package/dist/patterns/image-preview/ImagePreview.d.ts.map +1 -0
  21. package/dist/patterns/image-preview/ImagePreview.js +41 -0
  22. package/dist/patterns/image-preview/ImagePreview.js.map +1 -0
  23. package/dist/patterns/image-preview/index.d.ts +3 -0
  24. package/dist/patterns/image-preview/index.d.ts.map +1 -0
  25. package/dist/patterns/image-preview/index.js +2 -0
  26. package/dist/patterns/image-preview/index.js.map +1 -0
  27. package/dist/patterns/image-preview/types.d.ts +36 -0
  28. package/dist/patterns/image-preview/types.d.ts.map +1 -0
  29. package/dist/patterns/image-preview/types.js +2 -0
  30. package/dist/patterns/image-preview/types.js.map +1 -0
  31. package/dist/patterns/image-upload-field/ImageUploadField.d.ts +4 -0
  32. package/dist/patterns/image-upload-field/ImageUploadField.d.ts.map +1 -0
  33. package/dist/patterns/image-upload-field/ImageUploadField.js +211 -0
  34. package/dist/patterns/image-upload-field/ImageUploadField.js.map +1 -0
  35. package/dist/patterns/image-upload-field/index.d.ts +3 -0
  36. package/dist/patterns/image-upload-field/index.d.ts.map +1 -0
  37. package/dist/patterns/image-upload-field/index.js +2 -0
  38. package/dist/patterns/image-upload-field/index.js.map +1 -0
  39. package/dist/patterns/image-upload-field/types.d.ts +35 -0
  40. package/dist/patterns/image-upload-field/types.d.ts.map +1 -0
  41. package/dist/patterns/image-upload-field/types.js +2 -0
  42. package/dist/patterns/image-upload-field/types.js.map +1 -0
  43. package/dist/patterns/image-upload-field/uploadFlow.d.ts +18 -0
  44. package/dist/patterns/image-upload-field/uploadFlow.d.ts.map +1 -0
  45. package/dist/patterns/image-upload-field/uploadFlow.js +106 -0
  46. package/dist/patterns/image-upload-field/uploadFlow.js.map +1 -0
  47. package/package.json +3 -3
  48. package/src/components/image/Image.tsx +11 -0
  49. package/src/components/image/index.ts +2 -0
  50. package/src/components/image/types.ts +7 -0
  51. package/src/index.ts +14 -0
  52. package/src/patterns/image-preview/ImagePreview.tsx +76 -0
  53. package/src/patterns/image-preview/index.ts +2 -0
  54. package/src/patterns/image-preview/types.ts +41 -0
  55. package/src/patterns/image-upload-field/ImageUploadField.tsx +293 -0
  56. package/src/patterns/image-upload-field/index.ts +2 -0
  57. package/src/patterns/image-upload-field/types.ts +41 -0
  58. package/src/patterns/image-upload-field/uploadFlow.test.ts +117 -0
  59. package/src/patterns/image-upload-field/uploadFlow.ts +145 -0
  60. package/src/showcaseCoverage.test.ts +3 -0
@@ -0,0 +1,211 @@
1
+ import React from 'react';
2
+ import { Button } from '../../components/button';
3
+ import { Modal } from '../../components/modal';
4
+ import { Progress } from '../../components/progress';
5
+ import { Text } from '../../components/text';
6
+ import { Box, Stack } from '../../foundation';
7
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
8
+ import { FormField } from '../form-field';
9
+ import { ImagePreview } from '../image-preview';
10
+ import { createOptimisticAssetFromPicked, formatAcceptHint, formatMaxSizeHint, resolveRenderableUrl, validatePickedImage, } from './uploadFlow';
11
+ function clampProgress(value) {
12
+ if (value === null)
13
+ return null;
14
+ if (!Number.isFinite(value))
15
+ return null;
16
+ return Math.max(0, Math.min(1, value));
17
+ }
18
+ function formatUnknownError(error) {
19
+ if (error instanceof Error && error.message)
20
+ return error.message;
21
+ if (typeof error === 'string' && error.trim().length > 0)
22
+ return error;
23
+ return 'Something went wrong.';
24
+ }
25
+ function ImageUploadFieldInner({ themeId: _themeId, mode: _mode, testID, value, onChange, label, description, helperText, errorText, required, disabled = false, readOnly = false, accept, maxSizeBytes, validatePicked, onPick, onUpload, onRemove, aspectRatio = 1, previewTitle = 'Image preview', previewDescription, }) {
26
+ const [internalError, setInternalError] = React.useState(undefined);
27
+ const [uploading, setUploading] = React.useState(false);
28
+ const [removing, setRemoving] = React.useState(false);
29
+ const [progress, setProgress] = React.useState(null);
30
+ const [previewOpen, setPreviewOpen] = React.useState(false);
31
+ const isMountedRef = React.useRef(true);
32
+ React.useEffect(() => {
33
+ return () => {
34
+ isMountedRef.current = false;
35
+ };
36
+ }, []);
37
+ const renderableUrl = resolveRenderableUrl(value);
38
+ const isRenderable = renderableUrl !== null;
39
+ const actionsDisabled = disabled || readOnly;
40
+ const effectiveError = errorText ?? internalError;
41
+ const invalid = Boolean(effectiveError);
42
+ const acceptHint = formatAcceptHint(accept);
43
+ const maxSizeHint = formatMaxSizeHint(maxSizeBytes);
44
+ const setProgressSafe = React.useCallback((next) => {
45
+ if (!isMountedRef.current)
46
+ return;
47
+ setProgress(clampProgress(next));
48
+ }, []);
49
+ const clearTransientState = React.useCallback(() => {
50
+ if (!isMountedRef.current)
51
+ return;
52
+ setUploading(false);
53
+ setRemoving(false);
54
+ setProgress(null);
55
+ }, []);
56
+ const handleReplace = React.useCallback(async () => {
57
+ if (actionsDisabled || uploading || removing)
58
+ return;
59
+ setInternalError(undefined);
60
+ let picked;
61
+ try {
62
+ picked = await onPick();
63
+ }
64
+ catch (error) {
65
+ if (!isMountedRef.current)
66
+ return;
67
+ setInternalError(formatUnknownError(error));
68
+ return;
69
+ }
70
+ if (!picked)
71
+ return;
72
+ const validationError = validatePickedImage({
73
+ picked,
74
+ accept,
75
+ maxSizeBytes,
76
+ validatePicked,
77
+ });
78
+ if (validationError) {
79
+ setInternalError(validationError);
80
+ return;
81
+ }
82
+ if (!onUpload) {
83
+ onChange(createOptimisticAssetFromPicked(picked));
84
+ return;
85
+ }
86
+ const optimisticAsset = createOptimisticAssetFromPicked(picked);
87
+ onChange(optimisticAsset);
88
+ setUploading(true);
89
+ setProgressSafe(0);
90
+ try {
91
+ const uploaded = await onUpload(picked, { setProgress: setProgressSafe });
92
+ if (!isMountedRef.current)
93
+ return;
94
+ onChange(uploaded);
95
+ setInternalError(undefined);
96
+ clearTransientState();
97
+ }
98
+ catch (error) {
99
+ if (!isMountedRef.current)
100
+ return;
101
+ setInternalError(formatUnknownError(error));
102
+ setUploading(false);
103
+ setProgress(null);
104
+ }
105
+ }, [
106
+ accept,
107
+ actionsDisabled,
108
+ clearTransientState,
109
+ maxSizeBytes,
110
+ onChange,
111
+ onPick,
112
+ onUpload,
113
+ removing,
114
+ setProgressSafe,
115
+ uploading,
116
+ validatePicked,
117
+ ]);
118
+ const handleRemove = React.useCallback(async () => {
119
+ if (actionsDisabled || uploading || removing)
120
+ return;
121
+ if (!value) {
122
+ onChange(null);
123
+ return;
124
+ }
125
+ setInternalError(undefined);
126
+ if (!onRemove) {
127
+ onChange(null);
128
+ return;
129
+ }
130
+ setRemoving(true);
131
+ try {
132
+ await onRemove(value);
133
+ if (!isMountedRef.current)
134
+ return;
135
+ onChange(null);
136
+ setRemoving(false);
137
+ }
138
+ catch (error) {
139
+ if (!isMountedRef.current)
140
+ return;
141
+ setInternalError(formatUnknownError(error));
142
+ setRemoving(false);
143
+ }
144
+ }, [actionsDisabled, onChange, onRemove, removing, uploading, value]);
145
+ const handlePreview = React.useCallback(() => {
146
+ if (!isRenderable)
147
+ return;
148
+ setPreviewOpen(true);
149
+ }, [isRenderable]);
150
+ const closePreview = React.useCallback(() => setPreviewOpen(false), []);
151
+ return (<>
152
+ <FormField description={description} disabled={disabled} errorText={effectiveError} helperText={helperText} invalid={invalid} label={label} readOnly={readOnly} required={required} testID={testID}>
153
+ <Stack gap="m">
154
+ <ImagePreview aspectRatio={aspectRatio} asset={value} emptyDescription={actionsDisabled ? 'No image available.' : undefined}/>
155
+
156
+ {acceptHint || maxSizeHint ? (<Stack gap="xs">
157
+ {acceptHint ? (<Text tone="muted" variant="caption">
158
+ {acceptHint}
159
+ </Text>) : null}
160
+ {maxSizeHint ? (<Text tone="muted" variant="caption">
161
+ {maxSizeHint}
162
+ </Text>) : null}
163
+ </Stack>) : null}
164
+
165
+ {uploading ? (<Stack gap="xs">
166
+ <Text tone="muted" variant="caption">
167
+ Uploading…
168
+ </Text>
169
+ {progress !== null ? <Progress max={1} value={progress}/> : null}
170
+ </Stack>) : null}
171
+
172
+ <Stack direction={{ base: 'column', md: 'row' }} gap="s">
173
+ <Button disabled={actionsDisabled || uploading || removing} onPress={() => {
174
+ void handleReplace();
175
+ }}>
176
+ {value ? 'Replace image' : 'Select image'}
177
+ </Button>
178
+
179
+ {value ? (<Button disabled={actionsDisabled || uploading || removing} emphasis="outline" loading={removing} tone="danger" onPress={() => {
180
+ void handleRemove();
181
+ }}>
182
+ Remove
183
+ </Button>) : null}
184
+
185
+ {isRenderable ? (<Button disabled={false} emphasis="soft" tone="neutral" onPress={handlePreview}>
186
+ Preview
187
+ </Button>) : null}
188
+ </Stack>
189
+
190
+ {!isRenderable && value?.kind === 'storage' && value.publicUrl === undefined ? (<Box>
191
+ <Text tone="muted" variant="caption">
192
+ This image is stored, but no preview URL is available yet.
193
+ </Text>
194
+ </Box>) : null}
195
+ </Stack>
196
+ </FormField>
197
+
198
+ {isRenderable ? (<Modal closeOnBackdrop description={previewDescription} title={previewTitle} visible={previewOpen} onDismiss={closePreview}>
199
+ <Stack gap="m">
200
+ <ImagePreview asset={value} aspectRatio={aspectRatio}/>
201
+ <Stack direction="row" justify="flex-end">
202
+ <Button emphasis="soft" tone="neutral" onPress={closePreview}>
203
+ Close
204
+ </Button>
205
+ </Stack>
206
+ </Stack>
207
+ </Modal>) : null}
208
+ </>);
209
+ }
210
+ export const ImageUploadField = withZoraThemeScope(ImageUploadFieldInner);
211
+ //# sourceMappingURL=ImageUploadField.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageUploadField.js","sourceRoot":"","sources":["../../../src/patterns/image-upload-field/ImageUploadField.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\n\nimport { Button } from '../../components/button';\nimport { Modal } from '../../components/modal';\nimport { Progress } from '../../components/progress';\nimport { Text } from '../../components/text';\nimport { Box, Stack } from '../../foundation';\nimport { withZoraThemeScope } from '../../theme/withZoraThemeScope';\nimport { FormField } from '../form-field';\nimport { ImagePreview } from '../image-preview';\nimport type { ImageUploadFieldProps } from './types';\nimport {\n createOptimisticAssetFromPicked,\n formatAcceptHint,\n formatMaxSizeHint,\n resolveRenderableUrl,\n validatePickedImage,\n} from './uploadFlow';\n\nfunction clampProgress(value: number | null): number | null {\n if (value === null) return null;\n if (!Number.isFinite(value)) return null;\n return Math.max(0, Math.min(1, value));\n}\n\nfunction formatUnknownError(error: unknown): string {\n if (error instanceof Error && error.message) return error.message;\n if (typeof error === 'string' && error.trim().length > 0) return error;\n return 'Something went wrong.';\n}\n\nfunction ImageUploadFieldInner({\n themeId: _themeId,\n mode: _mode,\n testID,\n value,\n onChange,\n label,\n description,\n helperText,\n errorText,\n required,\n disabled = false,\n readOnly = false,\n accept,\n maxSizeBytes,\n validatePicked,\n onPick,\n onUpload,\n onRemove,\n aspectRatio = 1,\n previewTitle = 'Image preview',\n previewDescription,\n}: ImageUploadFieldProps) {\n const [internalError, setInternalError] = React.useState<string | undefined>(undefined);\n const [uploading, setUploading] = React.useState(false);\n const [removing, setRemoving] = React.useState(false);\n const [progress, setProgress] = React.useState<number | null>(null);\n const [previewOpen, setPreviewOpen] = React.useState(false);\n const isMountedRef = React.useRef(true);\n\n React.useEffect(() => {\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n const renderableUrl = resolveRenderableUrl(value);\n const isRenderable = renderableUrl !== null;\n const actionsDisabled = disabled || readOnly;\n\n const effectiveError = errorText ?? internalError;\n const invalid = Boolean(effectiveError);\n\n const acceptHint = formatAcceptHint(accept);\n const maxSizeHint = formatMaxSizeHint(maxSizeBytes);\n\n const setProgressSafe = React.useCallback((next: number | null) => {\n if (!isMountedRef.current) return;\n setProgress(clampProgress(next));\n }, []);\n\n const clearTransientState = React.useCallback(() => {\n if (!isMountedRef.current) return;\n setUploading(false);\n setRemoving(false);\n setProgress(null);\n }, []);\n\n const handleReplace = React.useCallback(async () => {\n if (actionsDisabled || uploading || removing) return;\n\n setInternalError(undefined);\n let picked;\n try {\n picked = await onPick();\n } catch (error) {\n if (!isMountedRef.current) return;\n setInternalError(formatUnknownError(error));\n return;\n }\n if (!picked) return;\n\n const validationError = validatePickedImage({\n picked,\n accept,\n maxSizeBytes,\n validatePicked,\n });\n\n if (validationError) {\n setInternalError(validationError);\n return;\n }\n\n if (!onUpload) {\n onChange(createOptimisticAssetFromPicked(picked));\n return;\n }\n\n const optimisticAsset = createOptimisticAssetFromPicked(picked);\n onChange(optimisticAsset);\n setUploading(true);\n setProgressSafe(0);\n\n try {\n const uploaded = await onUpload(picked, { setProgress: setProgressSafe });\n if (!isMountedRef.current) return;\n onChange(uploaded);\n setInternalError(undefined);\n clearTransientState();\n } catch (error) {\n if (!isMountedRef.current) return;\n setInternalError(formatUnknownError(error));\n setUploading(false);\n setProgress(null);\n }\n }, [\n accept,\n actionsDisabled,\n clearTransientState,\n maxSizeBytes,\n onChange,\n onPick,\n onUpload,\n removing,\n setProgressSafe,\n uploading,\n validatePicked,\n ]);\n\n const handleRemove = React.useCallback(async () => {\n if (actionsDisabled || uploading || removing) return;\n if (!value) {\n onChange(null);\n return;\n }\n\n setInternalError(undefined);\n\n if (!onRemove) {\n onChange(null);\n return;\n }\n\n setRemoving(true);\n try {\n await onRemove(value);\n if (!isMountedRef.current) return;\n onChange(null);\n setRemoving(false);\n } catch (error) {\n if (!isMountedRef.current) return;\n setInternalError(formatUnknownError(error));\n setRemoving(false);\n }\n }, [actionsDisabled, onChange, onRemove, removing, uploading, value]);\n\n const handlePreview = React.useCallback(() => {\n if (!isRenderable) return;\n setPreviewOpen(true);\n }, [isRenderable]);\n\n const closePreview = React.useCallback(() => setPreviewOpen(false), []);\n\n return (\n <>\n <FormField\n description={description}\n disabled={disabled}\n errorText={effectiveError}\n helperText={helperText}\n invalid={invalid}\n label={label}\n readOnly={readOnly}\n required={required}\n testID={testID}\n >\n <Stack gap=\"m\">\n <ImagePreview\n aspectRatio={aspectRatio}\n asset={value}\n emptyDescription={actionsDisabled ? 'No image available.' : undefined}\n />\n\n {acceptHint || maxSizeHint ? (\n <Stack gap=\"xs\">\n {acceptHint ? (\n <Text tone=\"muted\" variant=\"caption\">\n {acceptHint}\n </Text>\n ) : null}\n {maxSizeHint ? (\n <Text tone=\"muted\" variant=\"caption\">\n {maxSizeHint}\n </Text>\n ) : null}\n </Stack>\n ) : null}\n\n {uploading ? (\n <Stack gap=\"xs\">\n <Text tone=\"muted\" variant=\"caption\">\n Uploading…\n </Text>\n {progress !== null ? <Progress max={1} value={progress} /> : null}\n </Stack>\n ) : null}\n\n <Stack direction={{ base: 'column', md: 'row' }} gap=\"s\">\n <Button\n disabled={actionsDisabled || uploading || removing}\n onPress={() => {\n void handleReplace();\n }}\n >\n {value ? 'Replace image' : 'Select image'}\n </Button>\n\n {value ? (\n <Button\n disabled={actionsDisabled || uploading || removing}\n emphasis=\"outline\"\n loading={removing}\n tone=\"danger\"\n onPress={() => {\n void handleRemove();\n }}\n >\n Remove\n </Button>\n ) : null}\n\n {isRenderable ? (\n <Button disabled={false} emphasis=\"soft\" tone=\"neutral\" onPress={handlePreview}>\n Preview\n </Button>\n ) : null}\n </Stack>\n\n {!isRenderable && value?.kind === 'storage' && value.publicUrl === undefined ? (\n <Box>\n <Text tone=\"muted\" variant=\"caption\">\n This image is stored, but no preview URL is available yet.\n </Text>\n </Box>\n ) : null}\n </Stack>\n </FormField>\n\n {isRenderable ? (\n <Modal\n closeOnBackdrop\n description={previewDescription}\n title={previewTitle}\n visible={previewOpen}\n onDismiss={closePreview}\n >\n <Stack gap=\"m\">\n <ImagePreview asset={value} aspectRatio={aspectRatio} />\n <Stack direction=\"row\" justify=\"flex-end\">\n <Button emphasis=\"soft\" tone=\"neutral\" onPress={closePreview}>\n Close\n </Button>\n </Stack>\n </Stack>\n </Modal>\n ) : null}\n </>\n );\n}\n\nexport const ImageUploadField = withZoraThemeScope(ImageUploadFieldInner);\n"]}
@@ -0,0 +1,3 @@
1
+ export { ImageUploadField } from './ImageUploadField';
2
+ export type { ImageUploadFieldProps, ImageUploadProgressContext, ZoraPickedImage } from './types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/patterns/image-upload-field/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { ImageUploadField } from './ImageUploadField';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/patterns/image-upload-field/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC","sourcesContent":["export { ImageUploadField } from './ImageUploadField';\nexport type { ImageUploadFieldProps, ImageUploadProgressContext, ZoraPickedImage } from './types';\n"]}
@@ -0,0 +1,35 @@
1
+ import type React from 'react';
2
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
3
+ import type { ZoraImageAsset } from '../image-preview';
4
+ export interface ZoraPickedImage {
5
+ uri: string;
6
+ fileName?: string;
7
+ sizeBytes?: number;
8
+ contentType?: string;
9
+ width?: number;
10
+ height?: number;
11
+ }
12
+ export interface ImageUploadProgressContext {
13
+ setProgress: (progress: number | null) => void;
14
+ }
15
+ export interface ImageUploadFieldProps extends ZoraBaseProps {
16
+ value: ZoraImageAsset | null;
17
+ onChange: (next: ZoraImageAsset | null) => void;
18
+ label: React.ReactNode;
19
+ description?: React.ReactNode;
20
+ helperText?: React.ReactNode;
21
+ errorText?: React.ReactNode;
22
+ required?: boolean;
23
+ disabled?: boolean;
24
+ readOnly?: boolean;
25
+ accept?: string;
26
+ maxSizeBytes?: number;
27
+ validatePicked?: (picked: ZoraPickedImage) => string | undefined;
28
+ onPick: () => Promise<ZoraPickedImage | null>;
29
+ onUpload?: (picked: ZoraPickedImage, context: ImageUploadProgressContext) => Promise<ZoraImageAsset>;
30
+ onRemove?: (current: ZoraImageAsset) => void | Promise<void>;
31
+ aspectRatio?: number;
32
+ previewTitle?: React.ReactNode;
33
+ previewDescription?: React.ReactNode;
34
+ }
35
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/patterns/image-upload-field/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAChD;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC7B,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,MAAM,GAAG,SAAS,CAAC;IACjE,MAAM,EAAE,MAAM,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IAC9C,QAAQ,CAAC,EAAE,CACT,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,0BAA0B,KAChC,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B,kBAAkB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACtC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/patterns/image-upload-field/types.ts"],"names":[],"mappings":"","sourcesContent":["import type React from 'react';\n\nimport type { ZoraBaseProps } from '../../theme/ZoraBaseProps';\nimport type { ZoraImageAsset } from '../image-preview';\n\nexport interface ZoraPickedImage {\n uri: string;\n fileName?: string;\n sizeBytes?: number;\n contentType?: string;\n width?: number;\n height?: number;\n}\n\nexport interface ImageUploadProgressContext {\n setProgress: (progress: number | null) => void;\n}\n\nexport interface ImageUploadFieldProps extends ZoraBaseProps {\n value: ZoraImageAsset | null;\n onChange: (next: ZoraImageAsset | null) => void;\n label: React.ReactNode;\n description?: React.ReactNode;\n helperText?: React.ReactNode;\n errorText?: React.ReactNode;\n required?: boolean;\n disabled?: boolean;\n readOnly?: boolean;\n accept?: string;\n maxSizeBytes?: number;\n validatePicked?: (picked: ZoraPickedImage) => string | undefined;\n onPick: () => Promise<ZoraPickedImage | null>;\n onUpload?: (\n picked: ZoraPickedImage,\n context: ImageUploadProgressContext,\n ) => Promise<ZoraImageAsset>;\n onRemove?: (current: ZoraImageAsset) => void | Promise<void>;\n aspectRatio?: number;\n previewTitle?: React.ReactNode;\n previewDescription?: React.ReactNode;\n}\n"]}
@@ -0,0 +1,18 @@
1
+ import type { ZoraImageAsset } from '../image-preview';
2
+ import type { ZoraPickedImage } from './types';
3
+ export declare function isAccepted({ accept, contentType, fileName, }: {
4
+ accept: string | undefined;
5
+ contentType: string | undefined;
6
+ fileName: string | undefined;
7
+ }): boolean;
8
+ export declare function formatAcceptHint(accept: string | undefined): string | null;
9
+ export declare function formatMaxSizeHint(maxSizeBytes: number | undefined): string | null;
10
+ export declare function createOptimisticAssetFromPicked(picked: ZoraPickedImage): ZoraImageAsset;
11
+ export declare function resolveRenderableUrl(asset: ZoraImageAsset | null): string | null;
12
+ export declare function validatePickedImage({ picked, accept, maxSizeBytes, validatePicked, }: {
13
+ picked: ZoraPickedImage;
14
+ accept: string | undefined;
15
+ maxSizeBytes: number | undefined;
16
+ validatePicked: ((picked: ZoraPickedImage) => string | undefined) | undefined;
17
+ }): string | undefined;
18
+ //# sourceMappingURL=uploadFlow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploadFlow.d.ts","sourceRoot":"","sources":["../../../src/patterns/image-upload-field/uploadFlow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAuC/C,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,WAAW,EACX,QAAQ,GACT,EAAE;IACD,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B,GAAG,OAAO,CAkBV;AAmBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAI1E;AAED,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAIjF;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,CAYvF;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAIhF;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,MAAM,EACN,YAAY,EACZ,cAAc,GACf,EAAE;IACD,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,eAAe,KAAK,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;CAC/E,GAAG,MAAM,GAAG,SAAS,CAiBrB"}
@@ -0,0 +1,106 @@
1
+ function parseAccept(accept) {
2
+ if (!accept)
3
+ return [];
4
+ return accept
5
+ .split(',')
6
+ .map((entry) => entry.trim())
7
+ .filter((entry) => entry.length > 0);
8
+ }
9
+ function matchesAcceptToken({ token, contentType, fileName, }) {
10
+ const normalizedToken = token.toLowerCase();
11
+ const normalizedContentType = contentType?.toLowerCase();
12
+ const normalizedFileName = fileName?.toLowerCase();
13
+ if (normalizedToken.startsWith('.')) {
14
+ if (!normalizedFileName)
15
+ return null;
16
+ return normalizedFileName.endsWith(normalizedToken);
17
+ }
18
+ if (normalizedToken.endsWith('/*')) {
19
+ if (!normalizedContentType)
20
+ return null;
21
+ const prefix = normalizedToken.slice(0, Math.max(0, normalizedToken.length - 1));
22
+ return normalizedContentType.startsWith(prefix);
23
+ }
24
+ if (!normalizedContentType)
25
+ return null;
26
+ return normalizedContentType === normalizedToken;
27
+ }
28
+ export function isAccepted({ accept, contentType, fileName, }) {
29
+ const tokens = parseAccept(accept);
30
+ if (tokens.length === 0)
31
+ return true;
32
+ let hadSignal = false;
33
+ for (const token of tokens) {
34
+ const matches = matchesAcceptToken({ token, contentType, fileName });
35
+ if (matches === null) {
36
+ continue;
37
+ }
38
+ hadSignal = true;
39
+ if (matches)
40
+ return true;
41
+ }
42
+ // If we cannot validate due to missing metadata, do not block.
43
+ return !hadSignal;
44
+ }
45
+ function formatBytes(value) {
46
+ if (!Number.isFinite(value) || value <= 0)
47
+ return '0 B';
48
+ const units = ['B', 'KB', 'MB', 'GB'];
49
+ let size = value;
50
+ let unitIndex = 0;
51
+ while (size >= 1024 && unitIndex < units.length - 1) {
52
+ size /= 1024;
53
+ unitIndex += 1;
54
+ }
55
+ const unit = units[unitIndex] ?? 'B';
56
+ const rounded = unitIndex === 0 ? Math.round(size) : Math.round(size * 10) / 10;
57
+ return `${rounded} ${unit}`;
58
+ }
59
+ export function formatAcceptHint(accept) {
60
+ const tokens = parseAccept(accept);
61
+ if (tokens.length === 0)
62
+ return null;
63
+ return `Accepted: ${tokens.join(', ')}`;
64
+ }
65
+ export function formatMaxSizeHint(maxSizeBytes) {
66
+ if (maxSizeBytes === undefined)
67
+ return null;
68
+ if (!Number.isFinite(maxSizeBytes) || maxSizeBytes <= 0)
69
+ return null;
70
+ return `Max size: ${formatBytes(maxSizeBytes)}`;
71
+ }
72
+ export function createOptimisticAssetFromPicked(picked) {
73
+ return {
74
+ kind: 'url',
75
+ url: picked.uri,
76
+ width: picked.width,
77
+ height: picked.height,
78
+ contentType: picked.contentType,
79
+ metadata: {
80
+ fileName: picked.fileName,
81
+ sizeBytes: picked.sizeBytes,
82
+ },
83
+ };
84
+ }
85
+ export function resolveRenderableUrl(asset) {
86
+ if (!asset)
87
+ return null;
88
+ if (asset.kind === 'url')
89
+ return asset.url;
90
+ return asset.publicUrl ?? null;
91
+ }
92
+ export function validatePickedImage({ picked, accept, maxSizeBytes, validatePicked, }) {
93
+ if (!isAccepted({ accept, contentType: picked.contentType, fileName: picked.fileName })) {
94
+ return accept ? `File type not accepted. Expected ${accept}.` : 'File type not accepted.';
95
+ }
96
+ if (maxSizeBytes !== undefined &&
97
+ Number.isFinite(maxSizeBytes) &&
98
+ maxSizeBytes > 0 &&
99
+ picked.sizeBytes !== undefined &&
100
+ Number.isFinite(picked.sizeBytes) &&
101
+ picked.sizeBytes > maxSizeBytes) {
102
+ return `File is too large (${formatBytes(picked.sizeBytes)}). Max ${formatBytes(maxSizeBytes)}.`;
103
+ }
104
+ return validatePicked?.(picked);
105
+ }
106
+ //# sourceMappingURL=uploadFlow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploadFlow.js","sourceRoot":"","sources":["../../../src/patterns/image-upload-field/uploadFlow.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ZoraImageAsset } from '../image-preview';\nimport type { ZoraPickedImage } from './types';\n\nfunction parseAccept(accept: string | undefined): readonly string[] {\n if (!accept) return [];\n\n return accept\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0);\n}\n\nfunction matchesAcceptToken({\n token,\n contentType,\n fileName,\n}: {\n token: string;\n contentType: string | undefined;\n fileName: string | undefined;\n}): boolean | null {\n const normalizedToken = token.toLowerCase();\n const normalizedContentType = contentType?.toLowerCase();\n const normalizedFileName = fileName?.toLowerCase();\n\n if (normalizedToken.startsWith('.')) {\n if (!normalizedFileName) return null;\n return normalizedFileName.endsWith(normalizedToken);\n }\n\n if (normalizedToken.endsWith('/*')) {\n if (!normalizedContentType) return null;\n const prefix = normalizedToken.slice(0, Math.max(0, normalizedToken.length - 1));\n return normalizedContentType.startsWith(prefix);\n }\n\n if (!normalizedContentType) return null;\n return normalizedContentType === normalizedToken;\n}\n\nexport function isAccepted({\n accept,\n contentType,\n fileName,\n}: {\n accept: string | undefined;\n contentType: string | undefined;\n fileName: string | undefined;\n}): boolean {\n const tokens = parseAccept(accept);\n if (tokens.length === 0) return true;\n\n let hadSignal = false;\n\n for (const token of tokens) {\n const matches = matchesAcceptToken({ token, contentType, fileName });\n if (matches === null) {\n continue;\n }\n\n hadSignal = true;\n if (matches) return true;\n }\n\n // If we cannot validate due to missing metadata, do not block.\n return !hadSignal;\n}\n\nfunction formatBytes(value: number): string {\n if (!Number.isFinite(value) || value <= 0) return '0 B';\n\n const units = ['B', 'KB', 'MB', 'GB'] as const;\n let size = value;\n let unitIndex = 0;\n\n while (size >= 1024 && unitIndex < units.length - 1) {\n size /= 1024;\n unitIndex += 1;\n }\n\n const unit = units[unitIndex] ?? 'B';\n const rounded = unitIndex === 0 ? Math.round(size) : Math.round(size * 10) / 10;\n return `${rounded} ${unit}`;\n}\n\nexport function formatAcceptHint(accept: string | undefined): string | null {\n const tokens = parseAccept(accept);\n if (tokens.length === 0) return null;\n return `Accepted: ${tokens.join(', ')}`;\n}\n\nexport function formatMaxSizeHint(maxSizeBytes: number | undefined): string | null {\n if (maxSizeBytes === undefined) return null;\n if (!Number.isFinite(maxSizeBytes) || maxSizeBytes <= 0) return null;\n return `Max size: ${formatBytes(maxSizeBytes)}`;\n}\n\nexport function createOptimisticAssetFromPicked(picked: ZoraPickedImage): ZoraImageAsset {\n return {\n kind: 'url',\n url: picked.uri,\n width: picked.width,\n height: picked.height,\n contentType: picked.contentType,\n metadata: {\n fileName: picked.fileName,\n sizeBytes: picked.sizeBytes,\n },\n };\n}\n\nexport function resolveRenderableUrl(asset: ZoraImageAsset | null): string | null {\n if (!asset) return null;\n if (asset.kind === 'url') return asset.url;\n return asset.publicUrl ?? null;\n}\n\nexport function validatePickedImage({\n picked,\n accept,\n maxSizeBytes,\n validatePicked,\n}: {\n picked: ZoraPickedImage;\n accept: string | undefined;\n maxSizeBytes: number | undefined;\n validatePicked: ((picked: ZoraPickedImage) => string | undefined) | undefined;\n}): string | undefined {\n if (!isAccepted({ accept, contentType: picked.contentType, fileName: picked.fileName })) {\n return accept ? `File type not accepted. Expected ${accept}.` : 'File type not accepted.';\n }\n\n if (\n maxSizeBytes !== undefined &&\n Number.isFinite(maxSizeBytes) &&\n maxSizeBytes > 0 &&\n picked.sizeBytes !== undefined &&\n Number.isFinite(picked.sizeBytes) &&\n picked.sizeBytes > maxSizeBytes\n ) {\n return `File is too large (${formatBytes(picked.sizeBytes)}). Max ${formatBytes(maxSizeBytes)}.`;\n }\n\n return validatePicked?.(picked);\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/zora",
3
3
  "type": "module",
4
- "version": "1.0.10",
4
+ "version": "1.1.0",
5
5
  "description": "Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.",
6
6
  "homepage": "https://github.com/ankhorage/zora#readme",
7
7
  "bugs": {
@@ -44,8 +44,8 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@ankhorage/color-theory": "^0.0.4",
47
- "@ankhorage/contracts": "^1.1.1",
48
- "@ankhorage/surface": "^1.1.0"
47
+ "@ankhorage/contracts": "^1.2.0",
48
+ "@ankhorage/surface": "^1.2.0"
49
49
  },
50
50
  "files": [
51
51
  "dist",
@@ -0,0 +1,11 @@
1
+ import { Image as SurfaceImage } from '@ankhorage/surface';
2
+ import React from 'react';
3
+
4
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
5
+ import type { ImageProps } from './types';
6
+
7
+ function ImageInner({ themeId: _themeId, mode: _mode, ...props }: ImageProps) {
8
+ return <SurfaceImage {...props} />;
9
+ }
10
+
11
+ export const Image = withZoraThemeScope(ImageInner);
@@ -0,0 +1,2 @@
1
+ export { Image } from './Image';
2
+ export type { ImageFit, ImageProps, SurfaceImageSource } from './types';
@@ -0,0 +1,7 @@
1
+ import type { ImageProps as SurfaceImageProps } from '@ankhorage/surface';
2
+
3
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
4
+
5
+ export type { ImageFit, SurfaceImageSource } from '@ankhorage/surface';
6
+
7
+ export interface ImageProps extends ZoraBaseProps, Omit<SurfaceImageProps, 'mode' | 'themeId'> {}
package/src/index.ts CHANGED
@@ -58,6 +58,8 @@ export type { IconProps } from './components/icon';
58
58
  export { Icon } from './components/icon';
59
59
  export type { IconButtonProps } from './components/icon-button';
60
60
  export { IconButton } from './components/icon-button';
61
+ export type { ImageFit, ImageProps, SurfaceImageSource } from './components/image';
62
+ export { Image } from './components/image';
61
63
  export type { InputProps, InputTrailingAction } from './components/input';
62
64
  export { Input } from './components/input';
63
65
  export type { MediaCardImageProps, MediaCardProps } from './components/media-card';
@@ -160,6 +162,18 @@ export type { EmptyStateAction, EmptyStateProps } from './patterns/empty-state';
160
162
  export { EmptyState } from './patterns/empty-state';
161
163
  export type { FilterBarProps } from './patterns/filter-bar';
162
164
  export { FilterBar } from './patterns/filter-bar';
165
+ export type {
166
+ ImagePreviewProps,
167
+ ZoraImageAsset,
168
+ ZoraImageMetadata,
169
+ } from './patterns/image-preview';
170
+ export { ImagePreview } from './patterns/image-preview';
171
+ export type {
172
+ ImageUploadFieldProps,
173
+ ImageUploadProgressContext,
174
+ ZoraPickedImage,
175
+ } from './patterns/image-upload-field';
176
+ export { ImageUploadField } from './patterns/image-upload-field';
163
177
  export type { InspectorFieldProps } from './patterns/inspector-field';
164
178
  export { InspectorField } from './patterns/inspector-field';
165
179
  export type {
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+
3
+ import { Image } from '../../components/image';
4
+ import { Text } from '../../components/text';
5
+ import { Box, Stack } from '../../foundation';
6
+ import { useZoraTheme } from '../../theme/useZoraTheme';
7
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
8
+ import type { ImagePreviewProps, ZoraImageAsset } from './types';
9
+
10
+ function resolveRenderableUrl(asset: ZoraImageAsset | null | undefined): string | null {
11
+ if (!asset) return null;
12
+
13
+ if (asset.kind === 'url') {
14
+ return asset.url;
15
+ }
16
+
17
+ return asset.publicUrl ?? null;
18
+ }
19
+
20
+ function resolveSafeAspectRatio(value: number | undefined): number {
21
+ if (!value || !Number.isFinite(value) || value <= 0) {
22
+ return 1;
23
+ }
24
+
25
+ return value;
26
+ }
27
+
28
+ function ImagePreviewInner({
29
+ themeId: _themeId,
30
+ mode: _mode,
31
+ testID,
32
+ asset,
33
+ aspectRatio,
34
+ fit = 'cover',
35
+ emptyTitle = 'No image',
36
+ emptyDescription = 'Select an image to preview it here.',
37
+ }: ImagePreviewProps) {
38
+ const { theme } = useZoraTheme();
39
+ const renderableUrl = resolveRenderableUrl(asset);
40
+ const resolvedAspectRatio = resolveSafeAspectRatio(aspectRatio);
41
+
42
+ return (
43
+ <Box
44
+ bg={theme.semantics.neutral.surface}
45
+ borderColor={theme.semantics.neutral.divider}
46
+ borderWidth={1}
47
+ radius="l"
48
+ testID={testID}
49
+ style={{ overflow: 'hidden' }}
50
+ >
51
+ {renderableUrl ? (
52
+ <Box style={{ aspectRatio: resolvedAspectRatio, width: '100%' }}>
53
+ <Image
54
+ alt={asset?.alt}
55
+ fit={fit}
56
+ source={renderableUrl}
57
+ style={{ height: '100%', width: '100%' }}
58
+ />
59
+ </Box>
60
+ ) : (
61
+ <Box p="l">
62
+ <Stack gap="xs">
63
+ <Text variant="label" weight="semiBold">
64
+ {emptyTitle}
65
+ </Text>
66
+ <Text tone="muted" variant="bodySmall">
67
+ {emptyDescription}
68
+ </Text>
69
+ </Stack>
70
+ </Box>
71
+ )}
72
+ </Box>
73
+ );
74
+ }
75
+
76
+ export const ImagePreview = withZoraThemeScope(ImagePreviewInner);
@@ -0,0 +1,2 @@
1
+ export { ImagePreview } from './ImagePreview';
2
+ export type { ImagePreviewProps, ZoraImageAsset, ZoraImageMetadata } from './types';