@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.
Files changed (260) hide show
  1. package/dist/fields/array/array-field.d.ts +14 -0
  2. package/dist/fields/array/array-field.js +177 -0
  3. package/dist/fields/array/array-field.module.js +11 -0
  4. package/dist/fields/array/array-field_module.css +32 -0
  5. package/dist/fields/blocks/blocks-field.d.ts +13 -0
  6. package/dist/fields/blocks/blocks-field.js +245 -0
  7. package/dist/fields/blocks/blocks-field.module.js +26 -0
  8. package/dist/fields/blocks/blocks-field_module.css +107 -0
  9. package/dist/fields/checkbox/checkbox-field.d.ts +16 -0
  10. package/dist/fields/checkbox/checkbox-field.js +28 -0
  11. package/dist/fields/checkbox/checkbox-field.module.js +6 -0
  12. package/dist/fields/checkbox/checkbox-field_module.css +4 -0
  13. package/dist/fields/column-formatter.d.ts +20 -0
  14. package/dist/fields/column-formatter.js +15 -0
  15. package/dist/fields/date-time-formatter.d.ts +16 -0
  16. package/dist/fields/date-time-formatter.js +8 -0
  17. package/dist/fields/datetime/datetime-field.d.ts +16 -0
  18. package/dist/fields/datetime/datetime-field.js +37 -0
  19. package/dist/fields/datetime/datetime-field.module.js +5 -0
  20. package/dist/fields/datetime/datetime-field_module.css +4 -0
  21. package/dist/fields/draggable-context-menu.d.ts +6 -0
  22. package/dist/fields/draggable-context-menu.js +85 -0
  23. package/dist/fields/draggable-context-menu.module.js +15 -0
  24. package/dist/fields/draggable-context-menu_module.css +91 -0
  25. package/dist/fields/field-helpers.d.ts +26 -0
  26. package/dist/fields/field-helpers.js +50 -0
  27. package/dist/fields/field-renderer.d.ts +37 -0
  28. package/dist/fields/field-renderer.js +206 -0
  29. package/dist/fields/field-renderer.module.js +8 -0
  30. package/dist/fields/field-renderer_module.css +11 -0
  31. package/dist/fields/field-services-context.d.ts +16 -0
  32. package/dist/fields/field-services-context.js +13 -0
  33. package/dist/fields/field-services-types.d.ts +63 -0
  34. package/dist/fields/field-services-types.js +1 -0
  35. package/dist/fields/file/file-field.d.ts +19 -0
  36. package/dist/fields/file/file-field.js +225 -0
  37. package/dist/fields/file/file-field.module.js +18 -0
  38. package/dist/fields/file/file-field_module.css +131 -0
  39. package/dist/fields/file/file-upload-field.d.ts +21 -0
  40. package/dist/fields/file/file-upload-field.js +130 -0
  41. package/dist/fields/file/file-upload-field.module.js +15 -0
  42. package/dist/fields/file/file-upload-field_module.css +74 -0
  43. package/dist/fields/group/group-field.d.ts +15 -0
  44. package/dist/fields/group/group-field.js +59 -0
  45. package/dist/fields/group/group-field.module.js +9 -0
  46. package/dist/fields/group/group-field_module.css +27 -0
  47. package/dist/fields/image/image-field.d.ts +19 -0
  48. package/dist/fields/image/image-field.js +241 -0
  49. package/dist/fields/image/image-field.module.js +22 -0
  50. package/dist/fields/image/image-field_module.css +121 -0
  51. package/dist/fields/image/image-upload-field.d.ts +21 -0
  52. package/dist/fields/image/image-upload-field.js +190 -0
  53. package/dist/fields/image/image-upload-field.module.js +19 -0
  54. package/dist/fields/image/image-upload-field_module.css +92 -0
  55. package/dist/fields/local-date-time.d.ts +27 -0
  56. package/dist/fields/local-date-time.js +49 -0
  57. package/dist/fields/locale-badge.d.ts +18 -0
  58. package/dist/fields/locale-badge.js +10 -0
  59. package/dist/fields/locale-badge.module.js +5 -0
  60. package/dist/fields/locale-badge_module.css +27 -0
  61. package/dist/fields/numerical/numerical-field.d.ts +18 -0
  62. package/dist/fields/numerical/numerical-field.js +74 -0
  63. package/dist/fields/relation/relation-display.d.ts +40 -0
  64. package/dist/fields/relation/relation-display.js +58 -0
  65. package/dist/fields/relation/relation-display.module.js +9 -0
  66. package/dist/fields/relation/relation-display_module.css +21 -0
  67. package/dist/fields/relation/relation-field.d.ts +18 -0
  68. package/dist/fields/relation/relation-field.js +138 -0
  69. package/dist/fields/relation/relation-field.module.js +13 -0
  70. package/dist/fields/relation/relation-field_module.css +62 -0
  71. package/dist/fields/relation/relation-picker.d.ts +49 -0
  72. package/dist/fields/relation/relation-picker.js +236 -0
  73. package/dist/fields/relation/relation-picker.module.js +26 -0
  74. package/dist/fields/relation/relation-picker_module.css +124 -0
  75. package/dist/fields/relation/relation-summary.d.ts +31 -0
  76. package/dist/fields/relation/relation-summary.js +50 -0
  77. package/dist/fields/relation/relation-summary.module.js +11 -0
  78. package/dist/fields/relation/relation-summary_module.css +37 -0
  79. package/dist/fields/select/select-field.d.ts +16 -0
  80. package/dist/fields/select/select-field.js +50 -0
  81. package/dist/fields/select/select-field.module.js +5 -0
  82. package/dist/fields/select/select-field_module.css +4 -0
  83. package/dist/fields/sortable-item.d.ts +15 -0
  84. package/dist/fields/sortable-item.js +81 -0
  85. package/dist/fields/sortable-item.module.js +22 -0
  86. package/dist/fields/sortable-item_module.css +124 -0
  87. package/dist/fields/text/text-field.d.ts +20 -0
  88. package/dist/fields/text/text-field.js +104 -0
  89. package/dist/fields/text/text-field.module.js +6 -0
  90. package/dist/fields/text/text-field_module.css +5 -0
  91. package/dist/fields/text-area/text-area-field.d.ts +20 -0
  92. package/dist/fields/text-area/text-area-field.js +105 -0
  93. package/dist/fields/text-area/text-area-field.module.js +6 -0
  94. package/dist/fields/text-area/text-area-field_module.css +5 -0
  95. package/dist/fields/use-field-change-handler.d.ts +23 -0
  96. package/dist/fields/use-field-change-handler.js +52 -0
  97. package/dist/forms/document-actions.d.ts +48 -0
  98. package/dist/forms/document-actions.js +475 -0
  99. package/dist/forms/document-actions.module.js +34 -0
  100. package/dist/forms/document-actions_module.css +118 -0
  101. package/dist/forms/form-context.d.ts +89 -0
  102. package/dist/forms/form-context.js +466 -0
  103. package/dist/forms/form-renderer.d.ts +98 -0
  104. package/dist/forms/form-renderer.js +597 -0
  105. package/dist/forms/form-renderer.module.js +46 -0
  106. package/dist/forms/form-renderer_module.css +245 -0
  107. package/dist/forms/navigation-guard.d.ts +54 -0
  108. package/dist/forms/navigation-guard.js +22 -0
  109. package/dist/forms/path-widget.d.ts +36 -0
  110. package/dist/forms/path-widget.js +116 -0
  111. package/dist/forms/path-widget.module.js +8 -0
  112. package/dist/forms/path-widget_module.css +29 -0
  113. package/dist/forms/upload-executor.d.ts +57 -0
  114. package/dist/forms/upload-executor.js +94 -0
  115. package/dist/lib/translate-validation-error.d.ts +36 -0
  116. package/dist/lib/translate-validation-error.js +11 -0
  117. package/dist/modules/admin-account/commands.d.ts +2 -1
  118. package/dist/modules/admin-account/commands.js +13 -2
  119. package/dist/modules/admin-account/components/change-password.js +45 -36
  120. package/dist/modules/admin-account/components/container.js +185 -134
  121. package/dist/modules/admin-account/components/preferences.d.ts +8 -0
  122. package/dist/modules/admin-account/components/preferences.js +152 -0
  123. package/dist/modules/admin-account/components/preferences.module.js +11 -0
  124. package/dist/modules/admin-account/components/preferences_module.css +41 -0
  125. package/dist/modules/admin-account/components/update.js +50 -31
  126. package/dist/modules/admin-account/index.d.ts +3 -3
  127. package/dist/modules/admin-account/index.js +2 -2
  128. package/dist/modules/admin-account/schemas.d.ts +4 -0
  129. package/dist/modules/admin-account/schemas.js +4 -1
  130. package/dist/modules/admin-account/service.d.ts +1 -0
  131. package/dist/modules/admin-account/service.js +8 -0
  132. package/dist/modules/admin-permissions/components/inspector.js +31 -41
  133. package/dist/modules/admin-roles/components/create.js +43 -26
  134. package/dist/modules/admin-roles/components/permissions.js +26 -35
  135. package/dist/modules/admin-roles/components/update.js +26 -16
  136. package/dist/modules/admin-users/components/create.js +60 -40
  137. package/dist/modules/admin-users/components/roles.js +9 -15
  138. package/dist/modules/admin-users/components/set-password.js +30 -31
  139. package/dist/modules/admin-users/components/update.js +58 -39
  140. package/dist/modules/admin-users/dto.js +1 -0
  141. package/dist/modules/admin-users/repository.d.ts +17 -0
  142. package/dist/modules/admin-users/schemas.d.ts +4 -0
  143. package/dist/modules/admin-users/schemas.js +6 -2
  144. package/dist/modules/auth/components/sign-in-form.js +10 -8
  145. package/dist/presentation/group.d.ts +27 -0
  146. package/dist/presentation/group.js +14 -0
  147. package/dist/presentation/group.module.js +6 -0
  148. package/dist/presentation/group_module.css +19 -0
  149. package/dist/presentation/row.d.ts +25 -0
  150. package/dist/presentation/row.js +8 -0
  151. package/dist/presentation/row.module.js +5 -0
  152. package/dist/presentation/row_module.css +18 -0
  153. package/dist/presentation/tabs.d.ts +25 -0
  154. package/dist/presentation/tabs.js +39 -0
  155. package/dist/presentation/tabs.module.js +10 -0
  156. package/dist/presentation/tabs_module.css +68 -0
  157. package/dist/react.d.ts +66 -0
  158. package/dist/react.js +36 -0
  159. package/dist/services/admin-services-types.d.ts +16 -0
  160. package/dist/widgets/diff-viewer/diff-modal.d.ts +22 -0
  161. package/dist/widgets/diff-viewer/diff-modal.js +149 -0
  162. package/dist/widgets/diff-viewer/diff-modal.module.js +14 -0
  163. package/dist/widgets/diff-viewer/diff-modal_module.css +56 -0
  164. package/dist/widgets/status-badge/status-badge.d.ts +25 -0
  165. package/dist/widgets/status-badge/status-badge.js +37 -0
  166. package/dist/widgets/status-badge/status-badge.module.js +7 -0
  167. package/dist/widgets/status-badge/status-badge_module.css +20 -0
  168. package/package.json +14 -4
  169. package/src/fields/array/array-field.module.css +48 -0
  170. package/src/fields/array/array-field.tsx +267 -0
  171. package/src/fields/blocks/blocks-field.module.css +148 -0
  172. package/src/fields/blocks/blocks-field.tsx +323 -0
  173. package/src/fields/checkbox/checkbox-field.module.css +4 -0
  174. package/src/fields/checkbox/checkbox-field.tsx +54 -0
  175. package/src/fields/column-formatter.tsx +31 -0
  176. package/src/fields/date-time-formatter.tsx +22 -0
  177. package/src/fields/datetime/datetime-field.module.css +13 -0
  178. package/src/fields/datetime/datetime-field.tsx +54 -0
  179. package/src/fields/draggable-context-menu.module.css +127 -0
  180. package/src/fields/draggable-context-menu.tsx +87 -0
  181. package/src/fields/field-helpers.ts +69 -0
  182. package/src/fields/field-renderer.module.css +22 -0
  183. package/src/fields/field-renderer.tsx +288 -0
  184. package/src/fields/field-services-context.tsx +35 -0
  185. package/src/fields/field-services-types.ts +68 -0
  186. package/src/fields/file/file-field.module.css +153 -0
  187. package/src/fields/file/file-field.tsx +286 -0
  188. package/src/fields/file/file-upload-field.module.css +101 -0
  189. package/src/fields/file/file-upload-field.tsx +187 -0
  190. package/src/fields/group/group-field.module.css +43 -0
  191. package/src/fields/group/group-field.tsx +84 -0
  192. package/src/fields/image/image-field.module.css +155 -0
  193. package/src/fields/image/image-field.tsx +306 -0
  194. package/src/fields/image/image-upload-field.module.css +123 -0
  195. package/src/fields/image/image-upload-field.tsx +276 -0
  196. package/src/fields/local-date-time.tsx +88 -0
  197. package/src/fields/locale-badge.module.css +37 -0
  198. package/src/fields/locale-badge.tsx +32 -0
  199. package/src/fields/numerical/numerical-field.tsx +114 -0
  200. package/src/fields/relation/relation-display.module.css +36 -0
  201. package/src/fields/relation/relation-display.tsx +130 -0
  202. package/src/fields/relation/relation-field.module.css +83 -0
  203. package/src/fields/relation/relation-field.tsx +211 -0
  204. package/src/fields/relation/relation-picker.module.css +168 -0
  205. package/src/fields/relation/relation-picker.tsx +326 -0
  206. package/src/fields/relation/relation-summary.module.css +55 -0
  207. package/src/fields/relation/relation-summary.tsx +123 -0
  208. package/src/fields/select/select-field.module.css +13 -0
  209. package/src/fields/select/select-field.tsx +61 -0
  210. package/src/fields/sortable-item.module.css +167 -0
  211. package/src/fields/sortable-item.tsx +106 -0
  212. package/src/fields/text/text-field.module.css +13 -0
  213. package/src/fields/text/text-field.tsx +146 -0
  214. package/src/fields/text-area/text-area-field.module.css +13 -0
  215. package/src/fields/text-area/text-area-field.tsx +147 -0
  216. package/src/fields/use-field-change-handler.ts +112 -0
  217. package/src/forms/document-actions.module.css +160 -0
  218. package/src/forms/document-actions.tsx +482 -0
  219. package/src/forms/form-context.tsx +704 -0
  220. package/src/forms/form-renderer.module.css +321 -0
  221. package/src/forms/form-renderer.tsx +891 -0
  222. package/src/forms/navigation-guard.tsx +98 -0
  223. package/src/forms/path-widget.module.css +41 -0
  224. package/src/forms/path-widget.test.tsx +217 -0
  225. package/src/forms/path-widget.tsx +183 -0
  226. package/src/forms/upload-executor.ts +192 -0
  227. package/src/lib/translate-validation-error.ts +56 -0
  228. package/src/modules/admin-account/commands.ts +13 -0
  229. package/src/modules/admin-account/components/change-password.tsx +46 -31
  230. package/src/modules/admin-account/components/container.tsx +83 -38
  231. package/src/modules/admin-account/components/preferences.module.css +60 -0
  232. package/src/modules/admin-account/components/preferences.tsx +203 -0
  233. package/src/modules/admin-account/components/update.tsx +53 -27
  234. package/src/modules/admin-account/index.ts +3 -0
  235. package/src/modules/admin-account/schemas.ts +13 -0
  236. package/src/modules/admin-account/service.ts +12 -0
  237. package/src/modules/admin-permissions/components/inspector.tsx +22 -14
  238. package/src/modules/admin-roles/components/create.tsx +51 -23
  239. package/src/modules/admin-roles/components/permissions.tsx +25 -21
  240. package/src/modules/admin-roles/components/update.tsx +37 -19
  241. package/src/modules/admin-users/components/create.tsx +63 -34
  242. package/src/modules/admin-users/components/roles.tsx +9 -8
  243. package/src/modules/admin-users/components/set-password.tsx +34 -28
  244. package/src/modules/admin-users/components/update.tsx +58 -36
  245. package/src/modules/admin-users/dto.ts +1 -0
  246. package/src/modules/admin-users/repository.ts +17 -0
  247. package/src/modules/admin-users/schemas.ts +12 -0
  248. package/src/modules/auth/components/sign-in-form.tsx +14 -8
  249. package/src/presentation/group.module.css +41 -0
  250. package/src/presentation/group.tsx +40 -0
  251. package/src/presentation/row.module.css +32 -0
  252. package/src/presentation/row.tsx +33 -0
  253. package/src/presentation/tabs.module.css +107 -0
  254. package/src/presentation/tabs.tsx +84 -0
  255. package/src/react.ts +84 -0
  256. package/src/services/admin-services-types.ts +18 -0
  257. package/src/widgets/diff-viewer/diff-modal.module.css +79 -0
  258. package/src/widgets/diff-viewer/diff-modal.tsx +186 -0
  259. package/src/widgets/status-badge/status-badge.module.css +31 -0
  260. package/src/widgets/status-badge/status-badge.tsx +71 -0
