@byline/ui 2.5.2 → 2.6.1
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/components/shimmer/shimmer.d.ts +13 -1
- package/dist/components/shimmer/shimmer.js +29 -20
- package/dist/components/shimmer/shimmer_module.css +4 -4
- package/dist/dnd/draggable-sortable/demo/draggable-list-demo.js +1 -1
- package/dist/react.d.ts +18 -54
- package/dist/react.js +0 -35
- package/dist/styles/styles.css +3 -0
- package/dist/uikit.d.ts +1 -0
- package/dist/uikit.js +1 -0
- package/package.json +2 -8
- package/src/components/shimmer/shimmer.module.css +8 -4
- package/src/components/shimmer/shimmer.tsx +34 -9
- package/src/dnd/draggable-sortable/demo/draggable-list-demo.tsx +1 -1
- package/src/react.ts +20 -68
- package/src/styles/functional/surfaces.css +13 -1
- package/src/uikit.ts +1 -0
- package/dist/admin/group.d.ts +0 -27
- package/dist/admin/group.js +0 -14
- package/dist/admin/group.module.js +0 -6
- package/dist/admin/group_module.css +0 -19
- package/dist/admin/row.d.ts +0 -25
- package/dist/admin/row.js +0 -8
- package/dist/admin/row.module.js +0 -5
- package/dist/admin/row_module.css +0 -18
- package/dist/admin/tabs.d.ts +0 -25
- package/dist/admin/tabs.js +0 -35
- package/dist/admin/tabs.module.js +0 -10
- package/dist/admin/tabs_module.css +0 -68
- package/dist/fields/array/array-field.d.ts +0 -14
- package/dist/fields/array/array-field.js +0 -176
- package/dist/fields/array/array-field.module.js +0 -11
- package/dist/fields/array/array-field_module.css +0 -32
- package/dist/fields/blocks/blocks-field.d.ts +0 -13
- package/dist/fields/blocks/blocks-field.js +0 -244
- package/dist/fields/blocks/blocks-field.module.js +0 -26
- package/dist/fields/blocks/blocks-field_module.css +0 -107
- package/dist/fields/checkbox/checkbox-field.d.ts +0 -16
- package/dist/fields/checkbox/checkbox-field.js +0 -28
- package/dist/fields/checkbox/checkbox-field.module.js +0 -6
- package/dist/fields/checkbox/checkbox-field_module.css +0 -4
- package/dist/fields/column-formatter.d.ts +0 -20
- package/dist/fields/column-formatter.js +0 -15
- package/dist/fields/date-time-formatter.d.ts +0 -16
- package/dist/fields/date-time-formatter.js +0 -8
- package/dist/fields/datetime/datetime-field.d.ts +0 -16
- package/dist/fields/datetime/datetime-field.js +0 -37
- package/dist/fields/datetime/datetime-field.module.js +0 -5
- package/dist/fields/datetime/datetime-field_module.css +0 -4
- package/dist/fields/draggable-context-menu.d.ts +0 -6
- package/dist/fields/draggable-context-menu.js +0 -83
- package/dist/fields/draggable-context-menu.module.js +0 -15
- package/dist/fields/draggable-context-menu_module.css +0 -91
- package/dist/fields/field-helpers.d.ts +0 -26
- package/dist/fields/field-helpers.js +0 -50
- package/dist/fields/field-renderer.d.ts +0 -37
- package/dist/fields/field-renderer.js +0 -206
- package/dist/fields/field-renderer.module.js +0 -8
- package/dist/fields/field-renderer_module.css +0 -11
- package/dist/fields/file/file-field.d.ts +0 -19
- package/dist/fields/file/file-field.js +0 -226
- package/dist/fields/file/file-field.module.js +0 -18
- package/dist/fields/file/file-field_module.css +0 -131
- package/dist/fields/file/file-upload-field.d.ts +0 -21
- package/dist/fields/file/file-upload-field.js +0 -128
- package/dist/fields/file/file-upload-field.module.js +0 -15
- package/dist/fields/file/file-upload-field_module.css +0 -74
- package/dist/fields/group/group-field.d.ts +0 -15
- package/dist/fields/group/group-field.js +0 -59
- package/dist/fields/group/group-field.module.js +0 -9
- package/dist/fields/group/group-field_module.css +0 -27
- package/dist/fields/image/image-field.d.ts +0 -19
- package/dist/fields/image/image-field.js +0 -242
- package/dist/fields/image/image-field.module.js +0 -22
- package/dist/fields/image/image-field_module.css +0 -121
- package/dist/fields/image/image-upload-field.d.ts +0 -21
- package/dist/fields/image/image-upload-field.js +0 -187
- package/dist/fields/image/image-upload-field.module.js +0 -19
- package/dist/fields/image/image-upload-field_module.css +0 -92
- package/dist/fields/local-date-time.d.ts +0 -27
- package/dist/fields/local-date-time.js +0 -49
- package/dist/fields/locale-badge.d.ts +0 -18
- package/dist/fields/locale-badge.js +0 -10
- package/dist/fields/locale-badge.module.js +0 -5
- package/dist/fields/locale-badge_module.css +0 -27
- package/dist/fields/numerical/numerical-field.d.ts +0 -18
- package/dist/fields/numerical/numerical-field.js +0 -74
- package/dist/fields/relation/relation-display.d.ts +0 -40
- package/dist/fields/relation/relation-display.js +0 -58
- package/dist/fields/relation/relation-display.module.js +0 -9
- package/dist/fields/relation/relation-display_module.css +0 -21
- package/dist/fields/relation/relation-field.d.ts +0 -18
- package/dist/fields/relation/relation-field.js +0 -146
- package/dist/fields/relation/relation-field.module.js +0 -13
- package/dist/fields/relation/relation-field_module.css +0 -62
- package/dist/fields/relation/relation-picker.d.ts +0 -49
- package/dist/fields/relation/relation-picker.js +0 -233
- package/dist/fields/relation/relation-picker.module.js +0 -26
- package/dist/fields/relation/relation-picker_module.css +0 -124
- package/dist/fields/relation/relation-summary.d.ts +0 -31
- package/dist/fields/relation/relation-summary.js +0 -50
- package/dist/fields/relation/relation-summary.module.js +0 -11
- package/dist/fields/relation/relation-summary_module.css +0 -37
- package/dist/fields/select/select-field.d.ts +0 -16
- package/dist/fields/select/select-field.js +0 -50
- package/dist/fields/select/select-field.module.js +0 -5
- package/dist/fields/select/select-field_module.css +0 -4
- package/dist/fields/sortable-item.d.ts +0 -15
- package/dist/fields/sortable-item.js +0 -80
- package/dist/fields/sortable-item.module.js +0 -22
- package/dist/fields/sortable-item_module.css +0 -124
- package/dist/fields/text/text-field.d.ts +0 -20
- package/dist/fields/text/text-field.js +0 -104
- package/dist/fields/text/text-field.module.js +0 -6
- package/dist/fields/text/text-field_module.css +0 -5
- package/dist/fields/text-area/text-area-field.d.ts +0 -20
- package/dist/fields/text-area/text-area-field.js +0 -105
- package/dist/fields/text-area/text-area-field.module.js +0 -6
- package/dist/fields/text-area/text-area-field_module.css +0 -5
- package/dist/fields/use-field-change-handler.d.ts +0 -23
- package/dist/fields/use-field-change-handler.js +0 -52
- package/dist/forms/document-actions.d.ts +0 -48
- package/dist/forms/document-actions.js +0 -469
- package/dist/forms/document-actions.module.js +0 -34
- package/dist/forms/document-actions_module.css +0 -118
- package/dist/forms/form-context.d.ts +0 -89
- package/dist/forms/form-context.js +0 -466
- package/dist/forms/form-renderer.d.ts +0 -98
- package/dist/forms/form-renderer.js +0 -591
- package/dist/forms/form-renderer.module.js +0 -46
- package/dist/forms/form-renderer_module.css +0 -245
- package/dist/forms/navigation-guard.d.ts +0 -54
- package/dist/forms/navigation-guard.js +0 -22
- package/dist/forms/path-widget.d.ts +0 -36
- package/dist/forms/path-widget.js +0 -107
- package/dist/forms/path-widget.module.js +0 -8
- package/dist/forms/path-widget_module.css +0 -29
- package/dist/forms/upload-executor.d.ts +0 -57
- package/dist/forms/upload-executor.js +0 -92
- package/dist/services/field-services-context.d.ts +0 -16
- package/dist/services/field-services-context.js +0 -13
- package/dist/services/field-services-types.d.ts +0 -63
- package/dist/services/field-services-types.js +0 -1
- package/dist/widgets/diff-viewer/diff-modal.d.ts +0 -22
- package/dist/widgets/diff-viewer/diff-modal.js +0 -146
- package/dist/widgets/diff-viewer/diff-modal.module.js +0 -14
- package/dist/widgets/diff-viewer/diff-modal_module.css +0 -56
- package/dist/widgets/status-badge/status-badge.d.ts +0 -25
- package/dist/widgets/status-badge/status-badge.js +0 -35
- package/dist/widgets/status-badge/status-badge.module.js +0 -7
- package/dist/widgets/status-badge/status-badge_module.css +0 -20
- package/src/admin/group.module.css +0 -41
- package/src/admin/group.tsx +0 -40
- package/src/admin/row.module.css +0 -32
- package/src/admin/row.tsx +0 -33
- package/src/admin/tabs.module.css +0 -107
- package/src/admin/tabs.tsx +0 -82
- package/src/fields/array/array-field.module.css +0 -48
- package/src/fields/array/array-field.tsx +0 -266
- package/src/fields/blocks/blocks-field.module.css +0 -148
- package/src/fields/blocks/blocks-field.tsx +0 -312
- package/src/fields/checkbox/checkbox-field.module.css +0 -4
- package/src/fields/checkbox/checkbox-field.tsx +0 -54
- package/src/fields/column-formatter.tsx +0 -31
- package/src/fields/date-time-formatter.tsx +0 -22
- package/src/fields/datetime/datetime-field.module.css +0 -13
- package/src/fields/datetime/datetime-field.tsx +0 -54
- package/src/fields/draggable-context-menu.module.css +0 -127
- package/src/fields/draggable-context-menu.tsx +0 -85
- package/src/fields/field-helpers.ts +0 -69
- package/src/fields/field-renderer.module.css +0 -22
- package/src/fields/field-renderer.tsx +0 -288
- package/src/fields/file/file-field.module.css +0 -153
- package/src/fields/file/file-field.tsx +0 -271
- package/src/fields/file/file-upload-field.module.css +0 -101
- package/src/fields/file/file-upload-field.tsx +0 -183
- package/src/fields/group/group-field.module.css +0 -43
- package/src/fields/group/group-field.tsx +0 -84
- package/src/fields/image/image-field.module.css +0 -155
- package/src/fields/image/image-field.tsx +0 -291
- package/src/fields/image/image-upload-field.module.css +0 -123
- package/src/fields/image/image-upload-field.tsx +0 -270
- package/src/fields/local-date-time.tsx +0 -88
- package/src/fields/locale-badge.module.css +0 -37
- package/src/fields/locale-badge.tsx +0 -32
- package/src/fields/numerical/numerical-field.tsx +0 -114
- package/src/fields/relation/relation-display.module.css +0 -36
- package/src/fields/relation/relation-display.tsx +0 -130
- package/src/fields/relation/relation-field.module.css +0 -83
- package/src/fields/relation/relation-field.tsx +0 -206
- package/src/fields/relation/relation-picker.module.css +0 -168
- package/src/fields/relation/relation-picker.tsx +0 -325
- package/src/fields/relation/relation-summary.module.css +0 -55
- package/src/fields/relation/relation-summary.tsx +0 -123
- package/src/fields/select/select-field.module.css +0 -13
- package/src/fields/select/select-field.tsx +0 -61
- package/src/fields/sortable-item.module.css +0 -167
- package/src/fields/sortable-item.tsx +0 -101
- package/src/fields/text/text-field.module.css +0 -13
- package/src/fields/text/text-field.tsx +0 -146
- package/src/fields/text-area/text-area-field.module.css +0 -13
- package/src/fields/text-area/text-area-field.tsx +0 -147
- package/src/fields/use-field-change-handler.ts +0 -112
- package/src/forms/document-actions.module.css +0 -160
- package/src/forms/document-actions.tsx +0 -487
- package/src/forms/form-context.tsx +0 -704
- package/src/forms/form-renderer.module.css +0 -321
- package/src/forms/form-renderer.tsx +0 -888
- package/src/forms/navigation-guard.tsx +0 -98
- package/src/forms/path-widget.module.css +0 -41
- package/src/forms/path-widget.test.tsx +0 -217
- package/src/forms/path-widget.tsx +0 -181
- package/src/forms/upload-executor.ts +0 -190
- package/src/services/field-services-context.tsx +0 -35
- package/src/services/field-services-types.ts +0 -68
- package/src/widgets/diff-viewer/diff-modal.module.css +0 -79
- package/src/widgets/diff-viewer/diff-modal.tsx +0 -184
- package/src/widgets/status-badge/status-badge.module.css +0 -31
- package/src/widgets/status-badge/status-badge.tsx +0 -69
|
@@ -1,270 +0,0 @@
|
|
|
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 cx from 'classnames'
|
|
30
|
-
|
|
31
|
-
import { useFormContext } from '../../forms/form-context'
|
|
32
|
-
import styles from './image-upload-field.module.css'
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// Types
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
interface ImageUploadFieldProps {
|
|
39
|
-
field: FieldType
|
|
40
|
-
/** Collection path used to build the upload URL (e.g. `'media'`). */
|
|
41
|
-
collectionPath: string
|
|
42
|
-
/** Field path in the form (e.g. `'image'` or `'content.0.image'`). */
|
|
43
|
-
fieldPath: string
|
|
44
|
-
/** Called with the PendingStoredFileValue for immediate preview. */
|
|
45
|
-
onUploaded: (value: StoredFileValue | PendingStoredFileValue) => void
|
|
46
|
-
/** Optional accepted-file MIME types string for the native file input. */
|
|
47
|
-
accept?: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
type SelectionStatus = 'idle' | 'processing' | 'error'
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Component
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
export const ImageUploadField = ({
|
|
57
|
-
field: _field,
|
|
58
|
-
collectionPath,
|
|
59
|
-
fieldPath,
|
|
60
|
-
onUploaded,
|
|
61
|
-
accept = 'image/*',
|
|
62
|
-
}: ImageUploadFieldProps) => {
|
|
63
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
64
|
-
const [status, setStatus] = useState<SelectionStatus>('idle')
|
|
65
|
-
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
66
|
-
const [isDragOver, setIsDragOver] = useState(false)
|
|
67
|
-
const { addPendingUpload } = useFormContext()
|
|
68
|
-
|
|
69
|
-
// -------------------------------------------------------------------------
|
|
70
|
-
// Core file selection logic (deferred upload)
|
|
71
|
-
// -------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
const handleFileSelected = useCallback(
|
|
74
|
-
(file: File) => {
|
|
75
|
-
setStatus('processing')
|
|
76
|
-
setErrorMessage(null)
|
|
77
|
-
|
|
78
|
-
// Basic client-side validation
|
|
79
|
-
if (!file.type.startsWith('image/')) {
|
|
80
|
-
setStatus('error')
|
|
81
|
-
setErrorMessage('Please select an image file.')
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Create a blob URL for immediate preview
|
|
86
|
-
const previewUrl = URL.createObjectURL(file)
|
|
87
|
-
|
|
88
|
-
// Extract image dimensions for the pending value
|
|
89
|
-
const img = new Image()
|
|
90
|
-
img.onload = () => {
|
|
91
|
-
// SVGs without explicit width/height attrs (viewBox-only) report naturalWidth/Height = 0.
|
|
92
|
-
// Skip dimensions when zero so they are stored as null (scalable, no fixed size).
|
|
93
|
-
const w = img.naturalWidth
|
|
94
|
-
const h = img.naturalHeight
|
|
95
|
-
const dimensions = w > 0 && h > 0 ? { width: w, height: h } : undefined
|
|
96
|
-
|
|
97
|
-
// Create the pending stored file value
|
|
98
|
-
const pendingValue = createPendingStoredFileValue(file, previewUrl, dimensions)
|
|
99
|
-
|
|
100
|
-
// Register the pending upload in form context
|
|
101
|
-
addPendingUpload(fieldPath, {
|
|
102
|
-
file,
|
|
103
|
-
previewUrl,
|
|
104
|
-
collectionPath,
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
setStatus('idle')
|
|
108
|
-
onUploaded(pendingValue)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
img.onerror = () => {
|
|
112
|
-
URL.revokeObjectURL(previewUrl)
|
|
113
|
-
setStatus('error')
|
|
114
|
-
setErrorMessage('Could not read image. Please try a different file.')
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
img.src = previewUrl
|
|
118
|
-
},
|
|
119
|
-
[collectionPath, fieldPath, addPendingUpload, onUploaded]
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
// -------------------------------------------------------------------------
|
|
123
|
-
// File input
|
|
124
|
-
// -------------------------------------------------------------------------
|
|
125
|
-
|
|
126
|
-
const handleFileChange = useCallback(
|
|
127
|
-
(e: ChangeEvent<HTMLInputElement>) => {
|
|
128
|
-
const file = e.target.files?.[0]
|
|
129
|
-
if (file) handleFileSelected(file)
|
|
130
|
-
// Reset so re-selecting the same file fires the event again.
|
|
131
|
-
e.target.value = ''
|
|
132
|
-
},
|
|
133
|
-
[handleFileSelected]
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
const handleBrowseClick = useCallback(() => {
|
|
137
|
-
inputRef.current?.click()
|
|
138
|
-
}, [])
|
|
139
|
-
|
|
140
|
-
// -------------------------------------------------------------------------
|
|
141
|
-
// Drag and drop
|
|
142
|
-
// -------------------------------------------------------------------------
|
|
143
|
-
|
|
144
|
-
const handleDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {
|
|
145
|
-
e.preventDefault()
|
|
146
|
-
setIsDragOver(true)
|
|
147
|
-
}, [])
|
|
148
|
-
|
|
149
|
-
const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
|
|
150
|
-
e.preventDefault()
|
|
151
|
-
setIsDragOver(false)
|
|
152
|
-
}, [])
|
|
153
|
-
|
|
154
|
-
const handleDrop = useCallback(
|
|
155
|
-
(e: DragEvent<HTMLDivElement>) => {
|
|
156
|
-
e.preventDefault()
|
|
157
|
-
setIsDragOver(false)
|
|
158
|
-
const file = e.dataTransfer.files?.[0]
|
|
159
|
-
if (file) handleFileSelected(file)
|
|
160
|
-
},
|
|
161
|
-
[handleFileSelected]
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
// -------------------------------------------------------------------------
|
|
165
|
-
// Render
|
|
166
|
-
// -------------------------------------------------------------------------
|
|
167
|
-
|
|
168
|
-
const isProcessing = status === 'processing'
|
|
169
|
-
|
|
170
|
-
return (
|
|
171
|
-
<div className={cx('byline-field-image-upload', styles.root)}>
|
|
172
|
-
{/* Hidden native file input */}
|
|
173
|
-
<input
|
|
174
|
-
ref={inputRef}
|
|
175
|
-
type="file"
|
|
176
|
-
accept={accept}
|
|
177
|
-
className={cx('byline-field-image-upload-input', styles.input)}
|
|
178
|
-
onChange={handleFileChange}
|
|
179
|
-
disabled={isProcessing}
|
|
180
|
-
aria-hidden="true"
|
|
181
|
-
tabIndex={-1}
|
|
182
|
-
/>
|
|
183
|
-
|
|
184
|
-
{/* Drop zone */}
|
|
185
|
-
<div
|
|
186
|
-
role="button"
|
|
187
|
-
tabIndex={0}
|
|
188
|
-
aria-label="Upload image — drag and drop or click to browse"
|
|
189
|
-
onDragOver={handleDragOver}
|
|
190
|
-
onDragLeave={handleDragLeave}
|
|
191
|
-
onDrop={handleDrop}
|
|
192
|
-
onClick={handleBrowseClick}
|
|
193
|
-
onKeyDown={(e) => {
|
|
194
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
195
|
-
e.preventDefault()
|
|
196
|
-
handleBrowseClick()
|
|
197
|
-
}
|
|
198
|
-
}}
|
|
199
|
-
className={cx(
|
|
200
|
-
'byline-field-image-upload-zone',
|
|
201
|
-
styles.zone,
|
|
202
|
-
isDragOver &&
|
|
203
|
-
!isProcessing && ['byline-field-image-upload-zone-active', styles['zone-active']],
|
|
204
|
-
isProcessing && ['byline-field-image-upload-zone-busy', styles['zone-busy']]
|
|
205
|
-
)}
|
|
206
|
-
>
|
|
207
|
-
{isProcessing ? (
|
|
208
|
-
<>
|
|
209
|
-
{/* Spinner */}
|
|
210
|
-
<svg
|
|
211
|
-
className={cx('byline-field-image-upload-spinner', styles.spinner)}
|
|
212
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
213
|
-
fill="none"
|
|
214
|
-
viewBox="0 0 24 24"
|
|
215
|
-
aria-hidden="true"
|
|
216
|
-
>
|
|
217
|
-
<circle
|
|
218
|
-
style={{ opacity: 0.25 }}
|
|
219
|
-
cx="12"
|
|
220
|
-
cy="12"
|
|
221
|
-
r="10"
|
|
222
|
-
stroke="currentColor"
|
|
223
|
-
strokeWidth="4"
|
|
224
|
-
/>
|
|
225
|
-
<path
|
|
226
|
-
style={{ opacity: 0.75 }}
|
|
227
|
-
fill="currentColor"
|
|
228
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
229
|
-
/>
|
|
230
|
-
</svg>
|
|
231
|
-
<span className={cx('byline-field-image-upload-label', styles.label)}>Processing…</span>
|
|
232
|
-
</>
|
|
233
|
-
) : (
|
|
234
|
-
<>
|
|
235
|
-
{/* Upload icon */}
|
|
236
|
-
<svg
|
|
237
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
238
|
-
className={cx('byline-field-image-upload-icon', styles.icon)}
|
|
239
|
-
fill="none"
|
|
240
|
-
viewBox="0 0 24 24"
|
|
241
|
-
stroke="currentColor"
|
|
242
|
-
strokeWidth={1.5}
|
|
243
|
-
aria-hidden="true"
|
|
244
|
-
>
|
|
245
|
-
<path
|
|
246
|
-
strokeLinecap="round"
|
|
247
|
-
strokeLinejoin="round"
|
|
248
|
-
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"
|
|
249
|
-
/>
|
|
250
|
-
</svg>
|
|
251
|
-
<span className={cx('byline-field-image-upload-label', styles.label)}>
|
|
252
|
-
Drop image here or{' '}
|
|
253
|
-
<span className={cx('byline-field-image-upload-action', styles.action)}>browse</span>
|
|
254
|
-
</span>
|
|
255
|
-
<span className={cx('byline-field-image-upload-hint', styles.hint)}>
|
|
256
|
-
JPEG, PNG, WebP, GIF, SVG, AVIF
|
|
257
|
-
</span>
|
|
258
|
-
</>
|
|
259
|
-
)}
|
|
260
|
-
</div>
|
|
261
|
-
|
|
262
|
-
{/* Error message */}
|
|
263
|
-
{status === 'error' && errorMessage && (
|
|
264
|
-
<p className={cx('byline-field-image-upload-error', styles.error)} role="alert">
|
|
265
|
-
{errorMessage}
|
|
266
|
-
</p>
|
|
267
|
-
)}
|
|
268
|
-
</div>
|
|
269
|
-
)
|
|
270
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react'
|
|
4
|
-
|
|
5
|
-
// undefined will use the user's locale
|
|
6
|
-
const dateTimeFormatter = new Intl.DateTimeFormat(undefined, {
|
|
7
|
-
year: 'numeric',
|
|
8
|
-
month: 'short', // <- short month text (locale-aware)
|
|
9
|
-
day: '2-digit',
|
|
10
|
-
hour: 'numeric',
|
|
11
|
-
minute: 'numeric',
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
const dateFormatter = new Intl.DateTimeFormat(undefined, {
|
|
15
|
-
year: 'numeric',
|
|
16
|
-
month: 'short', // <- short month text (locale-aware)
|
|
17
|
-
day: '2-digit',
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
interface LocalDateTimeProps {
|
|
21
|
-
value: string | Date | null | undefined
|
|
22
|
-
mode?: 'datetime' | 'date'
|
|
23
|
-
className?: string
|
|
24
|
-
fallback?: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Client-side date/time formatter that displays dates in the user's local timezone.
|
|
29
|
-
*
|
|
30
|
-
* This component avoids hydration mismatches by:
|
|
31
|
-
* 1. Rendering a placeholder on the server/initial render
|
|
32
|
-
* 2. Updating to the correctly formatted date after hydration
|
|
33
|
-
*
|
|
34
|
-
* Use this instead of formatDateTime when you need to display dates
|
|
35
|
-
* in the user's local timezone rather than UTC.
|
|
36
|
-
*/
|
|
37
|
-
export function LocalDateTime({
|
|
38
|
-
value,
|
|
39
|
-
mode = 'datetime',
|
|
40
|
-
className,
|
|
41
|
-
fallback = '—',
|
|
42
|
-
}: LocalDateTimeProps): React.JSX.Element {
|
|
43
|
-
const [formattedDate, setFormattedDate] = useState<string | null>(null)
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
if (value == null) {
|
|
47
|
-
setFormattedDate(null)
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const date = typeof value === 'string' ? Date.parse(value) : value.getTime()
|
|
52
|
-
if (Number.isNaN(date)) {
|
|
53
|
-
setFormattedDate('Error')
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const formatter = mode === 'datetime' ? dateTimeFormatter : dateFormatter
|
|
58
|
-
setFormattedDate(formatter.format(date))
|
|
59
|
-
}, [value, mode])
|
|
60
|
-
|
|
61
|
-
// Show fallback during SSR and before useEffect runs
|
|
62
|
-
if (formattedDate === null) {
|
|
63
|
-
return <span className={className}>{value ? '...' : fallback}</span>
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return <span className={className}>{formattedDate}</span>
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Formats a date string to the user's local timezone.
|
|
71
|
-
*
|
|
72
|
-
* WARNING: Only use this in client components where hydration
|
|
73
|
-
* mismatches are acceptable or managed. For most cases, prefer
|
|
74
|
-
* the <LocalDateTime /> component.
|
|
75
|
-
*/
|
|
76
|
-
export function formatLocalDateTime(value: string | Date | null | undefined): string {
|
|
77
|
-
if (value == null) return '—'
|
|
78
|
-
const date = typeof value === 'string' ? Date.parse(value) : value.getTime()
|
|
79
|
-
if (Number.isNaN(date)) return 'Error'
|
|
80
|
-
return dateTimeFormatter.format(date)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function formatLocalDate(value: string | Date | null | undefined): string {
|
|
84
|
-
if (value == null) return '—'
|
|
85
|
-
const date = typeof value === 'string' ? Date.parse(value) : value.getTime()
|
|
86
|
-
if (Number.isNaN(date)) return 'Error'
|
|
87
|
-
return dateFormatter.format(date)
|
|
88
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LocaleBadge — small inline badge shown next to a localised field label.
|
|
3
|
-
*
|
|
4
|
-
* Override handle: `.byline-locale-badge`
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
.badge,
|
|
8
|
-
:global(.byline-locale-badge) {
|
|
9
|
-
display: inline-flex;
|
|
10
|
-
align-items: center;
|
|
11
|
-
gap: 0.125rem;
|
|
12
|
-
margin-left: 0.375rem;
|
|
13
|
-
padding: 0.125rem 0.375rem;
|
|
14
|
-
border: var(--border-width-thin) var(--border-style-solid) var(--yellow-400);
|
|
15
|
-
border-radius: var(--border-radius-sm);
|
|
16
|
-
background-color: var(--yellow-100);
|
|
17
|
-
color: var(--yellow-800);
|
|
18
|
-
font-size: 0.6rem;
|
|
19
|
-
font-weight: var(--font-weight-semibold);
|
|
20
|
-
line-height: 1;
|
|
21
|
-
letter-spacing: var(--letter-spacing-wide);
|
|
22
|
-
text-transform: uppercase;
|
|
23
|
-
pointer-events: none;
|
|
24
|
-
user-select: none;
|
|
25
|
-
vertical-align: middle;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/* ─── Dark theme variants ───────────────────────────────────── */
|
|
29
|
-
|
|
30
|
-
:is([data-theme="dark"], :global(.dark)) {
|
|
31
|
-
.badge,
|
|
32
|
-
:global(.byline-locale-badge) {
|
|
33
|
-
border-color: var(--yellow-700);
|
|
34
|
-
background-color: oklch(from var(--yellow-900) l c h / 0.4);
|
|
35
|
-
color: var(--yellow-400);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
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 cx from 'classnames'
|
|
10
|
-
|
|
11
|
-
import styles from './locale-badge.module.css'
|
|
12
|
-
|
|
13
|
-
export interface LocaleBadgeProps {
|
|
14
|
-
locale: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Small inline badge shown next to the label of a field that has
|
|
19
|
-
* `localized: true` in its schema definition. Indicates which locale
|
|
20
|
-
* the editor is currently working in.
|
|
21
|
-
*
|
|
22
|
-
* Stable override handle: `.byline-locale-badge`.
|
|
23
|
-
*/
|
|
24
|
-
export const LocaleBadge = ({ locale }: LocaleBadgeProps) => (
|
|
25
|
-
<span
|
|
26
|
-
aria-hidden="true"
|
|
27
|
-
title={`Localised — editing ${locale.toUpperCase()} content`}
|
|
28
|
-
className={cx('byline-locale-badge', styles.badge)}
|
|
29
|
-
>
|
|
30
|
-
{locale}
|
|
31
|
-
</span>
|
|
32
|
-
)
|
|
@@ -1,114 +0,0 @@
|
|
|
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 type {
|
|
10
|
-
CounterField,
|
|
11
|
-
DecimalField,
|
|
12
|
-
Field,
|
|
13
|
-
FieldComponentSlots,
|
|
14
|
-
FloatField,
|
|
15
|
-
IntegerField,
|
|
16
|
-
} from '@byline/core'
|
|
17
|
-
|
|
18
|
-
import { useFieldError, useFieldValue } from '../../forms/form-context'
|
|
19
|
-
import { Input } from '../../uikit.js'
|
|
20
|
-
|
|
21
|
-
export const NumericalField = ({
|
|
22
|
-
field,
|
|
23
|
-
value,
|
|
24
|
-
defaultValue,
|
|
25
|
-
onChange,
|
|
26
|
-
id,
|
|
27
|
-
path,
|
|
28
|
-
components,
|
|
29
|
-
}: {
|
|
30
|
-
field: IntegerField | FloatField | DecimalField | CounterField
|
|
31
|
-
value?: string | number | null
|
|
32
|
-
defaultValue?: string | number | null
|
|
33
|
-
onChange?: (value: string) => void
|
|
34
|
-
id?: string
|
|
35
|
-
path?: string
|
|
36
|
-
/** Optional UI component slot overrides from the admin config. */
|
|
37
|
-
components?: FieldComponentSlots
|
|
38
|
-
}) => {
|
|
39
|
-
const fieldPath = path ?? field.name
|
|
40
|
-
const fieldError = useFieldError(fieldPath)
|
|
41
|
-
const fieldValue = useFieldValue<string | number | undefined>(fieldPath)
|
|
42
|
-
const incomingValue = value ?? fieldValue ?? defaultValue ?? ''
|
|
43
|
-
const htmlId = id ?? fieldPath
|
|
44
|
-
const displayValue =
|
|
45
|
-
incomingValue === undefined || incomingValue === null ? '' : String(incomingValue)
|
|
46
|
-
|
|
47
|
-
// Custom component slots (from admin config)
|
|
48
|
-
const slots = components
|
|
49
|
-
const CustomLabel = slots?.Label
|
|
50
|
-
const CustomHelpText = slots?.HelpText
|
|
51
|
-
const CustomField = slots?.Field
|
|
52
|
-
const BeforeField = slots?.beforeField
|
|
53
|
-
const AfterField = slots?.afterField
|
|
54
|
-
|
|
55
|
-
// Shared props available to every slot component
|
|
56
|
-
const slotBaseProps = {
|
|
57
|
-
field: field as Field,
|
|
58
|
-
path: fieldPath,
|
|
59
|
-
value: incomingValue,
|
|
60
|
-
error: fieldError,
|
|
61
|
-
id: htmlId,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const hasCustomLabel = !!CustomLabel
|
|
65
|
-
const suppressInputLabel = hasCustomLabel
|
|
66
|
-
const suppressInputHelpText = !!CustomHelpText
|
|
67
|
-
|
|
68
|
-
// ── Label rendering ──────────────────────────────────────────
|
|
69
|
-
const renderLabel = () => {
|
|
70
|
-
if (hasCustomLabel) {
|
|
71
|
-
return <CustomLabel {...slotBaseProps} label={field.label} required={!field.optional} />
|
|
72
|
-
}
|
|
73
|
-
return null
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ── Field input rendering ────────────────────────────────────
|
|
77
|
-
const renderInput = () => {
|
|
78
|
-
if (CustomField) {
|
|
79
|
-
return (
|
|
80
|
-
<CustomField
|
|
81
|
-
{...slotBaseProps}
|
|
82
|
-
onChange={(v: any) => onChange?.(v)}
|
|
83
|
-
defaultValue={defaultValue}
|
|
84
|
-
placeholder={field.placeholder}
|
|
85
|
-
/>
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
return (
|
|
89
|
-
<Input
|
|
90
|
-
type="number"
|
|
91
|
-
id={htmlId}
|
|
92
|
-
name={field.name}
|
|
93
|
-
label={suppressInputLabel ? undefined : field.label}
|
|
94
|
-
required={!field.optional}
|
|
95
|
-
readOnly={field.readOnly}
|
|
96
|
-
helpText={suppressInputHelpText ? undefined : field.helpText}
|
|
97
|
-
value={displayValue}
|
|
98
|
-
onChange={(e) => onChange?.(e.target.value)}
|
|
99
|
-
error={fieldError != null}
|
|
100
|
-
errorText={fieldError}
|
|
101
|
-
/>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<div className={`byline-field-${field.type} ${field.name}`}>
|
|
107
|
-
{renderLabel()}
|
|
108
|
-
{BeforeField && <BeforeField {...slotBaseProps} />}
|
|
109
|
-
{renderInput()}
|
|
110
|
-
{AfterField && <AfterField {...slotBaseProps} />}
|
|
111
|
-
{CustomHelpText && <CustomHelpText {...slotBaseProps} helpText={field.helpText} />}
|
|
112
|
-
</div>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RelationDisplay — shared cell renderer for relation picker rows and the
|
|
3
|
-
* relation-summary selected-value tile.
|
|
4
|
-
*
|
|
5
|
-
* Override handles:
|
|
6
|
-
* .byline-relation-cell — single column cell
|
|
7
|
-
* .byline-relation-cell-center — cell with align: center
|
|
8
|
-
* .byline-relation-cell-right — cell with align: right
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
.cell,
|
|
12
|
-
:global(.byline-relation-cell) {
|
|
13
|
-
min-width: 0;
|
|
14
|
-
color: var(--gray-700);
|
|
15
|
-
font-size: var(--font-size-sm);
|
|
16
|
-
overflow: hidden;
|
|
17
|
-
text-overflow: ellipsis;
|
|
18
|
-
white-space: nowrap;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.cell-center,
|
|
22
|
-
:global(.byline-relation-cell-center) {
|
|
23
|
-
text-align: center;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.cell-right,
|
|
27
|
-
:global(.byline-relation-cell-right) {
|
|
28
|
-
text-align: right;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
:is([data-theme="dark"], :global(.dark)) {
|
|
32
|
-
.cell,
|
|
33
|
-
:global(.byline-relation-cell) {
|
|
34
|
-
color: var(--gray-100);
|
|
35
|
-
}
|
|
36
|
-
}
|