@byline/admin 3.20.0 → 3.20.2

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.
@@ -2,7 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { getCollectionAdminConfig, getCollectionDefinition } from "@byline/core";
4
4
  import { useTranslation } from "@byline/i18n/react";
5
- import { Button, CloseIcon, EditIcon, ErrorText, IconButton, Label } from "@byline/ui/react";
5
+ import { Button, CloseIcon, EditIcon, ErrorText, HelpText, IconButton, Label } from "@byline/ui/react";
6
6
  import classnames from "classnames";
7
7
  import { useFieldError, useFieldValue } from "../../forms/form-context.js";
8
8
  import relation_field_module from "./relation-field.module.js";
@@ -48,10 +48,6 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
48
48
  required: !field.optional
49
49
  })
50
50
  }),
51
- field.helpText && /*#__PURE__*/ jsx("div", {
52
- className: classnames('byline-field-relation-help', relation_field_module.help),
53
- children: field.helpText
54
- }),
55
51
  isUnknown ? /*#__PURE__*/ jsxs("div", {
56
52
  className: classnames('byline-field-relation-error-tile', relation_field_module["error-tile"]),
57
53
  children: [
@@ -66,60 +62,66 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
66
62
  children: t('fields.relation.unknownHint')
67
63
  })
68
64
  ]