@@ -0,0 +1,187 @@
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
+ * FileUploadField
11
+ *
12
+ * Generic drag-and-drop / click-to-browse file picker that prepares a file for
13
+ * upload. Mirrors `ImageUploadField` but without image-specific validation or
14
+ * dimension extraction. The actual upload is deferred until form submission —
15
+ * this component stores the file in the form context's pending uploads and
16
+ * emits a placeholder StoredFileValue with a blob URL (used by the form
17
+ * orchestrator for cleanup; not shown to the user).
18
+ */
19
+
20
+ import type { ChangeEvent, DragEvent } from 'react'
21
+ import { useCallback, useRef, useState } from 'react'
22
+
23
+ import {
24
+ createPendingStoredFileValue,
25
+ type FileField 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 './file-upload-field.module.css'
34
+
35
+ interface FileUploadFieldProps {
36
+ field: FieldType
37
+ /** Collection path used to build the upload URL (e.g. `'media'`). */
38
+ collectionPath: string
39
+ /** Field path in the form (e.g. `'attachment'` or `'content.0.attachment'`). */
40
+ fieldPath: string
41
+ /** Called with the PendingStoredFileValue for immediate UI update. */
42
+ onUploaded: (value: StoredFileValue | PendingStoredFileValue) => void
43
+ /** Optional `accept` MIME-type / extension string for the native file input. */
44
+ accept?: string
45
+ }
46
+
47
+ type SelectionStatus = 'idle' | 'processing' | 'error'
48
+
49
+ export const FileUploadField = ({
50
+ field: _field,
51
+ collectionPath,
52
+ fieldPath,
53
+ onUploaded,
54
+ accept,
55
+ }: FileUploadFieldProps) => {
56
+ const inputRef = useRef<HTMLInputElement>(null)
57
+ const { t } = useTranslation('byline-admin')
58
+ const [status, setStatus] = useState<SelectionStatus>('idle')
59
+ const [errorMessage, setErrorMessage] = useState<string | null>(null)
60
+ const [isDragOver, setIsDragOver] = useState(false)
61
+ const { addPendingUpload } = useFormContext()
62
+
63
+ const handleFileSelected = useCallback(
64
+ (file: File) => {
65
+ setStatus('processing')
66
+ setErrorMessage(null)
67
+
68
+ // Blob URL is created so the form orchestrator can revoke it on cleanup
69
+ // alongside image-field uploads; it isn't surfaced in the UI here.
70
+ const previewUrl = URL.createObjectURL(file)
71
+
72
+ const pendingValue = createPendingStoredFileValue(file, previewUrl)
73
+
74
+ addPendingUpload(fieldPath, {
75
+ file,
76
+ previewUrl,
77
+ collectionPath,
78
+ })
79
+
80
+ setStatus('idle')
81
+ onUploaded(pendingValue)
82
+ },
83
+ [collectionPath, fieldPath, addPendingUpload, onUploaded]
84
+ )
85
+
86
+ const handleFileChange = useCallback(
87
+ (e: ChangeEvent<HTMLInputElement>) => {
88
+ const file = e.target.files?.[0]
89
+ if (file) handleFileSelected(file)
90
+ // Reset so re-selecting the same file fires the event again.
91
+ e.target.value = ''
92
+ },
93
+ [handleFileSelected]
94
+ )
95
+
96
+ const handleBrowseClick = useCallback(() => {
97
+ inputRef.current?.click()
98
+ }, [])
99
+
100
+ const handleDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {
101
+ e.preventDefault()
102
+ setIsDragOver(true)
103
+ }, [])
104
+
105
+ const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
106
+ e.preventDefault()
107
+ setIsDragOver(false)
108
+ }, [])
109
+
110
+ const handleDrop = useCallback(
111
+ (e: DragEvent<HTMLDivElement>) => {
112
+ e.preventDefault()
113
+ setIsDragOver(false)
114
+ const file = e.dataTransfer.files?.[0]
115
+ if (file) handleFileSelected(file)
116
+ },
117
+ [handleFileSelected]
118
+ )
119
+
120
+ const isProcessing = status === 'processing'
121
+
122
+ return (
123
+ <div className={cx('byline-field-file-upload', styles.root)}>
124
+ <input
125
+ ref={inputRef}
126
+ type="file"
127
+ accept={accept}
128
+ className={cx('byline-field-file-upload-input', styles.input)}
129
+ onChange={handleFileChange}
130
+ disabled={isProcessing}
131
+ aria-hidden="true"
132
+ tabIndex={-1}
133
+ />
134
+
135
+ <div
136
+ role="button"
137
+ tabIndex={0}
138
+ aria-label={t('fields.file.upload.zoneAriaLabel')}
139
+ onDragOver={handleDragOver}
140
+ onDragLeave={handleDragLeave}
141
+ onDrop={handleDrop}
142
+ onClick={handleBrowseClick}
143
+ onKeyDown={(e) => {
144
+ if (e.key === 'Enter' || e.key === ' ') {
145
+ e.preventDefault()
146
+ handleBrowseClick()
147
+ }
148
+ }}
149
+ className={cx(
150
+ 'byline-field-file-upload-zone',
151
+ styles.zone,
152
+ isDragOver &&
153
+ !isProcessing && ['byline-field-file-upload-zone-active', styles['zone-active']],
154
+ isProcessing && ['byline-field-file-upload-zone-busy', styles['zone-busy']]
155
+ )}
156
+ >
157
+ <svg
158
+ xmlns="http://www.w3.org/2000/svg"
159
+ className={cx('byline-field-file-upload-icon', styles.icon)}
160
+ fill="none"
161
+ viewBox="0 0 24 24"
162
+ stroke="currentColor"
163
+ strokeWidth={1.5}
164
+ aria-hidden="true"
165
+ >
166
+ <path
167
+ strokeLinecap="round"
168
+ strokeLinejoin="round"
169
+ 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"
170
+ />
171
+ </svg>
172
+ <span className={cx('byline-field-file-upload-label', styles.label)}>
173
+ {t('fields.file.upload.label')}{' '}
174
+ <span className={cx('byline-field-file-upload-action', styles.action)}>
175
+ {t('fields.file.upload.browse')}
176
+ </span>
177
+ </span>
178
+ </div>
179
+
180
+ {status === 'error' && errorMessage && (
181
+ <p className={cx('byline-field-file-upload-error', styles.error)} role="alert">
182
+ {errorMessage}
183
+ </p>
184
+ )}
185
+ </div>
186
+ )
187
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * GroupField — fixed-order group of child fields.
3
+ *
4
+ * Override handles:
5
+ * .byline-field-group — outer wrapper
6
+ * .byline-field-group-header — label/help text block
7
+ * .byline-field-group-title — title heading
8
+ * .byline-field-group-required — required asterisk
9
+ * .byline-field-group-help — help text below title
10
+ * .byline-field-group-body — vertical stack of children
11
+ */
12
+
13
+ .header,
14
+ :global(.byline-field-group-header) {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: 0.125rem;
18
+ margin-bottom: var(--spacing-8);
19
+ }
20
+
21
+ .title,
22
+ :global(.byline-field-group-title) {
23
+ font-size: 1rem;
24
+ font-weight: var(--font-weight-medium);
25
+ }
26
+
27
+ .required,
28
+ :global(.byline-field-group-required) {
29
+ color: var(--red-500);
30
+ }
31
+
32
+ .help,
33
+ :global(.byline-field-group-help) {
34
+ color: var(--gray-500);
35
+ font-size: var(--font-size-xs);
36
+ }
37
+
38
+ .body,
39
+ :global(.byline-field-group-body) {
40
+ display: flex;
41
+ flex-direction: column;
42
+ gap: var(--spacing-8);
43
+ }
@@ -0,0 +1,84 @@
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 { useMemo } from 'react'
10
+
11
+ import type { Field, GroupField as GroupFieldType } from '@byline/core'
12
+ import { ErrorText } from '@byline/ui/react'
13
+ import cx from 'classnames'
14
+
15
+ import { placeholderForField } from '../../fields/field-helpers'
16
+ import { FieldRenderer } from '../../fields/field-renderer'
17
+ import { useFieldError } from '../../forms/form-context'
18
+ import styles from './group-field.module.css'
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // GroupField — renders a fixed-order group of child fields wrapped in a
22
+ // single div. No drag-and-drop. No add/remove.
23
+ // The outer div carries the field type ('group') and field name as classes
24
+ // so consumers can target individual groups via CSS.
25
+ //
26
+ // Stable override handles: `.byline-field-group`, `.byline-field-group-header`,
27
+ // `.byline-field-group-title`, `.byline-field-group-help`,
28
+ // `.byline-field-group-body`.
29
+ // ---------------------------------------------------------------------------
30
+
31
+ interface GroupFieldProps {
32
+ field: GroupFieldType
33
+ defaultValue: any
34
+ path: string
35
+ }
36
+
37
+ export const GroupField = ({ field, defaultValue, path }: GroupFieldProps) => {
38
+ const fieldError = useFieldError(field.name)
39
+ // Default value for a group field is a plain object: { rating: 5, comment: '...' }
40
+ // Normalize to a plain object if not already one.
41
+ const groupData = useMemo(() => {
42
+ if (defaultValue && typeof defaultValue === 'object' && !Array.isArray(defaultValue)) {
43
+ return defaultValue
44
+ }
45
+ // Fallback: build a placeholder object from child field definitions
46
+ const placeholder: Record<string, any> = {}
47
+ for (const childField of field.fields as Field[]) {
48
+ placeholder[childField.name] = placeholderForField(childField)
49
+ }
50
+ return placeholder
51
+ }, [defaultValue, field.fields])
52
+
53
+ return (
54
+ <div className={`byline-field-group ${field.name}`}>
55
+ {field.label && (
56
+ <div className={cx('byline-field-group-header', styles.header)}>
57
+ <h3 className={cx('byline-field-group-title', styles.title)}>
58
+ {field.label}{' '}
59
+ {!field.optional && (
60
+ <span className={cx('byline-field-group-required', styles.required)}>*</span>
61
+ )}
62
+ </h3>
63
+ {field.helpText && (
64
+ <p className={cx('byline-field-group-help', styles.help)}>{field.helpText}</p>
65
+ )}
66
+ </div>
67
+ )}
68
+ <div className={cx('byline-field-group-body', styles.body)}>
69
+ {(field.fields as Field[]).map((innerField) => {
70
+ return (
71
+ <FieldRenderer
72
+ key={innerField.name}
73
+ field={innerField}
74
+ defaultValue={groupData[innerField.name]}
75
+ basePath={path}
76
+ disableSorting={true}
77
+ />
78
+ )
79
+ })}
80
+ </div>
81
+ {fieldError && <ErrorText id={`${field.name}-error`} text={fieldError} />}
82
+ </div>
83
+ )
84
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * ImageField — image preview + metadata + remove/upload affordances.
3
+ *
4
+ * Override handles:
5
+ * .byline-field-image — wrapper div
6
+ * .byline-field-image-header — label row
7
+ * .byline-field-image-remove — remove-button (icon, top-right of tile)
8
+ * .byline-field-image-empty — empty-state hint text
9
+ * .byline-field-image-tile — bordered preview/metadata tile
10
+ * .byline-field-image-preview-wrap — preview-image positioned wrapper
11
+ * .byline-field-image-preview — the <img> preview
12
+ * .byline-field-image-preview-svg — preview sized for SVG aspect
13
+ * .byline-field-image-pending — yellow "pending upload" pill
14
+ * .byline-field-image-meta — metadata list
15
+ * .byline-field-image-meta-key — metadata field-name span
16
+ * .byline-field-image-meta-pending — yellow text inside pending status
17
+ */
18
+
19
+ .header,
20
+ :global(.byline-field-image-header) {
21
+ display: flex;
22
+ align-items: baseline;
23
+ gap: var(--spacing-8);
24
+ margin-bottom: 0.25rem;
25
+ }
26
+
27
+ .remove,
28
+ :global(.byline-field-image-remove) {
29
+ position: absolute;
30
+ top: var(--spacing-6);
31
+ right: var(--spacing-6);
32
+ z-index: 1;
33
+ }
34
+
35
+ :global(.byline-field-image-remove .byline-button) {
36
+ color: var(--gray-900);
37
+ }
38
+
39
+ :global(.dark .byline-field-image-remove .byline-button),
40
+ :global([data-theme="dark"] .byline-field-image-remove .byline-button) {
41
+ color: var(--gray-200);
42
+ }
43
+
44
+ .empty,
45
+ :global(.byline-field-image-empty) {
46
+ color: var(--gray-500);
47
+ font-size: var(--font-size-xs);
48
+ font-style: italic;
49
+ }
50
+
51
+ .tile,
52
+ :global(.byline-field-image-tile) {
53
+ position: relative;
54
+ display: flex;
55
+ gap: var(--spacing-16);
56
+ margin-top: 0.25rem;
57
+ padding: var(--spacing-8);
58
+ border: var(--border-width-thin) var(--border-style-solid) var(--primary-500);
59
+ border-radius: var(--border-radius-md);
60
+ }
61
+
62
+ .preview-wrap,
63
+ :global(.byline-field-image-preview-wrap) {
64
+ position: relative;
65
+ }
66
+
67
+ .preview-button,
68
+ :global(.byline-field-image-preview-button) {
69
+ appearance: none;
70
+ background: none;
71
+ border: none;
72
+ padding: 0;
73
+ margin: 0;
74
+ display: block;
75
+ cursor: pointer;
76
+ border-radius: var(--border-radius-sm);
77
+ }
78
+
79
+ .preview-button:focus-visible,
80
+ :global(.byline-field-image-preview-button):focus-visible {
81
+ outline: 2px solid var(--primary-500);
82
+ outline-offset: 2px;
83
+ }
84
+
85
+ .preview-button img,
86
+ :global(.byline-field-image-preview-button) img {
87
+ transition: opacity 120ms ease;
88
+ }
89
+
90
+ .preview-button:hover img,
91
+ :global(.byline-field-image-preview-button):hover img {
92
+ opacity: 0.85;
93
+ }
94
+
95
+ .preview,
96
+ :global(.byline-field-image-preview) {
97
+ max-height: 10rem;
98
+ min-height: 4rem;
99
+ min-width: 4rem;
100
+ border: var(--border-width-thin) var(--border-style-solid) var(--gray-600);
101
+ border-radius: var(--border-radius-sm);
102
+ object-fit: contain;
103
+ }
104
+
105
+ .preview-svg,
106
+ :global(.byline-field-image-preview-svg) {
107
+ width: 271px;
108
+ height: 159px;
109
+ max-height: none;
110
+ }
111
+
112
+ .pending,
113
+ :global(.byline-field-image-pending) {
114
+ position: absolute;
115
+ top: 0.25rem;
116
+ left: 0.25rem;
117
+ padding: 0.125rem 0.375rem;
118
+ background-color: oklch(from var(--yellow-600) l c h / 0.9);
119
+ color: var(--yellow-100);
120
+ font-size: 0.6rem;
121
+ font-weight: var(--font-weight-medium);
122
+ border-radius: var(--border-radius-sm);
123
+ }
124
+
125
+ .uploading,
126
+ :global(.byline-field-image-uploading) {
127
+ position: absolute;
128
+ inset: 0;
129
+ z-index: 2;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ background-color: oklch(from var(--gray-950) l c h / 0.5);
134
+ border-radius: var(--border-radius-md);
135
+ }
136
+
137
+ .meta,
138
+ :global(.byline-field-image-meta) {
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: 0.125rem;
142
+ padding-right: var(--spacing-32);
143
+ color: var(--gray-200);
144
+ font-size: var(--font-size-xs);
145
+ }
146
+
147
+ .meta-key,
148
+ :global(.byline-field-image-meta-key) {
149
+ font-weight: var(--font-weight-semibold);
150
+ }
151
+
152
+ .meta-pending,
153
+ :global(.byline-field-image-meta-pending) {
154
+ color: var(--yellow-400);
155
+ }