@byline/admin 2.5.2 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fields/array/array-field.d.ts +14 -0
- package/dist/fields/array/array-field.js +177 -0
- package/dist/fields/array/array-field.module.js +11 -0
- package/dist/fields/array/array-field_module.css +32 -0
- package/dist/fields/blocks/blocks-field.d.ts +13 -0
- package/dist/fields/blocks/blocks-field.js +245 -0
- package/dist/fields/blocks/blocks-field.module.js +26 -0
- package/dist/fields/blocks/blocks-field_module.css +107 -0
- package/dist/fields/checkbox/checkbox-field.d.ts +16 -0
- package/dist/fields/checkbox/checkbox-field.js +28 -0
- package/dist/fields/checkbox/checkbox-field.module.js +6 -0
- package/dist/fields/checkbox/checkbox-field_module.css +4 -0
- package/dist/fields/column-formatter.d.ts +20 -0
- package/dist/fields/column-formatter.js +15 -0
- package/dist/fields/date-time-formatter.d.ts +16 -0
- package/dist/fields/date-time-formatter.js +8 -0
- package/dist/fields/datetime/datetime-field.d.ts +16 -0
- package/dist/fields/datetime/datetime-field.js +37 -0
- package/dist/fields/datetime/datetime-field.module.js +5 -0
- package/dist/fields/datetime/datetime-field_module.css +4 -0
- package/dist/fields/draggable-context-menu.d.ts +6 -0
- package/dist/fields/draggable-context-menu.js +85 -0
- package/dist/fields/draggable-context-menu.module.js +15 -0
- package/dist/fields/draggable-context-menu_module.css +91 -0
- package/dist/fields/field-helpers.d.ts +26 -0
- package/dist/fields/field-helpers.js +50 -0
- package/dist/fields/field-renderer.d.ts +37 -0
- package/dist/fields/field-renderer.js +206 -0
- package/dist/fields/field-renderer.module.js +8 -0
- package/dist/fields/field-renderer_module.css +11 -0
- package/dist/fields/field-services-context.d.ts +16 -0
- package/dist/fields/field-services-context.js +13 -0
- package/dist/fields/field-services-types.d.ts +63 -0
- package/dist/fields/field-services-types.js +1 -0
- package/dist/fields/file/file-field.d.ts +19 -0
- package/dist/fields/file/file-field.js +225 -0
- package/dist/fields/file/file-field.module.js +18 -0
- package/dist/fields/file/file-field_module.css +131 -0
- package/dist/fields/file/file-upload-field.d.ts +21 -0
- package/dist/fields/file/file-upload-field.js +130 -0
- package/dist/fields/file/file-upload-field.module.js +15 -0
- package/dist/fields/file/file-upload-field_module.css +74 -0
- package/dist/fields/group/group-field.d.ts +15 -0
- package/dist/fields/group/group-field.js +59 -0
- package/dist/fields/group/group-field.module.js +9 -0
- package/dist/fields/group/group-field_module.css +27 -0
- package/dist/fields/image/image-field.d.ts +19 -0
- package/dist/fields/image/image-field.js +241 -0
- package/dist/fields/image/image-field.module.js +22 -0
- package/dist/fields/image/image-field_module.css +121 -0
- package/dist/fields/image/image-upload-field.d.ts +21 -0
- package/dist/fields/image/image-upload-field.js +190 -0
- package/dist/fields/image/image-upload-field.module.js +19 -0
- package/dist/fields/image/image-upload-field_module.css +92 -0
- package/dist/fields/local-date-time.d.ts +27 -0
- package/dist/fields/local-date-time.js +49 -0
- package/dist/fields/locale-badge.d.ts +18 -0
- package/dist/fields/locale-badge.js +10 -0
- package/dist/fields/locale-badge.module.js +5 -0
- package/dist/fields/locale-badge_module.css +27 -0
- package/dist/fields/numerical/numerical-field.d.ts +18 -0
- package/dist/fields/numerical/numerical-field.js +74 -0
- package/dist/fields/relation/relation-display.d.ts +40 -0
- package/dist/fields/relation/relation-display.js +58 -0
- package/dist/fields/relation/relation-display.module.js +9 -0
- package/dist/fields/relation/relation-display_module.css +21 -0
- package/dist/fields/relation/relation-field.d.ts +18 -0
- package/dist/fields/relation/relation-field.js +138 -0
- package/dist/fields/relation/relation-field.module.js +13 -0
- package/dist/fields/relation/relation-field_module.css +62 -0
- package/dist/fields/relation/relation-picker.d.ts +49 -0
- package/dist/fields/relation/relation-picker.js +236 -0
- package/dist/fields/relation/relation-picker.module.js +26 -0
- package/dist/fields/relation/relation-picker_module.css +124 -0
- package/dist/fields/relation/relation-summary.d.ts +31 -0
- package/dist/fields/relation/relation-summary.js +50 -0
- package/dist/fields/relation/relation-summary.module.js +11 -0
- package/dist/fields/relation/relation-summary_module.css +37 -0
- package/dist/fields/select/select-field.d.ts +16 -0
- package/dist/fields/select/select-field.js +50 -0
- package/dist/fields/select/select-field.module.js +5 -0
- package/dist/fields/select/select-field_module.css +4 -0
- package/dist/fields/sortable-item.d.ts +15 -0
- package/dist/fields/sortable-item.js +81 -0
- package/dist/fields/sortable-item.module.js +22 -0
- package/dist/fields/sortable-item_module.css +124 -0
- package/dist/fields/text/text-field.d.ts +20 -0
- package/dist/fields/text/text-field.js +104 -0
- package/dist/fields/text/text-field.module.js +6 -0
- package/dist/fields/text/text-field_module.css +5 -0
- package/dist/fields/text-area/text-area-field.d.ts +20 -0
- package/dist/fields/text-area/text-area-field.js +105 -0
- package/dist/fields/text-area/text-area-field.module.js +6 -0
- package/dist/fields/text-area/text-area-field_module.css +5 -0
- package/dist/fields/use-field-change-handler.d.ts +23 -0
- package/dist/fields/use-field-change-handler.js +52 -0
- package/dist/forms/document-actions.d.ts +48 -0
- package/dist/forms/document-actions.js +475 -0
- package/dist/forms/document-actions.module.js +34 -0
- package/dist/forms/document-actions_module.css +118 -0
- package/dist/forms/form-context.d.ts +89 -0
- package/dist/forms/form-context.js +466 -0
- package/dist/forms/form-renderer.d.ts +98 -0
- package/dist/forms/form-renderer.js +597 -0
- package/dist/forms/form-renderer.module.js +46 -0
- package/dist/forms/form-renderer_module.css +245 -0
- package/dist/forms/navigation-guard.d.ts +54 -0
- package/dist/forms/navigation-guard.js +22 -0
- package/dist/forms/path-widget.d.ts +36 -0
- package/dist/forms/path-widget.js +116 -0
- package/dist/forms/path-widget.module.js +8 -0
- package/dist/forms/path-widget_module.css +29 -0
- package/dist/forms/upload-executor.d.ts +57 -0
- package/dist/forms/upload-executor.js +94 -0
- package/dist/lib/translate-validation-error.d.ts +36 -0
- package/dist/lib/translate-validation-error.js +11 -0
- package/dist/modules/admin-account/commands.d.ts +2 -1
- package/dist/modules/admin-account/commands.js +13 -2
- package/dist/modules/admin-account/components/change-password.js +45 -36
- package/dist/modules/admin-account/components/container.js +185 -134
- package/dist/modules/admin-account/components/preferences.d.ts +8 -0
- package/dist/modules/admin-account/components/preferences.js +152 -0
- package/dist/modules/admin-account/components/preferences.module.js +11 -0
- package/dist/modules/admin-account/components/preferences_module.css +41 -0
- package/dist/modules/admin-account/components/update.js +50 -31
- package/dist/modules/admin-account/index.d.ts +3 -3
- package/dist/modules/admin-account/index.js +2 -2
- package/dist/modules/admin-account/schemas.d.ts +4 -0
- package/dist/modules/admin-account/schemas.js +4 -1
- package/dist/modules/admin-account/service.d.ts +1 -0
- package/dist/modules/admin-account/service.js +8 -0
- package/dist/modules/admin-permissions/components/inspector.js +31 -41
- package/dist/modules/admin-roles/components/create.js +43 -26
- package/dist/modules/admin-roles/components/permissions.js +26 -35
- package/dist/modules/admin-roles/components/update.js +26 -16
- package/dist/modules/admin-users/components/create.js +60 -40
- package/dist/modules/admin-users/components/roles.js +9 -15
- package/dist/modules/admin-users/components/set-password.js +30 -31
- package/dist/modules/admin-users/components/update.js +58 -39
- package/dist/modules/admin-users/dto.js +1 -0
- package/dist/modules/admin-users/repository.d.ts +17 -0
- package/dist/modules/admin-users/schemas.d.ts +4 -0
- package/dist/modules/admin-users/schemas.js +6 -2
- package/dist/modules/auth/components/sign-in-form.js +10 -8
- package/dist/presentation/group.d.ts +27 -0
- package/dist/presentation/group.js +14 -0
- package/dist/presentation/group.module.js +6 -0
- package/dist/presentation/group_module.css +19 -0
- package/dist/presentation/row.d.ts +25 -0
- package/dist/presentation/row.js +8 -0
- package/dist/presentation/row.module.js +5 -0
- package/dist/presentation/row_module.css +18 -0
- package/dist/presentation/tabs.d.ts +25 -0
- package/dist/presentation/tabs.js +39 -0
- package/dist/presentation/tabs.module.js +10 -0
- package/dist/presentation/tabs_module.css +68 -0
- package/dist/react.d.ts +66 -0
- package/dist/react.js +36 -0
- package/dist/services/admin-services-types.d.ts +16 -0
- package/dist/widgets/diff-viewer/diff-modal.d.ts +22 -0
- package/dist/widgets/diff-viewer/diff-modal.js +149 -0
- package/dist/widgets/diff-viewer/diff-modal.module.js +14 -0
- package/dist/widgets/diff-viewer/diff-modal_module.css +56 -0
- package/dist/widgets/status-badge/status-badge.d.ts +25 -0
- package/dist/widgets/status-badge/status-badge.js +37 -0
- package/dist/widgets/status-badge/status-badge.module.js +7 -0
- package/dist/widgets/status-badge/status-badge_module.css +20 -0
- package/package.json +14 -4
- package/src/fields/array/array-field.module.css +48 -0
- package/src/fields/array/array-field.tsx +267 -0
- package/src/fields/blocks/blocks-field.module.css +148 -0
- package/src/fields/blocks/blocks-field.tsx +323 -0
- package/src/fields/checkbox/checkbox-field.module.css +4 -0
- package/src/fields/checkbox/checkbox-field.tsx +54 -0
- package/src/fields/column-formatter.tsx +31 -0
- package/src/fields/date-time-formatter.tsx +22 -0
- package/src/fields/datetime/datetime-field.module.css +13 -0
- package/src/fields/datetime/datetime-field.tsx +54 -0
- package/src/fields/draggable-context-menu.module.css +127 -0
- package/src/fields/draggable-context-menu.tsx +87 -0
- package/src/fields/field-helpers.ts +69 -0
- package/src/fields/field-renderer.module.css +22 -0
- package/src/fields/field-renderer.tsx +288 -0
- package/src/fields/field-services-context.tsx +35 -0
- package/src/fields/field-services-types.ts +68 -0
- package/src/fields/file/file-field.module.css +153 -0
- package/src/fields/file/file-field.tsx +286 -0
- package/src/fields/file/file-upload-field.module.css +101 -0
- package/src/fields/file/file-upload-field.tsx +187 -0
- package/src/fields/group/group-field.module.css +43 -0
- package/src/fields/group/group-field.tsx +84 -0
- package/src/fields/image/image-field.module.css +155 -0
- package/src/fields/image/image-field.tsx +306 -0
- package/src/fields/image/image-upload-field.module.css +123 -0
- package/src/fields/image/image-upload-field.tsx +276 -0
- package/src/fields/local-date-time.tsx +88 -0
- package/src/fields/locale-badge.module.css +37 -0
- package/src/fields/locale-badge.tsx +32 -0
- package/src/fields/numerical/numerical-field.tsx +114 -0
- package/src/fields/relation/relation-display.module.css +36 -0
- package/src/fields/relation/relation-display.tsx +130 -0
- package/src/fields/relation/relation-field.module.css +83 -0
- package/src/fields/relation/relation-field.tsx +211 -0
- package/src/fields/relation/relation-picker.module.css +168 -0
- package/src/fields/relation/relation-picker.tsx +326 -0
- package/src/fields/relation/relation-summary.module.css +55 -0
- package/src/fields/relation/relation-summary.tsx +123 -0
- package/src/fields/select/select-field.module.css +13 -0
- package/src/fields/select/select-field.tsx +61 -0
- package/src/fields/sortable-item.module.css +167 -0
- package/src/fields/sortable-item.tsx +106 -0
- package/src/fields/text/text-field.module.css +13 -0
- package/src/fields/text/text-field.tsx +146 -0
- package/src/fields/text-area/text-area-field.module.css +13 -0
- package/src/fields/text-area/text-area-field.tsx +147 -0
- package/src/fields/use-field-change-handler.ts +112 -0
- package/src/forms/document-actions.module.css +160 -0
- package/src/forms/document-actions.tsx +482 -0
- package/src/forms/form-context.tsx +704 -0
- package/src/forms/form-renderer.module.css +321 -0
- package/src/forms/form-renderer.tsx +891 -0
- package/src/forms/navigation-guard.tsx +98 -0
- package/src/forms/path-widget.module.css +41 -0
- package/src/forms/path-widget.test.tsx +217 -0
- package/src/forms/path-widget.tsx +183 -0
- package/src/forms/upload-executor.ts +192 -0
- package/src/lib/translate-validation-error.ts +56 -0
- package/src/modules/admin-account/commands.ts +13 -0
- package/src/modules/admin-account/components/change-password.tsx +46 -31
- package/src/modules/admin-account/components/container.tsx +83 -38
- package/src/modules/admin-account/components/preferences.module.css +60 -0
- package/src/modules/admin-account/components/preferences.tsx +203 -0
- package/src/modules/admin-account/components/update.tsx +53 -27
- package/src/modules/admin-account/index.ts +3 -0
- package/src/modules/admin-account/schemas.ts +13 -0
- package/src/modules/admin-account/service.ts +12 -0
- package/src/modules/admin-permissions/components/inspector.tsx +22 -14
- package/src/modules/admin-roles/components/create.tsx +51 -23
- package/src/modules/admin-roles/components/permissions.tsx +25 -21
- package/src/modules/admin-roles/components/update.tsx +37 -19
- package/src/modules/admin-users/components/create.tsx +63 -34
- package/src/modules/admin-users/components/roles.tsx +9 -8
- package/src/modules/admin-users/components/set-password.tsx +34 -28
- package/src/modules/admin-users/components/update.tsx +58 -36
- package/src/modules/admin-users/dto.ts +1 -0
- package/src/modules/admin-users/repository.ts +17 -0
- package/src/modules/admin-users/schemas.ts +12 -0
- package/src/modules/auth/components/sign-in-form.tsx +14 -8
- package/src/presentation/group.module.css +41 -0
- package/src/presentation/group.tsx +40 -0
- package/src/presentation/row.module.css +32 -0
- package/src/presentation/row.tsx +33 -0
- package/src/presentation/tabs.module.css +107 -0
- package/src/presentation/tabs.tsx +84 -0
- package/src/react.ts +84 -0
- package/src/services/admin-services-types.ts +18 -0
- package/src/widgets/diff-viewer/diff-modal.module.css +79 -0
- package/src/widgets/diff-viewer/diff-modal.tsx +186 -0
- package/src/widgets/status-badge/status-badge.module.css +31 -0
- package/src/widgets/status-badge/status-badge.tsx +71 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState } from 'react'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type ImageField as FieldType,
|
|
13
|
+
isPendingStoredFileValue,
|
|
14
|
+
type StoredFileValue,
|
|
15
|
+
} from '@byline/core'
|
|
16
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
17
|
+
import {
|
|
18
|
+
CloseIcon,
|
|
19
|
+
ErrorText,
|
|
20
|
+
HelpText,
|
|
21
|
+
IconButton,
|
|
22
|
+
ImageLightbox,
|
|
23
|
+
Label,
|
|
24
|
+
LoaderRing,
|
|
25
|
+
} from '@byline/ui/react'
|
|
26
|
+
import cx from 'classnames'
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
useFieldError,
|
|
30
|
+
useFieldValue,
|
|
31
|
+
useFormContext,
|
|
32
|
+
useIsDirty,
|
|
33
|
+
useIsFieldUploading,
|
|
34
|
+
} from '../../forms/form-context'
|
|
35
|
+
import { useFieldChangeHandler } from '../use-field-change-handler'
|
|
36
|
+
import styles from './image-field.module.css'
|
|
37
|
+
import { ImageUploadField } from './image-upload-field'
|
|
38
|
+
|
|
39
|
+
interface ImageFieldProps {
|
|
40
|
+
field: FieldType
|
|
41
|
+
/** Collection path required to call the /upload endpoint. */
|
|
42
|
+
collectionPath?: string
|
|
43
|
+
// Stored value is currently a plain object with file/image metadata
|
|
44
|
+
// coming from the seed data / storage layer.
|
|
45
|
+
value?: StoredFileValue | null
|
|
46
|
+
defaultValue?: StoredFileValue | null
|
|
47
|
+
onChange?: (value: StoredFileValue | null) => void
|
|
48
|
+
path?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const ImageField = ({
|
|
52
|
+
field,
|
|
53
|
+
collectionPath,
|
|
54
|
+
value,
|
|
55
|
+
defaultValue,
|
|
56
|
+
onChange: _onChange,
|
|
57
|
+
path,
|
|
58
|
+
}: ImageFieldProps) => {
|
|
59
|
+
const fieldPath = path ?? field.name
|
|
60
|
+
const fieldError = useFieldError(fieldPath)
|
|
61
|
+
const isDirty = useIsDirty(fieldPath)
|
|
62
|
+
const fieldValue = useFieldValue<StoredFileValue | null | undefined>(fieldPath)
|
|
63
|
+
const isUploading = useIsFieldUploading(fieldPath)
|
|
64
|
+
const { removePendingUpload } = useFormContext()
|
|
65
|
+
const { t } = useTranslation('byline-admin')
|
|
66
|
+
|
|
67
|
+
// Re-use the standard field change handler so patches are emitted correctly.
|
|
68
|
+
const handleChange = useFieldChangeHandler(field, fieldPath)
|
|
69
|
+
|
|
70
|
+
// When the field has been explicitly set (dirty), use the field value from
|
|
71
|
+
// form state — even if it's null (user clicked Remove). Only fall back to
|
|
72
|
+
// the prop / defaultValue when the field hasn't been touched yet.
|
|
73
|
+
const incomingValue = isDirty
|
|
74
|
+
? (fieldValue ?? null)
|
|
75
|
+
: (value ?? fieldValue ?? defaultValue ?? null)
|
|
76
|
+
|
|
77
|
+
// Check if this is a pending upload (selected but not yet uploaded)
|
|
78
|
+
const isPending = isPendingStoredFileValue(incomingValue)
|
|
79
|
+
|
|
80
|
+
// Old placeholder check for backwards compatibility
|
|
81
|
+
const isOldPlaceholder = (v: unknown): boolean => {
|
|
82
|
+
if (!v || typeof v !== 'object') return false
|
|
83
|
+
const maybe = v as Partial<StoredFileValue>
|
|
84
|
+
return maybe.storageProvider === 'placeholder' && maybe.storagePath === 'pending'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Show upload widget only if no value or old placeholder
|
|
88
|
+
const showUploadWidget = incomingValue == null || isOldPlaceholder(incomingValue)
|
|
89
|
+
|
|
90
|
+
// Prefer the generated thumbnail variant for the preview tile. SVGs and
|
|
91
|
+
// other bypass types have no variants — fall back to the original.
|
|
92
|
+
const thumbVariant =
|
|
93
|
+
incomingValue && !isPendingStoredFileValue(incomingValue)
|
|
94
|
+
? incomingValue.variants?.find((v) => v.name === 'thumbnail')
|
|
95
|
+
: undefined
|
|
96
|
+
const previewUrl = thumbVariant?.storageUrl ?? incomingValue?.storageUrl
|
|
97
|
+
|
|
98
|
+
// Handle remove, including cleanup of pending uploads
|
|
99
|
+
const handleRemove = () => {
|
|
100
|
+
if (isPending) {
|
|
101
|
+
removePendingUpload(fieldPath)
|
|
102
|
+
}
|
|
103
|
+
handleChange(null)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Lightbox state — only enabled for stored (non-pending) images that have a
|
|
107
|
+
// resolvable original storageUrl.
|
|
108
|
+
const [lightboxOpen, setLightboxOpen] = useState(false)
|
|
109
|
+
const canOpenLightbox = !isPending && !!incomingValue?.storageUrl
|
|
110
|
+
|
|
111
|
+
const htmlId = fieldPath
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className={`byline-field-image ${field.name}`}>
|
|
115
|
+
<div className={cx('byline-field-image-header', styles.header)}>
|
|
116
|
+
<Label
|
|
117
|
+
id={htmlId}
|
|
118
|
+
htmlFor={htmlId}
|
|
119
|
+
label={field.label ?? field.name}
|
|
120
|
+
required={!field.optional}
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{showUploadWidget ? (
|
|
125
|
+
collectionPath ? (
|
|
126
|
+
<ImageUploadField
|
|
127
|
+
field={field}
|
|
128
|
+
collectionPath={collectionPath}
|
|
129
|
+
fieldPath={fieldPath}
|
|
130
|
+
onUploaded={(uploaded) => {
|
|
131
|
+
handleChange(uploaded)
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
) : (
|
|
135
|
+
<div className={cx('byline-field-image-empty', styles.empty)}>
|
|
136
|
+
{t('fields.image.empty')}
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
) : (
|
|
140
|
+
<div className={cx('byline-field-image-tile', styles.tile)}>
|
|
141
|
+
{isUploading && (
|
|
142
|
+
<div
|
|
143
|
+
className={cx('byline-field-image-uploading', styles.uploading)}
|
|
144
|
+
aria-live="polite"
|
|
145
|
+
aria-busy="true"
|
|
146
|
+
>
|
|
147
|
+
<LoaderRing />
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
{/* Remove button — shown when an image is set (including pending) */}
|
|
151
|
+
{collectionPath && (
|
|
152
|
+
<div className={cx('byline-field-image-remove', styles.remove)}>
|
|
153
|
+
<IconButton
|
|
154
|
+
type="button"
|
|
155
|
+
intent="noeffect"
|
|
156
|
+
onClick={handleRemove}
|
|
157
|
+
size="xs"
|
|
158
|
+
disabled={isUploading}
|
|
159
|
+
aria-label={t('fields.image.removeAriaLabel')}
|
|
160
|
+
>
|
|
161
|
+
<CloseIcon width="15px" height="15px" />
|
|
162
|
+
</IconButton>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
{/* Preview */}
|
|
166
|
+
{previewUrl && (
|
|
167
|
+
<div className={cx('byline-field-image-preview-wrap', styles['preview-wrap'])}>
|
|
168
|
+
{canOpenLightbox ? (
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
onClick={() => setLightboxOpen(true)}
|
|
172
|
+
aria-label={t('fields.image.openLightboxAriaLabel')}
|
|
173
|
+
className={cx('byline-field-image-preview-button', styles['preview-button'])}
|
|
174
|
+
>
|
|
175
|
+
<img
|
|
176
|
+
src={previewUrl}
|
|
177
|
+
alt={incomingValue.originalFilename ?? incomingValue.filename}
|
|
178
|
+
className={cx(
|
|
179
|
+
'byline-field-image-preview',
|
|
180
|
+
styles.preview,
|
|
181
|
+
incomingValue.mimeType === 'image/svg+xml' && [
|
|
182
|
+
'byline-field-image-preview-svg',
|
|
183
|
+
styles['preview-svg'],
|
|
184
|
+
]
|
|
185
|
+
)}
|
|
186
|
+
/>
|
|
187
|
+
</button>
|
|
188
|
+
) : (
|
|
189
|
+
<img
|
|
190
|
+
src={previewUrl}
|
|
191
|
+
alt={incomingValue.originalFilename ?? incomingValue.filename}
|
|
192
|
+
className={cx(
|
|
193
|
+
'byline-field-image-preview',
|
|
194
|
+
styles.preview,
|
|
195
|
+
incomingValue.mimeType === 'image/svg+xml' && [
|
|
196
|
+
'byline-field-image-preview-svg',
|
|
197
|
+
styles['preview-svg'],
|
|
198
|
+
]
|
|
199
|
+
)}
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
{/* Pending upload badge */}
|
|
203
|
+
{isPending && (
|
|
204
|
+
<div className={cx('byline-field-image-pending', styles.pending)}>
|
|
205
|
+
{t('fields.fileMeta.pendingUpload')}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
{/* Metadata */}
|
|
211
|
+
<div className={cx('byline-field-image-meta', styles.meta)}>
|
|
212
|
+
<div>
|
|
213
|
+
<span className={cx('byline-field-image-meta-key', styles['meta-key'])}>
|
|
214
|
+
{t('fields.fileMeta.filename')}
|
|
215
|
+
</span>{' '}
|
|
216
|
+
{incomingValue?.filename}
|
|
217
|
+
</div>
|
|
218
|
+
<div>
|
|
219
|
+
<span className={cx('byline-field-image-meta-key', styles['meta-key'])}>
|
|
220
|
+
{t('fields.fileMeta.original')}
|
|
221
|
+
</span>{' '}
|
|
222
|
+
{incomingValue?.originalFilename}
|
|
223
|
+
</div>
|
|
224
|
+
<div>
|
|
225
|
+
<span className={cx('byline-field-image-meta-key', styles['meta-key'])}>
|
|
226
|
+
{t('fields.fileMeta.type')}
|
|
227
|
+
</span>{' '}
|
|
228
|
+
{incomingValue?.mimeType}
|
|
229
|
+
</div>
|
|
230
|
+
<div>
|
|
231
|
+
<span className={cx('byline-field-image-meta-key', styles['meta-key'])}>
|
|
232
|
+
{t('fields.fileMeta.size')}
|
|
233
|
+
</span>{' '}
|
|
234
|
+
{incomingValue?.fileSize}
|
|
235
|
+
</div>
|
|
236
|
+
{isPending ? (
|
|
237
|
+
<div>
|
|
238
|
+
<span className={cx('byline-field-image-meta-key', styles['meta-key'])}>
|
|
239
|
+
{t('fields.fileMeta.status')}
|
|
240
|
+
</span>{' '}
|
|
241
|
+
<span className={cx('byline-field-image-meta-pending', styles['meta-pending'])}>
|
|
242
|
+
{t('fields.fileMeta.willUploadOnSave')}
|
|
243
|
+
</span>
|
|
244
|
+
</div>
|
|
245
|
+
) : (
|
|
246
|
+
<>
|
|
247
|
+
<div>
|
|
248
|
+
<span className={cx('byline-field-image-meta-key', styles['meta-key'])}>
|
|
249
|
+
{t('fields.fileMeta.storage')}
|
|
250
|
+
</span>{' '}
|
|
251
|
+
{incomingValue?.storageProvider}
|
|
252
|
+
</div>
|
|
253
|
+
{incomingValue?.imageWidth != null && (
|
|
254
|
+
<div>
|
|
255
|
+
<span className={cx('byline-field-image-meta-key', styles['meta-key'])}>
|
|
256
|
+
{t('fields.imageMeta.dimensions')}
|
|
257
|
+
</span>{' '}
|
|
258
|
+
{incomingValue.imageWidth}
|
|
259
|
+
{incomingValue.imageHeight != null ? `×${incomingValue.imageHeight}` : ''}
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
{incomingValue?.imageFormat != null && (
|
|
263
|
+
<div>
|
|
264
|
+
<span className={cx('byline-field-image-meta-key', styles['meta-key'])}>
|
|
265
|
+
{t('fields.imageMeta.format')}
|
|
266
|
+
</span>{' '}
|
|
267
|
+
{incomingValue.imageFormat}
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
<div>
|
|
271
|
+
<span className={cx('byline-field-image-meta-key', styles['meta-key'])}>
|
|
272
|
+
{t('fields.imageMeta.thumbnail')}
|
|
273
|
+
</span>{' '}
|
|
274
|
+
{thumbVariant
|
|
275
|
+
? t('fields.imageMeta.thumbnailGenerated')
|
|
276
|
+
: t('fields.imageMeta.thumbnailPending')}
|
|
277
|
+
</div>
|
|
278
|
+
</>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{field.helpText && <HelpText text={field.helpText} />}
|
|
285
|
+
|
|
286
|
+
{fieldError && <ErrorText id={`${field.name}-error`} text={fieldError} />}
|
|
287
|
+
|
|
288
|
+
{canOpenLightbox && incomingValue?.storageUrl && (
|
|
289
|
+
<ImageLightbox
|
|
290
|
+
isOpen={lightboxOpen}
|
|
291
|
+
onDismiss={() => setLightboxOpen(false)}
|
|
292
|
+
src={incomingValue.storageUrl}
|
|
293
|
+
alt={incomingValue.originalFilename ?? incomingValue.filename}
|
|
294
|
+
downloadFilename={incomingValue.originalFilename ?? incomingValue.filename}
|
|
295
|
+
title={incomingValue.originalFilename ?? incomingValue.filename}
|
|
296
|
+
meta={{
|
|
297
|
+
width: incomingValue.imageWidth,
|
|
298
|
+
height: incomingValue.imageHeight,
|
|
299
|
+
fileSize: incomingValue.fileSize,
|
|
300
|
+
mimeType: incomingValue.mimeType,
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
)
|
|
306
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageUploadField — drag-and-drop image picker that registers a deferred
|
|
3
|
+
* upload in form context.
|
|
4
|
+
*
|
|
5
|
+
* Override handles:
|
|
6
|
+
* .byline-field-image-upload — root wrapper
|
|
7
|
+
* .byline-field-image-upload-input — visually-hidden file input
|
|
8
|
+
* .byline-field-image-upload-zone — clickable / drop target
|
|
9
|
+
* .byline-field-image-upload-zone-active — drag-hovered state
|
|
10
|
+
* .byline-field-image-upload-zone-busy — processing state
|
|
11
|
+
* .byline-field-image-upload-spinner — animated spinner svg
|
|
12
|
+
* .byline-field-image-upload-icon — upload icon svg
|
|
13
|
+
* .byline-field-image-upload-label — primary text inside the zone
|
|
14
|
+
* .byline-field-image-upload-action — "browse" link inside the label
|
|
15
|
+
* .byline-field-image-upload-hint — secondary "JPEG, PNG …" hint
|
|
16
|
+
* .byline-field-image-upload-error — error message paragraph
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
.root,
|
|
20
|
+
:global(.byline-field-image-upload) {
|
|
21
|
+
margin-top: 0.25rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.input,
|
|
25
|
+
:global(.byline-field-image-upload-input) {
|
|
26
|
+
position: absolute;
|
|
27
|
+
width: 1px;
|
|
28
|
+
height: 1px;
|
|
29
|
+
padding: 0;
|
|
30
|
+
margin: -1px;
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
clip: rect(0, 0, 0, 0);
|
|
33
|
+
white-space: nowrap;
|
|
34
|
+
border: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.zone,
|
|
38
|
+
:global(.byline-field-image-upload-zone) {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
gap: var(--spacing-8);
|
|
44
|
+
padding: 1.5rem 1rem;
|
|
45
|
+
border: 2px dashed var(--gray-600);
|
|
46
|
+
border-radius: var(--border-radius-lg);
|
|
47
|
+
color: var(--gray-400);
|
|
48
|
+
text-align: center;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
user-select: none;
|
|
51
|
+
transition:
|
|
52
|
+
color 150ms ease,
|
|
53
|
+
background-color 150ms ease,
|
|
54
|
+
border-color 150ms ease;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.zone:hover,
|
|
58
|
+
:global(.byline-field-image-upload-zone):hover {
|
|
59
|
+
border-color: var(--primary-500);
|
|
60
|
+
background-color: oklch(from var(--primary-900) l c h / 0.1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.zone-active,
|
|
64
|
+
:global(.byline-field-image-upload-zone-active) {
|
|
65
|
+
border-color: var(--primary-400);
|
|
66
|
+
background-color: oklch(from var(--primary-900) l c h / 0.2);
|
|
67
|
+
color: var(--primary-300);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.zone-busy,
|
|
71
|
+
:global(.byline-field-image-upload-zone-busy) {
|
|
72
|
+
border-color: var(--gray-700);
|
|
73
|
+
background-color: oklch(from var(--canvas-800) l c h / 0.5);
|
|
74
|
+
color: var(--gray-600);
|
|
75
|
+
cursor: not-allowed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.spinner,
|
|
79
|
+
:global(.byline-field-image-upload-spinner) {
|
|
80
|
+
width: 1.5rem;
|
|
81
|
+
height: 1.5rem;
|
|
82
|
+
color: var(--primary-400);
|
|
83
|
+
animation: byline-image-upload-spin 1s linear infinite;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@keyframes byline-image-upload-spin {
|
|
87
|
+
to {
|
|
88
|
+
transform: rotate(360deg);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.icon,
|
|
93
|
+
:global(.byline-field-image-upload-icon) {
|
|
94
|
+
width: 1.75rem;
|
|
95
|
+
height: 1.75rem;
|
|
96
|
+
opacity: 0.6;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.label,
|
|
100
|
+
:global(.byline-field-image-upload-label) {
|
|
101
|
+
font-size: var(--font-size-xs);
|
|
102
|
+
font-weight: var(--font-weight-medium);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.action,
|
|
106
|
+
:global(.byline-field-image-upload-action) {
|
|
107
|
+
color: var(--primary-400);
|
|
108
|
+
text-decoration: underline;
|
|
109
|
+
text-underline-offset: 2px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.hint,
|
|
113
|
+
:global(.byline-field-image-upload-hint) {
|
|
114
|
+
color: var(--gray-500);
|
|
115
|
+
font-size: 0.65rem;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.error,
|
|
119
|
+
:global(.byline-field-image-upload-error) {
|
|
120
|
+
margin-top: 0.375rem;
|
|
121
|
+
color: var(--red-400);
|
|
122
|
+
font-size: var(--font-size-xs);
|
|
123
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ImageUploadField
|
|
11
|
+
*
|
|
12
|
+
* A drag-and-drop / click-to-browse file picker that prepares an image for
|
|
13
|
+
* upload. The actual upload is deferred until form submission — this component
|
|
14
|
+
* stores the file in the form context's pending uploads and emits a placeholder
|
|
15
|
+
* StoredFileValue with a blob URL for immediate preview.
|
|
16
|
+
*
|
|
17
|
+
* Prototype: no chunk upload, no resumable uploads, single file only.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { ChangeEvent, DragEvent } from 'react'
|
|
21
|
+
import { useCallback, useRef, useState } from 'react'
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
createPendingStoredFileValue,
|
|
25
|
+
type ImageField as FieldType,
|
|
26
|
+
type PendingStoredFileValue,
|
|
27
|
+
type StoredFileValue,
|
|
28
|
+
} from '@byline/core'
|
|
29
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
30
|
+
import cx from 'classnames'
|
|
31
|
+
|
|
32
|
+
import { useFormContext } from '../../forms/form-context'
|
|
33
|
+
import styles from './image-upload-field.module.css'
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Types
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
interface ImageUploadFieldProps {
|
|
40
|
+
field: FieldType
|
|
41
|
+
/** Collection path used to build the upload URL (e.g. `'media'`). */
|
|
42
|
+
collectionPath: string
|
|
43
|
+
/** Field path in the form (e.g. `'image'` or `'content.0.image'`). */
|
|
44
|
+
fieldPath: string
|
|
45
|
+
/** Called with the PendingStoredFileValue for immediate preview. */
|
|
46
|
+
onUploaded: (value: StoredFileValue | PendingStoredFileValue) => void
|
|
47
|
+
/** Optional accepted-file MIME types string for the native file input. */
|
|
48
|
+
accept?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type SelectionStatus = 'idle' | 'processing' | 'error'
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Component
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export const ImageUploadField = ({
|
|
58
|
+
field: _field,
|
|
59
|
+
collectionPath,
|
|
60
|
+
fieldPath,
|
|
61
|
+
onUploaded,
|
|
62
|
+
accept = 'image/*',
|
|
63
|
+
}: ImageUploadFieldProps) => {
|
|
64
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
65
|
+
const [status, setStatus] = useState<SelectionStatus>('idle')
|
|
66
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
67
|
+
const [isDragOver, setIsDragOver] = useState(false)
|
|
68
|
+
const { addPendingUpload } = useFormContext()
|
|
69
|
+
const { t } = useTranslation('byline-admin')
|
|
70
|
+
|
|
71
|
+
// -------------------------------------------------------------------------
|
|
72
|
+
// Core file selection logic (deferred upload)
|
|
73
|
+
// -------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
const handleFileSelected = useCallback(
|
|
76
|
+
(file: File) => {
|
|
77
|
+
setStatus('processing')
|
|
78
|
+
setErrorMessage(null)
|
|
79
|
+
|
|
80
|
+
// Basic client-side validation
|
|
81
|
+
if (!file.type.startsWith('image/')) {
|
|
82
|
+
setStatus('error')
|
|
83
|
+
setErrorMessage(t('fields.image.upload.errors.notAnImage'))
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create a blob URL for immediate preview
|
|
88
|
+
const previewUrl = URL.createObjectURL(file)
|
|
89
|
+
|
|
90
|
+
// Extract image dimensions for the pending value
|
|
91
|
+
const img = new Image()
|
|
92
|
+
img.onload = () => {
|
|
93
|
+
// SVGs without explicit width/height attrs (viewBox-only) report naturalWidth/Height = 0.
|
|
94
|
+
// Skip dimensions when zero so they are stored as null (scalable, no fixed size).
|
|
95
|
+
const w = img.naturalWidth
|
|
96
|
+
const h = img.naturalHeight
|
|
97
|
+
const dimensions = w > 0 && h > 0 ? { width: w, height: h } : undefined
|
|
98
|
+
|
|
99
|
+
// Create the pending stored file value
|
|
100
|
+
const pendingValue = createPendingStoredFileValue(file, previewUrl, dimensions)
|
|
101
|
+
|
|
102
|
+
// Register the pending upload in form context
|
|
103
|
+
addPendingUpload(fieldPath, {
|
|
104
|
+
file,
|
|
105
|
+
previewUrl,
|
|
106
|
+
collectionPath,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
setStatus('idle')
|
|
110
|
+
onUploaded(pendingValue)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
img.onerror = () => {
|
|
114
|
+
URL.revokeObjectURL(previewUrl)
|
|
115
|
+
setStatus('error')
|
|
116
|
+
setErrorMessage(t('fields.image.upload.errors.cannotRead'))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
img.src = previewUrl
|
|
120
|
+
},
|
|
121
|
+
[collectionPath, fieldPath, addPendingUpload, onUploaded, t]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
// -------------------------------------------------------------------------
|
|
125
|
+
// File input
|
|
126
|
+
// -------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
const handleFileChange = useCallback(
|
|
129
|
+
(e: ChangeEvent<HTMLInputElement>) => {
|
|
130
|
+
const file = e.target.files?.[0]
|
|
131
|
+
if (file) handleFileSelected(file)
|
|
132
|
+
// Reset so re-selecting the same file fires the event again.
|
|
133
|
+
e.target.value = ''
|
|
134
|
+
},
|
|
135
|
+
[handleFileSelected]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const handleBrowseClick = useCallback(() => {
|
|
139
|
+
inputRef.current?.click()
|
|
140
|
+
}, [])
|
|
141
|
+
|
|
142
|
+
// -------------------------------------------------------------------------
|
|
143
|
+
// Drag and drop
|
|
144
|
+
// -------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
const handleDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {
|
|
147
|
+
e.preventDefault()
|
|
148
|
+
setIsDragOver(true)
|
|
149
|
+
}, [])
|
|
150
|
+
|
|
151
|
+
const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
|
|
152
|
+
e.preventDefault()
|
|
153
|
+
setIsDragOver(false)
|
|
154
|
+
}, [])
|
|
155
|
+
|
|
156
|
+
const handleDrop = useCallback(
|
|
157
|
+
(e: DragEvent<HTMLDivElement>) => {
|
|
158
|
+
e.preventDefault()
|
|
159
|
+
setIsDragOver(false)
|
|
160
|
+
const file = e.dataTransfer.files?.[0]
|
|
161
|
+
if (file) handleFileSelected(file)
|
|
162
|
+
},
|
|
163
|
+
[handleFileSelected]
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
// -------------------------------------------------------------------------
|
|
167
|
+
// Render
|
|
168
|
+
// -------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
const isProcessing = status === 'processing'
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className={cx('byline-field-image-upload', styles.root)}>
|
|
174
|
+
{/* Hidden native file input */}
|
|
175
|
+
<input
|
|
176
|
+
ref={inputRef}
|
|
177
|
+
type="file"
|
|
178
|
+
accept={accept}
|
|
179
|
+
className={cx('byline-field-image-upload-input', styles.input)}
|
|
180
|
+
onChange={handleFileChange}
|
|
181
|
+
disabled={isProcessing}
|
|
182
|
+
aria-hidden="true"
|
|
183
|
+
tabIndex={-1}
|
|
184
|
+
/>
|
|
185
|
+
|
|
186
|
+
{/* Drop zone */}
|
|
187
|
+
<div
|
|
188
|
+
role="button"
|
|
189
|
+
tabIndex={0}
|
|
190
|
+
aria-label={t('fields.image.upload.zoneAriaLabel')}
|
|
191
|
+
onDragOver={handleDragOver}
|
|
192
|
+
onDragLeave={handleDragLeave}
|
|
193
|
+
onDrop={handleDrop}
|
|
194
|
+
onClick={handleBrowseClick}
|
|
195
|
+
onKeyDown={(e) => {
|
|
196
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
197
|
+
e.preventDefault()
|
|
198
|
+
handleBrowseClick()
|
|
199
|
+
}
|
|
200
|
+
}}
|
|
201
|
+
className={cx(
|
|
202
|
+
'byline-field-image-upload-zone',
|
|
203
|
+
styles.zone,
|
|
204
|
+
isDragOver &&
|
|
205
|
+
!isProcessing && ['byline-field-image-upload-zone-active', styles['zone-active']],
|
|
206
|
+
isProcessing && ['byline-field-image-upload-zone-busy', styles['zone-busy']]
|
|
207
|
+
)}
|
|
208
|
+
>
|
|
209
|
+
{isProcessing ? (
|
|
210
|
+
<>
|
|
211
|
+
{/* Spinner */}
|
|
212
|
+
<svg
|
|
213
|
+
className={cx('byline-field-image-upload-spinner', styles.spinner)}
|
|
214
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
215
|
+
fill="none"
|
|
216
|
+
viewBox="0 0 24 24"
|
|
217
|
+
aria-hidden="true"
|
|
218
|
+
>
|
|
219
|
+
<circle
|
|
220
|
+
style={{ opacity: 0.25 }}
|
|
221
|
+
cx="12"
|
|
222
|
+
cy="12"
|
|
223
|
+
r="10"
|
|
224
|
+
stroke="currentColor"
|
|
225
|
+
strokeWidth="4"
|
|
226
|
+
/>
|
|
227
|
+
<path
|
|
228
|
+
style={{ opacity: 0.75 }}
|
|
229
|
+
fill="currentColor"
|
|
230
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
231
|
+
/>
|
|
232
|
+
</svg>
|
|
233
|
+
<span className={cx('byline-field-image-upload-label', styles.label)}>
|
|
234
|
+
{t('fields.image.upload.processing')}
|
|
235
|
+
</span>
|
|
236
|
+
</>
|
|
237
|
+
) : (
|
|
238
|
+
<>
|
|
239
|
+
{/* Upload icon */}
|
|
240
|
+
<svg
|
|
241
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
242
|
+
className={cx('byline-field-image-upload-icon', styles.icon)}
|
|
243
|
+
fill="none"
|
|
244
|
+
viewBox="0 0 24 24"
|
|
245
|
+
stroke="currentColor"
|
|
246
|
+
strokeWidth={1.5}
|
|
247
|
+
aria-hidden="true"
|
|
248
|
+
>
|
|
249
|
+
<path
|
|
250
|
+
strokeLinecap="round"
|
|
251
|
+
strokeLinejoin="round"
|
|
252
|
+
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
|
253
|
+
/>
|
|
254
|
+
</svg>
|
|
255
|
+
<span className={cx('byline-field-image-upload-label', styles.label)}>
|
|
256
|
+
{t('fields.image.upload.label')}{' '}
|
|
257
|
+
<span className={cx('byline-field-image-upload-action', styles.action)}>
|
|
258
|
+
{t('fields.image.upload.browse')}
|
|
259
|
+
</span>
|
|
260
|
+
</span>
|
|
261
|
+
<span className={cx('byline-field-image-upload-hint', styles.hint)}>
|
|
262
|
+
{t('fields.image.upload.hint')}
|
|
263
|
+
</span>
|
|
264
|
+
</>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{/* Error message */}
|
|
269
|
+
{status === 'error' && errorMessage && (
|
|
270
|
+
<p className={cx('byline-field-image-upload-error', styles.error)} role="alert">
|
|
271
|
+
{errorMessage}
|
|
272
|
+
</p>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
)
|
|
276
|
+
}
|