69
- }) : incomingValue ? /*#__PURE__*/ jsxs("div", {
70
- className: classnames('byline-field-relation-tile', relation_field_module.tile),
71
- children: [
72
- /*#__PURE__*/ jsx(RelationSummary, {
73
- targetDefinition: targetDef,
74
- targetAdminConfig: targetAdminConfig,
75
- displayField: field.displayField,
76
- value: incomingValue,
77
- cachedRecord: cachedRecord
78
- }),
79
- /*#__PURE__*/ jsxs("div", {
80
- className: classnames('byline-field-relation-actions', relation_field_module.actions),
81
- children: [
82
- /*#__PURE__*/ jsx(IconButton, {
83
- id: htmlId,
84
- type: "button",
85
- intent: "noeffect",
86
- size: "xs",
87
- "aria-label": t('fields.relation.changeAriaLabel', {
88
- label: targetDef.labels.singular
89
- }),
90
- onClick: ()=>setPickerOpen(true),
91
- children: /*#__PURE__*/ jsx(EditIcon, {
92
- width: "15px",
93
- height: "15px"
94
- })
95
- }),
96
- /*#__PURE__*/ jsx(IconButton, {
97
- type: "button",
98
- intent: "noeffect",
99
- size: "xs",
100
- "aria-label": t('fields.relation.removeAriaLabel', {
101
- label: targetDef.labels.singular
65
+ }) : /*#__PURE__*/ jsx("div", {
66
+ className: classnames('byline-field-relation-frame', relation_field_module.frame),
67
+ children: incomingValue ? /*#__PURE__*/ jsxs("div", {
68
+ className: classnames('byline-field-relation-tile', relation_field_module.tile),
69
+ children: [
70
+ /*#__PURE__*/ jsx(RelationSummary, {
71
+ targetDefinition: targetDef,
72
+ targetAdminConfig: targetAdminConfig,
73
+ displayField: field.displayField,
74
+ value: incomingValue,
75
+ cachedRecord: cachedRecord
76
+ }),
77
+ /*#__PURE__*/ jsxs("div", {
78
+ className: classnames('byline-field-relation-actions', relation_field_module.actions),
79
+ children: [
80
+ /*#__PURE__*/ jsx(IconButton, {
81
+ id: htmlId,
82
+ type: "button",
83
+ intent: "noeffect",
84
+ size: "xs",
85
+ "aria-label": t('fields.relation.changeAriaLabel', {
86
+ label: targetDef.labels.singular
87
+ }),
88
+ onClick: ()=>setPickerOpen(true),
89
+ children: /*#__PURE__*/ jsx(EditIcon, {
90
+ width: "15px",
91
+ height: "15px"
92
+ })
102
93
  }),
103
- onClick: handleRemove,
104
- children: /*#__PURE__*/ jsx(CloseIcon, {
105
- width: "15px",
106
- height: "15px"
94
+ /*#__PURE__*/ jsx(IconButton, {
95
+ type: "button",
96
+ intent: "noeffect",
97
+ size: "xs",
98
+ "aria-label": t('fields.relation.removeAriaLabel', {
99
+ label: targetDef.labels.singular
100
+ }),
101
+ onClick: handleRemove,
102
+ children: /*#__PURE__*/ jsx(CloseIcon, {
103
+ width: "15px",
104
+ height: "15px"
105
+ })
107
106
  })
108
- })
109
- ]
107
+ ]
108
+ })
109
+ ]
110
+ }) : /*#__PURE__*/ jsx(Button, {
111
+ id: htmlId,
112
+ size: "xs",
113
+ variant: "outlined",
114
+ intent: "noeffect",
115
+ type: "button",
116
+ onClick: ()=>setPickerOpen(true),
117
+ children: t('fields.relation.selectButton', {
118
+ label: targetDef.labels.singular
110
119
  })
111
- ]
112
- }) : /*#__PURE__*/ jsx(Button, {
113
- id: htmlId,
114
- size: "xs",
115
- variant: "outlined",
116
- intent: "noeffect",
117
- type: "button",
118
- onClick: ()=>setPickerOpen(true),
119
- children: t('fields.relation.selectButton', {
120
- label: targetDef.labels.singular
121
120
  })
122
121
  }),
122
+ null != field.helpText && /*#__PURE__*/ jsx(HelpText, {
123
+ text: field.helpText
124
+ }),
123
125
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
124
126
  id: `${field.name}-error`,
125
127
  text: fieldError
@@ -1,7 +1,6 @@
1
1
  import "./relation-field_module.css";
2
2
  const relation_field_module = {
3
3
  header: "header-y7lSnb",
4
- help: "help-DEGVPH",
5
4
  "error-tile": "error-tile-y2kiiq",
6
5
  errorTile: "error-tile-y2kiiq",
7
6
  "error-text": "error-text-jHQh34",
@@ -23,6 +22,7 @@ const relation_field_module = {
23
22
  "many-remove": "many-remove-fuuKiO",
24
23
  manyRemove: "many-remove-fuuKiO",
25
24
  "many-add": "many-add-c5uQ7B",
26
- manyAdd: "many-add-c5uQ7B"
25
+ manyAdd: "many-add-c5uQ7B",
26
+ frame: "frame-uo2RPX"
27
27
  };
28
28
  export default relation_field_module;
@@ -5,12 +5,6 @@
5
5
  display: flex;
6
6
  }
7
7
 
8
- :is(.help-DEGVPH, .byline-field-relation-help) {
9
- color: var(--gray-400);
10
- font-size: var(--font-size-xs);
11
- margin-bottom: .25rem;
12
- }
13
-
14
8
  :is(.error-tile-y2kiiq, .byline-field-relation-error-tile) {
15
9
  padding: var(--spacing-8);
16
10
  border: var(--border-width-thin) var(--border-style-solid) var(--red-700);
@@ -124,3 +118,30 @@
124
118
  display: flex;
125
119
  }
126
120
 
121
+ :is(.frame-uo2RPX, .byline-field-relation-frame) {
122
+ border: var(--border-width-thin) var(--border-style-solid) var(--field-border);
123
+ border-radius: var(--border-radius-md, 6px);
124
+ margin-top: .25rem;
125
+ padding: .5rem;
126
+ }
127
+
128
+ .frame-uo2RPX > .tile-Y3_yre, .frame-uo2RPX > .many-ohEmr7 {
129
+ margin-top: 0;
130
+ }
131
+
132
+ .byline-field-relation-frame > .byline-field-relation-tile {
133
+ margin-top: 0;
134
+ }
135
+
136
+ .byline-field-relation-frame > .byline-field-relation-many {
137
+ margin-top: 0;
138
+ }
139
+
140
+ .frame-uo2RPX > .many-empty-T0OJd9 {
141
+ margin-top: 0;
142
+ }
143
+
144
+ .byline-field-relation-frame > .byline-field-relation-many-empty {
145
+ margin-top: 0;
146
+ }
147
+
@@ -2,7 +2,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { getCollectionAdminConfig, getCollectionDefinition } from "@byline/core";
4
4
  import { useTranslation } from "@byline/i18n/react";
5
- import { Button, CloseIcon, DraggableSortable, ErrorText, GripperVerticalIcon, IconButton, Label, PlusIcon, moveItem, useSortable } from "@byline/ui/react";
5
+ import { Button, CloseIcon, DraggableSortable, ErrorText, GripperVerticalIcon, HelpText, IconButton, Label, PlusIcon, moveItem, useSortable } from "@byline/ui/react";
6
6
  import classnames from "classnames";
7
7
  import { useFieldError, useFieldValue, useFormContext } from "../../forms/form-context.js";
8
8
  import relation_field_module from "./relation-field.module.js";
@@ -115,10 +115,6 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
115
115
  required: !field.optional
116
116
  })
117
117
  }),
118
- field.helpText && /*#__PURE__*/ jsx("div", {
119
- className: classnames('byline-field-relation-help', relation_field_module.help),
120
- children: field.helpText
121
- }),
122
118
  isUnknown ? /*#__PURE__*/ jsxs("div", {
123
119
  className: classnames('byline-field-relation-error-tile', relation_field_module["error-tile"]),
124
120
  children: [
@@ -135,54 +131,62 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
135
131
  ]
136
132
  }) : /*#__PURE__*/ jsxs(Fragment, {
137
133
  children: [
138
- 0 === items.length ? /*#__PURE__*/ jsx("p", {
139
- className: classnames('byline-field-relation-many-empty', relation_field_module["many-empty"]),
140
- children: t('fields.relation.manyEmpty', {
141
- label: targetDef.labels.plural
142
- })
143
- }) : /*#__PURE__*/ jsx(DraggableSortable, {
144
- ids: items.map((v)=>v.targetDocumentId),
145
- onDragEnd: handleDragEnd,
146
- className: classnames('byline-field-relation-many', relation_field_module.many),
147
- children: items.map((value)=>/*#__PURE__*/ jsx(RelationManyTile, {
148
- id: value.targetDocumentId,
149
- onRemove: ()=>handleRemove(value.targetDocumentId),
150
- removeAriaLabel: t('fields.relation.removeAriaLabel', {
151
- label: targetLabel
152
- }),
153
- children: /*#__PURE__*/ jsx(RelationSummary, {
154
- targetDefinition: targetDef,
155
- targetAdminConfig: targetAdminConfig,
156
- displayField: field.displayField,
157
- value: value,
158
- cachedRecord: pickedRecords[value.targetDocumentId] ?? null
134
+ /*#__PURE__*/ jsxs("div", {
135
+ className: classnames('byline-field-relation-frame', relation_field_module.frame),
136
+ children: [
137
+ 0 === items.length ? /*#__PURE__*/ jsx("p", {
138
+ className: classnames('byline-field-relation-many-empty', relation_field_module["many-empty"]),
139
+ children: t('fields.relation.manyEmpty', {
140
+ label: targetDef.labels.plural
159
141
  })
160
- }, value.targetDocumentId))
142
+ }) : /*#__PURE__*/ jsx(DraggableSortable, {
143
+ ids: items.map((v)=>v.targetDocumentId),
144
+ onDragEnd: handleDragEnd,
145
+ className: classnames('byline-field-relation-many', relation_field_module.many),
146
+ children: items.map((value)=>/*#__PURE__*/ jsx(RelationManyTile, {
147
+ id: value.targetDocumentId,
148
+ onRemove: ()=>handleRemove(value.targetDocumentId),
149
+ removeAriaLabel: t('fields.relation.removeAriaLabel', {
150
+ label: targetLabel
151
+ }),
152
+ children: /*#__PURE__*/ jsx(RelationSummary, {
153
+ targetDefinition: targetDef,
154
+ targetAdminConfig: targetAdminConfig,
155
+ displayField: field.displayField,
156
+ value: value,
157
+ cachedRecord: pickedRecords[value.targetDocumentId] ?? null
158
+ })
159
+ }, value.targetDocumentId))
160
+ }),
161
+ /*#__PURE__*/ jsx("div", {
162
+ className: classnames('byline-field-relation-many-add', relation_field_module["many-add"]),
163
+ children: /*#__PURE__*/ jsxs(Button, {
164
+ id: htmlId,
165
+ size: "xs",
166
+ variant: "outlined",
167
+ intent: "noeffect",
168
+ type: "button",
169
+ onClick: ()=>setPickerOpen(true),
170
+ children: [
171
+ /*#__PURE__*/ jsx(PlusIcon, {
172
+ width: "14px",
173
+ height: "14px"
174
+ }),
175
+ t('fields.relation.addButton', {
176
+ label: targetLabel
177
+ })
178
+ ]
179
+ })
180
+ })
181
+ ]
182
+ }),
183
+ null != field.helpText && /*#__PURE__*/ jsx(HelpText, {
184
+ text: field.helpText
161
185
  }),
162
186
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
163
187
  id: `${field.name}-error`,
164
188
  text: fieldError
165
189
  }),
166
- /*#__PURE__*/ jsx("div", {
167
- className: classnames('byline-field-relation-many-add', relation_field_module["many-add"]),
168
- children: /*#__PURE__*/ jsxs(Button, {
169
- id: htmlId,
170
- size: "xs",
171
- variant: "outlined",
172
- intent: "noeffect",
173
- type: "button",
174
- onClick: ()=>setPickerOpen(true),
175
- children: [
176
- /*#__PURE__*/ jsx(PlusIcon, {
177
- width: "14px",
178
- height: "14px"
179
- }),
180
- t('fields.relation.addButton', {
181
- label: targetLabel
182
- })
183
- ]
184
- })
185
- }),
186
190
  /*#__PURE__*/ jsx(RelationPicker, {
187
191
  multiple: true,
188
192
  targetCollectionPath: field.targetCollection,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/admin",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "3.20.0",
5
+ "version": "3.20.2",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -155,10 +155,10 @@
155
155
  "react-diff-viewer-continued": "^4.2.2",
156
156
  "uuid": "^14.0.1",
157
157
  "zod": "^4.4.3",
158
- "@byline/auth": "3.20.0",
159
- "@byline/ui": "3.20.0",
160
- "@byline/core": "3.20.0",
161
- "@byline/i18n": "3.20.0"
158
+ "@byline/auth": "3.20.2",
159
+ "@byline/ui": "3.20.2",
160
+ "@byline/i18n": "3.20.2",
161
+ "@byline/core": "3.20.2"
162
162
  },
163
163
  "peerDependencies": {
164
164
  "react": "^19.0.0",
@@ -4,12 +4,16 @@
4
4
  * Override handles:
5
5
  * .byline-field-relation — outer wrapper
6
6
  * .byline-field-relation-header — label row
7
- * .byline-field-relation-help help text below the label
7
+ * .byline-field-relation-frame rounded border around the interactive body
8
8
  * .byline-field-relation-error-tile — wrapper shown when target collection is unknown
9
9
  * .byline-field-relation-error-text — secondary error message below the title
10
10
  * .byline-field-relation-tile — bordered selected-value + actions container
11
11
  * .byline-field-relation-actions — top-right icon-button group (edit + remove)
12
12
  * .byline-field-relation-mono — monospace `<code>` for field/collection names
13
+ *
14
+ * Help text renders BELOW the frame via the ui-kit `HelpText` component
15
+ * (mirroring composite inputs like the partial-date widget), so there is no
16
+ * relation-specific help class.
13
17
  */
14
18
 
15
19
  .header,
@@ -20,13 +24,6 @@
20
24
  margin-bottom: 0.25rem;
21
25
  }
22
26
 
23
- .help,
24
- :global(.byline-field-relation-help) {
25
- margin-bottom: 0.25rem;
26
- color: var(--gray-400);
27
- font-size: var(--font-size-xs);
28
- }
29
-
30
27
  .error-tile,
31
28
  :global(.byline-field-relation-error-tile) {
32
29
  display: flex;
@@ -162,3 +159,36 @@
162
159
  display: flex;
163
160
  margin-top: 0.25rem;
164
161
  }
162
+
163
+ /* ---------------------------------------------------------------------------
164
+ * Frame — rounded border around the field's interactive body (tiles and/or
165
+ * buttons), rendered BELOW the label / help text, mirroring the framing
166
+ * convention of composite inputs like the partial-date widget. Bordered with
167
+ * `--field-border`, the same theme-aware token the Input / Select widgets
168
+ * use, so it matches them and flips automatically between light and dark.
169
+ *
170
+ * Override handle:
171
+ * .byline-field-relation-frame — the framing box
172
+ * --------------------------------------------------------------------------- */
173
+
174
+ .frame,
175
+ :global(.byline-field-relation-frame) {
176
+ margin-top: 0.25rem;
177
+ border: var(--border-width-thin) var(--border-style-solid) var(--field-border);
178
+ border-radius: var(--border-radius-md, 6px);
179
+ padding: 0.5rem;
180
+ }
181
+
182
+ /* The frame now supplies the label → content separation, so zero the legacy
183
+ top margins on children rendered inside it. */
184
+ .frame > .tile,
185
+ .frame > .many,
186
+ :global(.byline-field-relation-frame > .byline-field-relation-tile),
187
+ :global(.byline-field-relation-frame > .byline-field-relation-many) {
188
+ margin-top: 0;
189
+ }
190
+
191
+ .frame > .many-empty,
192
+ :global(.byline-field-relation-frame > .byline-field-relation-many-empty) {
193
+ margin-top: 0;
194
+ }
@@ -16,7 +16,15 @@ import type {
16
16
  } from '@byline/core'
17
17
  import { getCollectionAdminConfig, getCollectionDefinition } from '@byline/core'
18
18
  import { useTranslation } from '@byline/i18n/react'
19
- import { Button, CloseIcon, EditIcon, ErrorText, IconButton, Label } from '@byline/ui/react'
19
+ import {
20
+ Button,
21
+ CloseIcon,
22
+ EditIcon,
23
+ ErrorText,
24
+ HelpText,
25
+ IconButton,
26
+ Label,
27
+ } from '@byline/ui/react'
20
28
  import cx from 'classnames'
21
29
 
22
30
  import { useFieldError, useFieldValue } from '../../forms/form-context'
@@ -130,10 +138,6 @@ export const RelationField = ({
130
138
  required={!field.optional}
131
139
  />
132
140
  </div>
133
- {field.helpText && (
134
- <div className={cx('byline-field-relation-help', styles.help)}>{field.helpText}</div>
135
- )}
136
-
137
141
  {isUnknown ? (
138
142
  <div className={cx('byline-field-relation-error-tile', styles['error-tile'])}>
139
143
  <span>
@@ -146,54 +150,62 @@ export const RelationField = ({
146
150
  {t('fields.relation.unknownHint')}
147
151
  </span>
148
152
  </div>
149
- ) : incomingValue ? (
150
- <div className={cx('byline-field-relation-tile', styles.tile)}>
151
- <RelationSummary
152
- targetDefinition={targetDef}
153
- targetAdminConfig={targetAdminConfig}
154
- displayField={field.displayField}
155
- value={incomingValue}
156
- cachedRecord={cachedRecord}
157
- />
158
- <div className={cx('byline-field-relation-actions', styles.actions)}>
159
- <IconButton
153
+ ) : (
154
+ // Rounded frame below the label wrapping the interactive body
155
+ // (selected tile or select button) — see `.frame` in the CSS module.
156
+ <div className={cx('byline-field-relation-frame', styles.frame)}>
157
+ {incomingValue ? (
158
+ <div className={cx('byline-field-relation-tile', styles.tile)}>
159
+ <RelationSummary
160
+ targetDefinition={targetDef}
161
+ targetAdminConfig={targetAdminConfig}
162
+ displayField={field.displayField}
163
+ value={incomingValue}
164
+ cachedRecord={cachedRecord}
165
+ />
166
+ <div className={cx('byline-field-relation-actions', styles.actions)}>
167
+ <IconButton
168
+ id={htmlId}
169
+ type="button"
170
+ intent="noeffect"
171
+ size="xs"
172
+ aria-label={t('fields.relation.changeAriaLabel', {
173
+ label: targetDef.labels.singular,
174
+ })}
175
+ onClick={() => setPickerOpen(true)}
176
+ >
177
+ <EditIcon width="15px" height="15px" />
178
+ </IconButton>
179
+ <IconButton
180
+ type="button"
181
+ intent="noeffect"
182
+ size="xs"
183
+ aria-label={t('fields.relation.removeAriaLabel', {
184
+ label: targetDef.labels.singular,
185
+ })}
186
+ onClick={handleRemove}
187
+ >
188
+ <CloseIcon width="15px" height="15px" />
189
+ </IconButton>
190
+ </div>
191
+ </div>
192
+ ) : (
193
+ <Button
160
194
  id={htmlId}
161
- type="button"
162
- intent="noeffect"
163
195
  size="xs"
164
- aria-label={t('fields.relation.changeAriaLabel', {
165
- label: targetDef.labels.singular,
166
- })}
167
- onClick={() => setPickerOpen(true)}
168
- >
169
- <EditIcon width="15px" height="15px" />
170
- </IconButton>
171
- <IconButton
172
- type="button"
196
+ variant="outlined"
173
197
  intent="noeffect"
174
- size="xs"
175
- aria-label={t('fields.relation.removeAriaLabel', {
176
- label: targetDef.labels.singular,
177
- })}
178
- onClick={handleRemove}
198
+ type="button"
199
+ onClick={() => setPickerOpen(true)}
179
200
  >
180
- <CloseIcon width="15px" height="15px" />
181
- </IconButton>
182
- </div>
201
+ {t('fields.relation.selectButton', { label: targetDef.labels.singular })}
202
+ </Button>
203
+ )}
183
204
  </div>
184
- ) : (
185
- <Button
186
- id={htmlId}
187
- size="xs"
188
- variant="outlined"
189
- intent="noeffect"
190
- type="button"
191
- onClick={() => setPickerOpen(true)}
192
- >
193
- {t('fields.relation.selectButton', { label: targetDef.labels.singular })}
194
- </Button>
195
205
  )}
196
206
 
207
+ {field.helpText != null && <HelpText text={field.helpText} />}
208
+
197
209
  {fieldError && <ErrorText id={`${field.name}-error`} text={fieldError} />}
198
210
 
199
211
  {!isUnknown && (
@@ -22,6 +22,7 @@ import {
22
22
  DraggableSortable,
23
23
  ErrorText,
24
24
  GripperVerticalIcon,
25
+ HelpText,
25
26
  IconButton,
26
27
  Label,
27
28
  moveItem,
@@ -201,10 +202,6 @@ export const RelationManyField = ({ field, defaultValue, id, path }: RelationMan
201
202
  required={!field.optional}
202
203
  />
203
204
  </div>
204
- {field.helpText && (
205
- <div className={cx('byline-field-relation-help', styles.help)}>{field.helpText}</div>
206
- )}
207
-
208
205
  {isUnknown ? (
209
206
  <div className={cx('byline-field-relation-error-tile', styles['error-tile'])}>
210
207
  <span>
@@ -219,51 +216,58 @@ export const RelationManyField = ({ field, defaultValue, id, path }: RelationMan
219
216
  </div>
220
217
  ) : (
221
218
  <>
222
- {items.length === 0 ? (
223
- <p className={cx('byline-field-relation-many-empty', styles['many-empty'])}>
224
- {t('fields.relation.manyEmpty', { label: targetDef.labels.plural })}
225
- </p>
226
- ) : (
227
- <DraggableSortable
228
- ids={items.map((v) => v.targetDocumentId)}
229
- onDragEnd={handleDragEnd}
230
- className={cx('byline-field-relation-many', styles.many)}
231
- >
232
- {items.map((value) => (
233
- <RelationManyTile
234
- key={value.targetDocumentId}
235
- id={value.targetDocumentId}
236
- onRemove={() => handleRemove(value.targetDocumentId)}
237
- removeAriaLabel={t('fields.relation.removeAriaLabel', { label: targetLabel })}
238
- >
239
- <RelationSummary
240
- targetDefinition={targetDef}
241
- targetAdminConfig={targetAdminConfig}
242
- displayField={field.displayField}
243
- value={value}
244
- cachedRecord={pickedRecords[value.targetDocumentId] ?? null}
245
- />
246
- </RelationManyTile>
247
- ))}
248
- </DraggableSortable>
249
- )}
250
-
251
- {fieldError && <ErrorText id={`${field.name}-error`} text={fieldError} />}
219
+ {/* Rounded frame below the label wrapping the interactive
220
+ body (tile list and add button) — see `.frame` in the CSS
221
+ module. */}
222
+ <div className={cx('byline-field-relation-frame', styles.frame)}>
223
+ {items.length === 0 ? (
224
+ <p className={cx('byline-field-relation-many-empty', styles['many-empty'])}>
225
+ {t('fields.relation.manyEmpty', { label: targetDef.labels.plural })}
226
+ </p>
227
+ ) : (
228
+ <DraggableSortable
229
+ ids={items.map((v) => v.targetDocumentId)}
230
+ onDragEnd={handleDragEnd}
231
+ className={cx('byline-field-relation-many', styles.many)}
232
+ >
233
+ {items.map((value) => (
234
+ <RelationManyTile
235
+ key={value.targetDocumentId}
236
+ id={value.targetDocumentId}
237
+ onRemove={() => handleRemove(value.targetDocumentId)}
238
+ removeAriaLabel={t('fields.relation.removeAriaLabel', { label: targetLabel })}
239
+ >
240
+ <RelationSummary
241
+ targetDefinition={targetDef}
242
+ targetAdminConfig={targetAdminConfig}
243
+ displayField={field.displayField}
244
+ value={value}
245
+ cachedRecord={pickedRecords[value.targetDocumentId] ?? null}
246
+ />
247
+ </RelationManyTile>
248
+ ))}
249
+ </DraggableSortable>
250
+ )}
252
251
 
253
- <div className={cx('byline-field-relation-many-add', styles['many-add'])}>
254
- <Button
255
- id={htmlId}
256
- size="xs"
257
- variant="outlined"
258
- intent="noeffect"
259
- type="button"
260
- onClick={() => setPickerOpen(true)}
261
- >
262
- <PlusIcon width="14px" height="14px" />
263
- {t('fields.relation.addButton', { label: targetLabel })}
264
- </Button>
252
+ <div className={cx('byline-field-relation-many-add', styles['many-add'])}>
253
+ <Button
254
+ id={htmlId}
255
+ size="xs"
256
+ variant="outlined"
257
+ intent="noeffect"
258
+ type="button"
259
+ onClick={() => setPickerOpen(true)}
260
+ >
261
+ <PlusIcon width="14px" height="14px" />
262
+ {t('fields.relation.addButton', { label: targetLabel })}
263
+ </Button>
264
+ </div>
265
265
  </div>
266
266
 
267
+ {field.helpText != null && <HelpText text={field.helpText} />}
268
+
269
+ {fieldError && <ErrorText id={`${field.name}-error`} text={fieldError} />}
270
+
267
271
  <RelationPicker
268
272
  multiple
269
273
  targetCollectionPath={field.targetCollection}