@byline/admin 2.5.1 → 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,153 @@
1
+ /**
2
+ * FileField — metadata tile + remove/upload affordances for arbitrary file
3
+ * uploads (non-image). Mirrors `image-field.module.css` structurally; the
4
+ * image-only preview / SVG-shape / pending-badge rules are dropped.
5
+ *
6
+ * Override handles:
7
+ * .byline-field-file — wrapper div
8
+ * .byline-field-file-header — label row
9
+ * .byline-field-file-actions — top-right icon-button group (download + remove)
10
+ * .byline-field-file-empty — empty-state hint text
11
+ * .byline-field-file-tile — bordered metadata tile
12
+ * .byline-field-file-uploading — centered overlay shown while uploading
13
+ * .byline-field-file-icon-wrap — left-aligned document-icon wrapper
14
+ * .byline-field-file-icon — the document glyph itself
15
+ * .byline-field-file-pending — yellow "pending upload" pill
16
+ * .byline-field-file-meta — metadata list
17
+ * .byline-field-file-meta-key — metadata field-name span
18
+ * .byline-field-file-meta-pending — yellow text inside pending status
19
+ */
20
+
21
+ .header,
22
+ :global(.byline-field-file-header) {
23
+ display: flex;
24
+ align-items: baseline;
25
+ gap: var(--spacing-8);
26
+ margin-bottom: 0.25rem;
27
+ }
28
+
29
+ .actions,
30
+ :global(.byline-field-file-actions) {
31
+ position: absolute;
32
+ top: var(--spacing-6);
33
+ right: var(--spacing-6);
34
+ z-index: 1;
35
+ display: flex;
36
+ align-items: center;
37
+ gap: var(--spacing-4);
38
+ }
39
+
40
+ :global(.byline-field-file-actions .byline-button) {
41
+ color: var(--gray-900);
42
+ }
43
+
44
+ :global(.dark .byline-field-file-actions .byline-button),
45
+ :global([data-theme="dark"] .byline-field-file-actions .byline-button) {
46
+ color: var(--gray-200);
47
+ }
48
+
49
+ .empty,
50
+ :global(.byline-field-file-empty) {
51
+ color: var(--gray-500);
52
+ font-size: var(--font-size-xs);
53
+ font-style: italic;
54
+ }
55
+
56
+ .tile,
57
+ :global(.byline-field-file-tile) {
58
+ position: relative;
59
+ display: flex;
60
+ gap: var(--spacing-16);
61
+ margin-top: 0.25rem;
62
+ padding: var(--spacing-8);
63
+ border: var(--border-width-thin) var(--border-style-solid) var(--primary-500);
64
+ border-radius: var(--border-radius-md);
65
+ }
66
+
67
+ .uploading,
68
+ :global(.byline-field-file-uploading) {
69
+ position: absolute;
70
+ inset: 0;
71
+ z-index: 2;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ background-color: oklch(from var(--gray-950) l c h / 0.5);
76
+ border-radius: var(--border-radius-md);
77
+ }
78
+
79
+ .icon-wrap,
80
+ :global(.byline-field-file-icon-wrap) {
81
+ position: relative;
82
+ flex-shrink: 0;
83
+ align-self: flex-start;
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ width: 4rem;
88
+ height: 4rem;
89
+ border: var(--border-width-thin) var(--border-style-solid) var(--gray-600);
90
+ border-radius: var(--border-radius-sm);
91
+ color: var(--gray-300);
92
+ }
93
+
94
+ /* Interactive affordance when the wrap is rendered as a link
95
+ (file is stored — clicking opens the asset in a new tab). */
96
+ a.icon-wrap,
97
+ :global(a.byline-field-file-icon-wrap) {
98
+ cursor: pointer;
99
+ text-decoration: none;
100
+ transition:
101
+ border-color 120ms ease,
102
+ color 120ms ease;
103
+ }
104
+
105
+ a.icon-wrap:hover,
106
+ :global(a.byline-field-file-icon-wrap):hover {
107
+ border-color: var(--primary-500);
108
+ color: var(--primary-400);
109
+ }
110
+
111
+ a.icon-wrap:focus-visible,
112
+ :global(a.byline-field-file-icon-wrap):focus-visible {
113
+ outline: 2px solid var(--primary-500);
114
+ outline-offset: 2px;
115
+ }
116
+
117
+ .icon,
118
+ :global(.byline-field-file-icon) {
119
+ opacity: 0.85;
120
+ }
121
+
122
+ .pending,
123
+ :global(.byline-field-file-pending) {
124
+ position: absolute;
125
+ top: 0.25rem;
126
+ left: 0.25rem;
127
+ padding: 0.125rem 0.375rem;
128
+ background-color: oklch(from var(--yellow-600) l c h / 0.9);
129
+ color: var(--yellow-100);
130
+ font-size: 0.6rem;
131
+ font-weight: var(--font-weight-medium);
132
+ border-radius: var(--border-radius-sm);
133
+ }
134
+
135
+ .meta,
136
+ :global(.byline-field-file-meta) {
137
+ display: flex;
138
+ flex-direction: column;
139
+ gap: 0.125rem;
140
+ padding-right: var(--spacing-32);
141
+ color: var(--gray-200);
142
+ font-size: var(--font-size-xs);
143
+ }
144
+
145
+ .meta-key,
146
+ :global(.byline-field-file-meta-key) {
147
+ font-weight: var(--font-weight-semibold);
148
+ }
149
+
150
+ .meta-pending,
151
+ :global(.byline-field-file-meta-pending) {
152
+ color: var(--yellow-400);
153
+ }
@@ -0,0 +1,286 @@
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 {
10
+ type FileField as FieldType,
11
+ isPendingStoredFileValue,
12
+ type StoredFileValue,
13
+ } from '@byline/core'
14
+ import { useTranslation } from '@byline/i18n/react'
15
+ import {
16
+ CloseIcon,
17
+ DocumentIcon,
18
+ DownloadIcon,
19
+ ErrorText,
20
+ HelpText,
21
+ IconButton,
22
+ Label,
23
+ LoaderRing,
24
+ VideoIcon,
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 './file-field.module.css'
37
+ import { FileUploadField } from './file-upload-field'
38
+
39
+ /**
40
+ * Trigger a download via a temporary anchor. Mirrors the helper in
41
+ * `image-lightbox.tsx`: same-origin URLs respect the `download` attribute and
42
+ * save with the suggested filename; cross-origin URLs without CORS headers
43
+ * fall through to navigation in a new tab, where the user can right-click
44
+ * Save As.
45
+ */
46
+ function triggerDownload(url: string, filename?: string) {
47
+ if (typeof document === 'undefined') return
48
+ const a = document.createElement('a')
49
+ a.href = url
50
+ if (filename) a.download = filename
51
+ a.target = '_blank'
52
+ a.rel = 'noreferrer'
53
+ document.body.appendChild(a)
54
+ a.click()
55
+ document.body.removeChild(a)
56
+ }
57
+
58
+ interface FileFieldProps {
59
+ field: FieldType
60
+ /** Collection path required to call the /upload endpoint. */
61
+ collectionPath?: string
62
+ value?: StoredFileValue | null
63
+ defaultValue?: StoredFileValue | null
64
+ onChange?: (value: StoredFileValue | null) => void
65
+ path?: string
66
+ }
67
+
68
+ export const FileField = ({
69
+ field,
70
+ collectionPath,
71
+ value,
72
+ defaultValue,
73
+ onChange: _onChange,
74
+ path,
75
+ }: FileFieldProps) => {
76
+ const fieldPath = path ?? field.name
77
+ const { t } = useTranslation('byline-admin')
78
+ const fieldError = useFieldError(fieldPath)
79
+ const isDirty = useIsDirty(fieldPath)
80
+ const fieldValue = useFieldValue<StoredFileValue | null | undefined>(fieldPath)
81
+ const isUploading = useIsFieldUploading(fieldPath)
82
+ const { removePendingUpload } = useFormContext()
83
+
84
+ const handleChange = useFieldChangeHandler(field, fieldPath)
85
+
86
+ // Mirror the image-field rule: once the field has been touched, the form
87
+ // value is authoritative (even when null, so a click-to-remove sticks);
88
+ // otherwise fall back to props.
89
+ const incomingValue = isDirty
90
+ ? (fieldValue ?? null)
91
+ : (value ?? fieldValue ?? defaultValue ?? null)
92
+
93
+ const isPending = isPendingStoredFileValue(incomingValue)
94
+
95
+ // Legacy placeholder shape — kept for backwards compatibility with older
96
+ // seed data, matching the image-field check.
97
+ const isOldPlaceholder = (v: unknown): boolean => {
98
+ if (!v || typeof v !== 'object') return false
99
+ const maybe = v as Partial<StoredFileValue>
100
+ return maybe.storageProvider === 'placeholder' && maybe.storagePath === 'pending'
101
+ }
102
+
103
+ const showUploadWidget = incomingValue == null || isOldPlaceholder(incomingValue)
104
+
105
+ const handleRemove = () => {
106
+ if (isPending) {
107
+ removePendingUpload(fieldPath)
108
+ }
109
+ handleChange(null)
110
+ }
111
+
112
+ // MIME-driven glyph dispatch. Until a dedicated VideoField primitive lands,
113
+ // the FileField is the canonical home for video uploads — the schema's
114
+ // `upload.allowedMimeTypes` decides what gets in, and we swap the glyph
115
+ // here based on the resolved MIME so the tile reads as "video" rather
116
+ // than "generic document".
117
+ const isVideo = incomingValue?.mimeType?.startsWith('video/') === true
118
+ const FileGlyph = isVideo ? VideoIcon : DocumentIcon
119
+
120
+ const htmlId = fieldPath
121
+
122
+ return (
123
+ <div className={`byline-field-file ${field.name}`}>
124
+ <div className={cx('byline-field-file-header', styles.header)}>
125
+ <Label
126
+ id={htmlId}
127
+ htmlFor={htmlId}
128
+ label={field.label ?? field.name}
129
+ required={!field.optional}
130
+ />
131
+ </div>
132
+
133
+ {showUploadWidget ? (
134
+ collectionPath ? (
135
+ <FileUploadField
136
+ field={field}
137
+ collectionPath={collectionPath}
138
+ fieldPath={fieldPath}
139
+ onUploaded={(uploaded) => {
140
+ handleChange(uploaded)
141
+ }}
142
+ />
143
+ ) : (
144
+ <div className={cx('byline-field-file-empty', styles.empty)}>
145
+ {t('fields.file.empty')}
146
+ </div>
147
+ )
148
+ ) : (
149
+ <div className={cx('byline-field-file-tile', styles.tile)}>
150
+ {isUploading && (
151
+ <div
152
+ className={cx('byline-field-file-uploading', styles.uploading)}
153
+ aria-live="polite"
154
+ aria-busy="true"
155
+ >
156
+ <LoaderRing />
157
+ </div>
158
+ )}
159
+ {collectionPath && (
160
+ <div className={cx('byline-field-file-actions', styles.actions)}>
161
+ {!isPending && incomingValue?.storageUrl && (
162
+ <IconButton
163
+ type="button"
164
+ intent="noeffect"
165
+ onClick={() =>
166
+ triggerDownload(
167
+ incomingValue.storageUrl as string,
168
+ incomingValue.originalFilename ?? incomingValue.filename
169
+ )
170
+ }
171
+ size="xs"
172
+ disabled={isUploading}
173
+ aria-label={t('fields.file.downloadAriaLabel')}
174
+ >
175
+ <DownloadIcon width="15px" height="15px" />
176
+ </IconButton>
177
+ )}
178
+ <IconButton
179
+ type="button"
180
+ intent="noeffect"
181
+ onClick={handleRemove}
182
+ size="xs"
183
+ disabled={isUploading}
184
+ aria-label={t('fields.file.removeAriaLabel')}
185
+ >
186
+ <CloseIcon width="15px" height="15px" />
187
+ </IconButton>
188
+ </div>
189
+ )}
190
+ {/* Document icon + (optional) pending badge — mirrors the
191
+ image-field's preview-wrap so the file tile has the same
192
+ visual hierarchy: glyph on the left, metadata on the right.
193
+ When the file is stored (non-pending and resolvable storageUrl),
194
+ the wrap is rendered as an anchor that opens the asset in a new
195
+ tab — browser-native viewer dispatch (PDFs render inline,
196
+ non-renderable types fall through to download). */}
197
+ {!isPending && incomingValue?.storageUrl ? (
198
+ <a
199
+ href={incomingValue.storageUrl}
200
+ target="_blank"
201
+ rel="noreferrer"
202
+ aria-label={t('fields.file.openInNewTabAriaLabel', {
203
+ filename: incomingValue.originalFilename ?? incomingValue.filename,
204
+ })}
205
+ className={cx('byline-field-file-icon-wrap', styles['icon-wrap'])}
206
+ >
207
+ <FileGlyph
208
+ width="48px"
209
+ height="48px"
210
+ className={cx('byline-field-file-icon', styles.icon)}
211
+ />
212
+ </a>
213
+ ) : (
214
+ <div className={cx('byline-field-file-icon-wrap', styles['icon-wrap'])}>
215
+ <FileGlyph
216
+ width="48px"
217
+ height="48px"
218
+ className={cx('byline-field-file-icon', styles.icon)}
219
+ />
220
+ {isPending && (
221
+ <div className={cx('byline-field-file-pending', styles.pending)}>
222
+ {t('fields.fileMeta.pendingUpload')}
223
+ </div>
224
+ )}
225
+ </div>
226
+ )}
227
+ <div className={cx('byline-field-file-meta', styles.meta)}>
228
+ <div>
229
+ <span className={cx('byline-field-file-meta-key', styles['meta-key'])}>
230
+ {t('fields.fileMeta.filename')}
231
+ </span>{' '}
232
+ {incomingValue?.filename}
233
+ </div>
234
+ <div>
235
+ <span className={cx('byline-field-file-meta-key', styles['meta-key'])}>
236
+ {t('fields.fileMeta.original')}
237
+ </span>{' '}
238
+ {incomingValue?.originalFilename}
239
+ </div>
240
+ <div>
241
+ <span className={cx('byline-field-file-meta-key', styles['meta-key'])}>
242
+ {t('fields.fileMeta.type')}
243
+ </span>{' '}
244
+ {incomingValue?.mimeType}
245
+ </div>
246
+ <div>
247
+ <span className={cx('byline-field-file-meta-key', styles['meta-key'])}>
248
+ {t('fields.fileMeta.size')}
249
+ </span>{' '}
250
+ {incomingValue?.fileSize}
251
+ </div>
252
+ {isPending ? (
253
+ <div>
254
+ <span className={cx('byline-field-file-meta-key', styles['meta-key'])}>
255
+ {t('fields.fileMeta.status')}
256
+ </span>{' '}
257
+ <span className={cx('byline-field-file-meta-pending', styles['meta-pending'])}>
258
+ {t('fields.fileMeta.willUploadOnSave')}
259
+ </span>
260
+ </div>
261
+ ) : (
262
+ <>
263
+ <div>
264
+ <span className={cx('byline-field-file-meta-key', styles['meta-key'])}>
265
+ {t('fields.fileMeta.storage')}
266
+ </span>{' '}
267
+ {incomingValue?.storageProvider}
268
+ </div>
269
+ <div>
270
+ <span className={cx('byline-field-file-meta-key', styles['meta-key'])}>
271
+ {t('fields.fileMeta.path')}
272
+ </span>{' '}
273
+ {incomingValue?.storagePath}
274
+ </div>
275
+ </>
276
+ )}
277
+ </div>
278
+ </div>
279
+ )}
280
+
281
+ {field.helpText && <HelpText text={field.helpText} />}
282
+
283
+ {fieldError && <ErrorText id={`${field.name}-error`} text={fieldError} />}
284
+ </div>
285
+ )
286
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * FileUploadField — drag-and-drop file picker that registers a deferred
3
+ * upload in form context.
4
+ *
5
+ * Override handles:
6
+ * .byline-field-file-upload — root wrapper
7
+ * .byline-field-file-upload-input — visually-hidden file input
8
+ * .byline-field-file-upload-zone — clickable / drop target
9
+ * .byline-field-file-upload-zone-active — drag-hovered state
10
+ * .byline-field-file-upload-zone-busy — processing state
11
+ * .byline-field-file-upload-icon — upload icon svg
12
+ * .byline-field-file-upload-label — primary text inside the zone
13
+ * .byline-field-file-upload-action — "browse" link inside the label
14
+ * .byline-field-file-upload-error — error message paragraph
15
+ */
16
+
17
+ .root,
18
+ :global(.byline-field-file-upload) {
19
+ margin-top: 0.25rem;
20
+ }
21
+
22
+ .input,
23
+ :global(.byline-field-file-upload-input) {
24
+ position: absolute;
25
+ width: 1px;
26
+ height: 1px;
27
+ padding: 0;
28
+ margin: -1px;
29
+ overflow: hidden;
30
+ clip: rect(0, 0, 0, 0);
31
+ white-space: nowrap;
32
+ border: 0;
33
+ }
34
+
35
+ .zone,
36
+ :global(.byline-field-file-upload-zone) {
37
+ display: flex;
38
+ flex-direction: column;
39
+ align-items: center;
40
+ justify-content: center;
41
+ gap: var(--spacing-8);
42
+ padding: 1.5rem 1rem;
43
+ border: 2px dashed var(--gray-600);
44
+ border-radius: var(--border-radius-lg);
45
+ color: var(--gray-400);
46
+ text-align: center;
47
+ cursor: pointer;
48
+ user-select: none;
49
+ transition:
50
+ color 150ms ease,
51
+ background-color 150ms ease,
52
+ border-color 150ms ease;
53
+ }
54
+
55
+ .zone:hover,
56
+ :global(.byline-field-file-upload-zone):hover {
57
+ border-color: var(--primary-500);
58
+ background-color: oklch(from var(--primary-900) l c h / 0.1);
59
+ }
60
+
61
+ .zone-active,
62
+ :global(.byline-field-file-upload-zone-active) {
63
+ border-color: var(--primary-400);
64
+ background-color: oklch(from var(--primary-900) l c h / 0.2);
65
+ color: var(--primary-300);
66
+ }
67
+
68
+ .zone-busy,
69
+ :global(.byline-field-file-upload-zone-busy) {
70
+ border-color: var(--gray-700);
71
+ background-color: oklch(from var(--canvas-800) l c h / 0.5);
72
+ color: var(--gray-600);
73
+ cursor: not-allowed;
74
+ }
75
+
76
+ .icon,
77
+ :global(.byline-field-file-upload-icon) {
78
+ width: 1.75rem;
79
+ height: 1.75rem;
80
+ opacity: 0.6;
81
+ }
82
+
83
+ .label,
84
+ :global(.byline-field-file-upload-label) {
85
+ font-size: var(--font-size-xs);
86
+ font-weight: var(--font-weight-medium);
87
+ }
88
+
89
+ .action,
90
+ :global(.byline-field-file-upload-action) {
91
+ color: var(--primary-400);
92
+ text-decoration: underline;
93
+ text-underline-offset: 2px;
94
+ }
95
+
96
+ .error,
97
+ :global(.byline-field-file-upload-error) {
98
+ margin-top: 0.375rem;
99
+ color: var(--red-400);
100
+ font-size: var(--font-size-xs);
101
+ }