@byline/admin 4.0.0 → 4.2.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 (43) hide show
  1. package/dist/fields/array/array-field.js +20 -8
  2. package/dist/fields/array/array-field.module.js +5 -1
  3. package/dist/fields/array/array-field_module.css +23 -0
  4. package/dist/fields/blocks/blocks-field.js +33 -12
  5. package/dist/fields/blocks/blocks-field.module.js +2 -0
  6. package/dist/fields/blocks/blocks-field_module.css +37 -0
  7. package/dist/fields/code/code-editor.d.ts +20 -0
  8. package/dist/fields/code/code-editor.js +239 -0
  9. package/dist/fields/code/code-field.d.ts +21 -0
  10. package/dist/fields/code/code-field.js +124 -0
  11. package/dist/fields/code/code-field.module.js +7 -0
  12. package/dist/fields/code/code-field_module.css +58 -0
  13. package/dist/fields/field-helpers.js +1 -0
  14. package/dist/fields/field-renderer.js +15 -1
  15. package/dist/fields/group/group-field.d.ts +10 -2
  16. package/dist/fields/group/group-field.js +4 -2
  17. package/dist/fields/relation/relation-picker.js +8 -2
  18. package/dist/fields/select/select-field.js +4 -2
  19. package/dist/fields/select/select-field.module.js +1 -0
  20. package/dist/fields/select/select-field_module.css +4 -0
  21. package/dist/fields/text/text-field_module.css +1 -0
  22. package/dist/fields/text-area/text-area-field_module.css +1 -0
  23. package/dist/forms/form-renderer.d.ts +1 -1
  24. package/dist/forms/tree-placement-widget.d.ts +1 -1
  25. package/package.json +18 -5
  26. package/src/fields/array/array-field.module.css +30 -0
  27. package/src/fields/array/array-field.tsx +15 -2
  28. package/src/fields/blocks/blocks-field.module.css +53 -0
  29. package/src/fields/blocks/blocks-field.tsx +33 -1
  30. package/src/fields/code/code-editor.tsx +246 -0
  31. package/src/fields/code/code-field.module.css +84 -0
  32. package/src/fields/code/code-field.tsx +191 -0
  33. package/src/fields/field-helpers.ts +1 -0
  34. package/src/fields/field-renderer.tsx +19 -2
  35. package/src/fields/field-services-types.ts +1 -1
  36. package/src/fields/group/group-field.tsx +12 -1
  37. package/src/fields/relation/relation-picker.tsx +11 -0
  38. package/src/fields/select/select-field.module.css +5 -0
  39. package/src/fields/select/select-field.tsx +9 -2
  40. package/src/fields/text/text-field.module.css +1 -0
  41. package/src/fields/text-area/text-area-field.module.css +1 -0
  42. package/src/forms/form-renderer.tsx +1 -1
  43. package/src/forms/tree-placement-widget.tsx +1 -1
@@ -0,0 +1,58 @@
1
+ :is(.label-row-bNilI8, .byline-field-code-label-row) {
2
+ align-items: center;
3
+ margin-bottom: .25rem;
4
+ display: flex;
5
+ }
6
+
7
+ .byline-field-code {
8
+ --byline-code-bg: var(--canvas-50, #fafafa);
9
+ --byline-code-fg: var(--canvas-950, #1c1c1e);
10
+ --byline-code-border: var(--canvas-300, #d6d6d6);
11
+ --byline-code-gutter-bg: var(--canvas-100, #f2f2f2);
12
+ --byline-code-gutter-fg: var(--canvas-500, #8b8b90);
13
+ --byline-code-active-line: #0000000a;
14
+ --byline-code-selection: #3b82f62e;
15
+ --byline-code-caret: var(--canvas-950, #1c1c1e);
16
+ --byline-code-focus-ring: #3b82f699;
17
+ --byline-code-keyword: #7c3aed;
18
+ --byline-code-string: #15803d;
19
+ --byline-code-comment: #767680;
20
+ --byline-code-number: #c2410c;
21
+ --byline-code-function: #1d4ed8;
22
+ --byline-code-type: #0f766e;
23
+ --byline-code-property: #a16207;
24
+ --byline-code-operator: #52525b;
25
+ }
26
+
27
+ :is([data-theme="dark"], .dark) .byline-field-code {
28
+ --byline-code-bg: var(--canvas-900, #1b1b1f);
29
+ --byline-code-fg: var(--canvas-100, #e6e6ea);
30
+ --byline-code-border: var(--canvas-700, #3f3f46);
31
+ --byline-code-gutter-bg: var(--canvas-800, #232327);
32
+ --byline-code-gutter-fg: var(--canvas-500, #77777f);
33
+ --byline-code-active-line: #ffffff0d;
34
+ --byline-code-selection: #60a5fa47;
35
+ --byline-code-caret: var(--canvas-100, #e6e6ea);
36
+ --byline-code-focus-ring: #60a5fa8c;
37
+ --byline-code-keyword: #c4b5fd;
38
+ --byline-code-string: #86efac;
39
+ --byline-code-comment: #8e8e96;
40
+ --byline-code-number: #fdba74;
41
+ --byline-code-function: #93c5fd;
42
+ --byline-code-type: #5eead4;
43
+ --byline-code-property: #fde047;
44
+ --byline-code-operator: #a1a1aa;
45
+ }
46
+
47
+ :is(.loading-sejBT6, .byline-field-code-loading) {
48
+ resize: none;
49
+ width: 100%;
50
+ font-family: var(--byline-code-font, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
51
+ font-size: var(--byline-code-font-size, 13px);
52
+ color: var(--byline-code-fg);
53
+ background-color: var(--byline-code-bg);
54
+ border: 1px solid var(--byline-code-border);
55
+ border-radius: var(--byline-code-radius, 4px);
56
+ padding: .5rem;
57
+ }
58
+
@@ -19,6 +19,7 @@ const placeholderForField = (f)=>{
19
19
  switch(f.type){
20
20
  case 'text':
21
21
  case 'textArea':
22
+ case 'code':
22
23
  return '';
23
24
  case 'checkbox':
24
25
  return false;
@@ -5,6 +5,7 @@ import { useFormContext } from "../forms/form-context.js";
5
5
  import { ArrayField } from "./array/array-field.js";
6
6
  import { BlocksField } from "./blocks/blocks-field.js";
7
7
  import { CheckboxField } from "./checkbox/checkbox-field.js";
8
+ import { CodeField } from "./code/code-field.js";
8
9
  import { DateTimeField } from "./datetime/datetime-field.js";
9
10
  import field_renderer_module from "./field-renderer.module.js";
10
11
  import { FileField } from "./file/file-field.js";
@@ -60,6 +61,19 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
60
61
  locale: isLocalised ? contentLocale : void 0,
61
62
  components: components
62
63
  });
64
+ case 'code':
65
+ return /*#__PURE__*/ jsx(CodeField, {
66
+ field: hideLabel ? {
67
+ ...field,
68
+ label: void 0
69
+ } : field,
70
+ defaultValue: defaultValue,
71
+ onChange: handleChange,
72
+ path: path,
73
+ id: htmlId,
74
+ locale: isLocalised ? contentLocale : void 0,
75
+ components: components
76
+ });
63
77
  case 'checkbox':
64
78
  return /*#__PURE__*/ jsx(CheckboxField, {
65
79
  field: hideLabel ? {
@@ -214,7 +228,7 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
214
228
  return null;
215
229
  }
216
230
  };
217
- const selfBadge = 'text' === field.type || 'textArea' === field.type || 'richText' === field.type;
231
+ const selfBadge = 'text' === field.type || 'textArea' === field.type || 'code' === field.type || 'richText' === field.type;
218
232
  if (badge && !selfBadge) return /*#__PURE__*/ jsxs("div", {
219
233
  className: classnames('byline-field-localized-wrap', field_renderer_module["localized-wrap"]),
220
234
  children: [
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- import type { GroupField as GroupFieldType } from '@byline/core';
8
+ import type { FieldAdminConfig, GroupField as GroupFieldType } from '@byline/core';
9
9
  interface GroupFieldProps {
10
10
  field: GroupFieldType;
11
11
  defaultValue: any;
@@ -22,6 +22,14 @@ interface GroupFieldProps {
22
22
  * locale badge.
23
23
  */
24
24
  contentLocale?: string;
25
+ /**
26
+ * Per-child-field admin overrides (`components` slots, richtext `editor`),
27
+ * keyed by child field name. Threaded by `BlocksField` from the site-wide
28
+ * `ClientConfig.blockAdmin` registry so block children can take per-field
29
+ * admin config; plain groups receive none today (their children inherit
30
+ * site-wide defaults).
31
+ */
32
+ fieldAdmin?: Record<string, FieldAdminConfig>;
25
33
  }
26
- export declare const GroupField: ({ field, defaultValue, path, collectionPath, contentLocale, }: GroupFieldProps) => import("react").JSX.Element;
34
+ export declare const GroupField: ({ field, defaultValue, path, collectionPath, contentLocale, fieldAdmin, }: GroupFieldProps) => import("react").JSX.Element;
27
35
  export {};
@@ -6,7 +6,7 @@ import { placeholderForField } from "../field-helpers.js";
6
6
  import { FieldRenderer } from "../field-renderer.js";
7
7
  import { useFieldError } from "../../forms/form-context.js";
8
8
  import group_field_module from "./group-field.module.js";
9
- const GroupField = ({ field, defaultValue, path, collectionPath, contentLocale })=>{
9
+ const GroupField = ({ field, defaultValue, path, collectionPath, contentLocale, fieldAdmin })=>{
10
10
  const fieldError = useFieldError(field.name);
11
11
  const groupData = useMemo(()=>{
12
12
  if (defaultValue && 'object' == typeof defaultValue && !Array.isArray(defaultValue)) return defaultValue;
@@ -48,7 +48,9 @@ const GroupField = ({ field, defaultValue, path, collectionPath, contentLocale }
48
48
  basePath: path,
49
49
  disableSorting: true,
50
50
  collectionPath: collectionPath,
51
- contentLocale: contentLocale
51
+ contentLocale: contentLocale,
52
+ components: fieldAdmin?.[innerField.name]?.components,
53
+ editor: fieldAdmin?.[innerField.name]?.editor
52
54
  }, innerField.name))
53
55
  }),
54
56
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
@@ -39,13 +39,18 @@ const RelationPicker = ({ targetCollectionPath, targetDefinition, displayField,
39
39
  const selectFields = resolveSelectFields(targetDefinition, displayField, pickerColumns, extraSelectFields);
40
40
  setLoading(true);
41
41
  setError(null);
42
+ const itemViewSort = targetAdminConfig?.itemViewSort;
42
43
  getCollectionDocuments({
43
44
  collection: targetCollectionPath,
44
45
  params: {
45
46
  page,
46
47
  page_size: PAGE_SIZE,
47
48
  query: query.length > 0 ? query : void 0,
48
- fields: selectFields
49
+ fields: selectFields,
50
+ ...null != itemViewSort ? {
51
+ order: String(itemViewSort.field),
52
+ desc: 'desc' === itemViewSort.direction
53
+ } : {}
49
54
  }
50
55
  }).then((response)=>{
51
56
  if (cancelled) return;
@@ -71,7 +76,8 @@ const RelationPicker = ({ targetCollectionPath, targetDefinition, displayField,
71
76
  targetDefinition,
72
77
  pickerColumns,
73
78
  getCollectionDocuments,
74
- t
79
+ t,
80
+ targetAdminConfig?.itemViewSort
75
81
  ]);
76
82
  const resolvedDisplayField = displayField ?? targetDefinition?.useAsTitle ?? resolveFallbackDisplayField(targetDefinition) ?? null;
77
83
  const handleSelect = useCallback(()=>{
@@ -17,10 +17,12 @@ const SelectField = ({ field, value, defaultValue, onChange, id, path })=>{
17
17
  id: htmlId,
18
18
  htmlFor: htmlId,
19
19
  label: field.label,
20
- required: !field.optional
20
+ required: !field.optional,
21
+ className: classnames('byline-field-select-label', select_field_module.label)
21
22
  }),
22
23
  /*#__PURE__*/ jsx(Select, {
23
- size: "sm",
24
+ size: "xs",
25
+ variant: "outlined",
24
26
  id: htmlId,
25
27
  name: field.name,
26
28
  placeholder: "Select an option",
@@ -1,5 +1,6 @@
1
1
  import "./select-field_module.css";
2
2
  const select_field_module = {
3
+ label: "label-ScZ_3S",
3
4
  dirty: "dirty-aKaGZN"
4
5
  };
5
6
  export default select_field_module;
@@ -1,3 +1,7 @@
1
+ :is(.label-ScZ_3S, .byline-field-select-label) {
2
+ margin-bottom: .25rem;
3
+ }
4
+
1
5
  :is(.dirty-aKaGZN, .byline-field-select-dirty) {
2
6
  border-color: var(--blue-300);
3
7
  }
@@ -1,5 +1,6 @@
1
1
  :is(.label-row-sjBuRb, .byline-field-text-label-row) {
2
2
  align-items: center;
3
+ margin-bottom: .25rem;
3
4
  display: flex;
4
5
  }
5
6
 
@@ -1,5 +1,6 @@
1
1
  :is(.label-row-HwX7C9, .byline-field-text-area-label-row) {
2
2
  align-items: center;
3
+ margin-bottom: .25rem;
3
4
  display: flex;
4
5
  }
5
6
 
@@ -104,7 +104,7 @@ export interface FormRendererProps {
104
104
  * Opts the document-tree placement widget into the sidebar (above the
105
105
  * available-locales widget). Sourced from `CollectionDefinition.tree` by the
106
106
  * caller. Renders only in edit mode (placement needs a persisted document)
107
- * and only when the host wires the tree services. See docs/04-collections/03-document-trees.md.
107
+ * and only when the host wires the tree services. See docs/04-collections/04-document-trees.md.
108
108
  */
109
109
  tree?: boolean;
110
110
  headingLabel?: string;
@@ -8,7 +8,7 @@ export interface TreePlacementWidgetProps {
8
8
  }
9
9
  /**
10
10
  * Sidebar widget for placing the current document within its collection's
11
- * single-parent document tree (the `tree: true` primitive — docs/04-collections/03-document-trees.md).
11
+ * single-parent document tree (the `tree: true` primitive — docs/04-collections/04-document-trees.md).
12
12
  *
13
13
  * The tree is document-grain and **unversioned**, so changes here write
14
14
  * immediately (independent of the form's content save). The editor picks a
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": "4.0.0",
5
+ "version": "4.2.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -149,16 +149,29 @@
149
149
  "src/"
150
150
  ],
151
151
  "dependencies": {
152
+ "@codemirror/commands": "^6.10.4",
153
+ "@codemirror/lang-css": "^6.3.1",
154
+ "@codemirror/lang-html": "^6.4.11",
155
+ "@codemirror/lang-javascript": "^6.2.5",
156
+ "@codemirror/lang-json": "^6.0.2",
157
+ "@codemirror/lang-markdown": "^6.5.1",
158
+ "@codemirror/lang-python": "^6.2.1",
159
+ "@codemirror/lang-sql": "^6.10.0",
160
+ "@codemirror/lang-yaml": "^6.1.3",
161
+ "@codemirror/language": "^6.12.4",
162
+ "@codemirror/state": "^6.7.1",
163
+ "@codemirror/view": "^6.43.6",
164
+ "@lezer/highlight": "^1.2.3",
152
165
  "@tanstack/react-form-start": "^1.33.0",
153
166
  "classnames": "^2.5.1",
154
167
  "jose": "^6.2.3",
155
168
  "react-diff-viewer-continued": "^4.2.2",
156
169
  "uuid": "^14.0.1",
157
170
  "zod": "^4.4.3",
158
- "@byline/auth": "4.0.0",
159
- "@byline/core": "4.0.0",
160
- "@byline/ui": "4.0.0",
161
- "@byline/i18n": "4.0.0"
171
+ "@byline/core": "4.2.0",
172
+ "@byline/i18n": "4.2.0",
173
+ "@byline/auth": "4.2.0",
174
+ "@byline/ui": "4.2.0"
162
175
  },
163
176
  "peerDependencies": {
164
177
  "react": "^19.0.0",
@@ -5,6 +5,8 @@
5
5
  * .byline-field-array — outer wrapper
6
6
  * .byline-field-array-title — array label heading
7
7
  * .byline-field-array-stack — vertical layout for items + add row
8
+ * .byline-field-array-add-row — add-button row at the bottom
9
+ * .byline-field-array-add-label — clickable text label in the add row
8
10
  * .byline-field-array-card — disable-sorting fallback card per item
9
11
  * .byline-field-array-group-header — sub-group title above grouped fields
10
12
  * .byline-field-array-group-fields — vertical stack of fields inside an item
@@ -46,3 +48,31 @@
46
48
  flex-direction: column;
47
49
  gap: var(--spacing-16);
48
50
  }
51
+
52
+ .add-row,
53
+ :global(.byline-field-array-add-row) {
54
+ display: flex;
55
+ align-items: center;
56
+ font-size: 0.9rem;
57
+ color: var(--muted);
58
+ gap: var(--spacing-8);
59
+ }
60
+
61
+ /* Text-styled companion button for the add row's label — clickable like the
62
+ icon button. Hovering anywhere on the row brightens the label text. */
63
+ .add-label,
64
+ :global(.byline-field-array-add-label) {
65
+ appearance: none;
66
+ background: none;
67
+ border: none;
68
+ padding: 0;
69
+ font: inherit;
70
+ color: inherit;
71
+ cursor: pointer;
72
+ transition: color 120ms ease;
73
+ }
74
+
75
+ .add-row:hover .add-label,
76
+ :global(.byline-field-array-add-row):hover :global(.byline-field-array-add-label) {
77
+ color: var(--text);
78
+ }
@@ -290,7 +290,7 @@ export const ArrayField = ({
290
290
  className={cx('byline-field-array-stack', styles.stack)}
291
291
  >
292
292
  {items.map((item, index) => renderItem(item, index))}
293
- <span>
293
+ <div className={cx('byline-field-array-add-row', styles['add-row'])}>
294
294
  <IconButton
295
295
  onClick={() => {
296
296
  void handleAddItem()
@@ -299,7 +299,20 @@ export const ArrayField = ({
299
299
  >
300
300
  <PlusIcon />
301
301
  </IconButton>
302
- </span>
302
+ {/* Text-styled button so the label is clickable too. Removed from
303
+ the tab order (tabIndex -1) — the IconButton is the single
304
+ keyboard/screen-reader control for this action. */}
305
+ <button
306
+ type="button"
307
+ tabIndex={-1}
308
+ onClick={() => {
309
+ void handleAddItem()
310
+ }}
311
+ className={cx('byline-field-array-add-label', styles['add-label'])}
312
+ >
313
+ {t('fields.array.addItem')}
314
+ </button>
315
+ </div>
303
316
  </DraggableSortable>
304
317
  )}
305
318
  </div>
@@ -6,6 +6,7 @@
6
6
  * .byline-field-blocks-title — block-list label heading
7
7
  * .byline-field-blocks-stack — vertical stack of blocks + add row
8
8
  * .byline-field-blocks-add-row — add-button row at the bottom
9
+ * .byline-field-blocks-add-label — clickable text label in the add row
9
10
  * .byline-field-blocks-modal-head — picker modal header
10
11
  * .byline-field-blocks-modal-title — picker modal title
11
12
  * .byline-field-blocks-grid — grid of variant cards
@@ -36,9 +37,35 @@
36
37
  :global(.byline-field-blocks-add-row) {
37
38
  display: flex;
38
39
  align-items: center;
40
+ font-size: 0.9rem;
41
+ color: var(--muted);
39
42
  gap: var(--spacing-8);
40
43
  }
41
44
 
45
+ /* Text-styled companion button for the add row's label — clickable like the
46
+ icon button. Hovering anywhere on the row brightens the label text. */
47
+ .add-label,
48
+ :global(.byline-field-blocks-add-label) {
49
+ appearance: none;
50
+ background: none;
51
+ border: none;
52
+ padding: 0;
53
+ font: inherit;
54
+ color: inherit;
55
+ cursor: pointer;
56
+ transition: color 120ms ease;
57
+ }
58
+
59
+ .add-label:disabled,
60
+ :global(.byline-field-blocks-add-label):disabled {
61
+ cursor: default;
62
+ }
63
+
64
+ .add-row:hover .add-label:not(:disabled),
65
+ :global(.byline-field-blocks-add-row):hover :global(.byline-field-blocks-add-label):not(:disabled) {
66
+ color: var(--text);
67
+ }
68
+
42
69
  .modal-head,
43
70
  :global(.byline-field-blocks-modal-head) {
44
71
  padding-top: 1rem;
@@ -54,6 +81,32 @@
54
81
  .modal-content,
55
82
  :global(.byline-field-blocks-card-cursor) {
56
83
  cursor: pointer;
84
+ /* Cap the picker's height and scroll the card grid when a collection
85
+ declares more blocks than fit. Mobile-first: 80% of the (dynamic)
86
+ viewport height, tightening to 70% on tablet and 60% on desktop —
87
+ smaller screens get proportionally more of the picker. `min-height: 0`
88
+ lets this flex child (Modal.Content is a flex column item) actually
89
+ shrink and scroll; `overscroll-behavior` keeps wheel momentum from
90
+ spilling into the document behind the modal. All caps stay inside the
91
+ Modal.Container's own viewport-height limit. */
92
+ max-height: 80dvh;
93
+ min-height: 0;
94
+ overflow-y: auto;
95
+ overscroll-behavior: contain;
96
+ }
97
+
98
+ @media (min-width: 40rem) {
99
+ .modal-content,
100
+ :global(.byline-field-blocks-card-cursor) {
101
+ max-height: 70dvh;
102
+ }
103
+ }
104
+
105
+ @media (min-width: 64rem) {
106
+ .modal-content,
107
+ :global(.byline-field-blocks-card-cursor) {
108
+ max-height: 60dvh;
109
+ }
57
110
  }
58
111
 
59
112
  .grid,
@@ -9,10 +9,12 @@
9
9
  import { useEffect, useMemo, useState } from 'react'
10
10
 
11
11
  import type {
12
+ BlockAdminConfig,
12
13
  BlocksField as BlocksFieldType,
13
14
  Field,
14
15
  GroupField as GroupFieldType,
15
16
  } from '@byline/core'
17
+ import { getClientConfig } from '@byline/core'
16
18
  import { useTranslation } from '@byline/i18n/react'
17
19
  import {
18
20
  Card,
@@ -59,6 +61,19 @@ export const BlocksField = ({
59
61
  const [pendingInsertIndex, setPendingInsertIndex] = useState<number | null>(null)
60
62
 
61
63
  const availableBlocks = useMemo(() => field.blocks ?? [], [field.blocks])
64
+
65
+ // Site-wide per-block admin config (`ClientConfig.blockAdmin`), resolved by
66
+ // blockType. Registry-based on purpose: blocks are cross-collection units,
67
+ // so their admin config applies wherever the block renders — no threading
68
+ // from the collection admin config is needed (mirrors how FieldRenderer
69
+ // resolves the global richText editor from client config).
70
+ const blockAdminByType = useMemo(() => {
71
+ const map = new Map<string, BlockAdminConfig>()
72
+ for (const entry of getClientConfig().blockAdmin ?? []) {
73
+ map.set(entry.blockType, entry)
74
+ }
75
+ return map
76
+ }, [])
62
77
  const [selectedBlockName, setSelectedBlockName] = useState<string>(
63
78
  () => availableBlocks[0]?.blockType ?? ''
64
79
  )
@@ -235,6 +250,7 @@ export const BlocksField = ({
235
250
  defaultValue={fieldData}
236
251
  path={arrayElementPath}
237
252
  contentLocale={contentLocale}
253
+ fieldAdmin={blockAdminByType.get(item._type)?.fields}
238
254
  />
239
255
  )
240
256
 
@@ -270,9 +286,25 @@ export const BlocksField = ({
270
286
  }}
271
287
  disabled={!selectedBlockName}
272
288
  aria-label={t('fields.blocks.addBlockAriaLabel')}
289
+ variant="outlined"
273
290
  >
274
291
  <PlusIcon />
275
292
  </IconButton>
293
+ {/* Text-styled button so the label is clickable too. Removed from
294
+ the tab order (tabIndex -1) — the IconButton is the single
295
+ keyboard/screen-reader control for this action. */}
296
+ <button
297
+ type="button"
298
+ tabIndex={-1}
299
+ disabled={!selectedBlockName}
300
+ onClick={() => {
301
+ setPendingInsertIndex(null)
302
+ setShowAddBlockModal(true)
303
+ }}
304
+ className={cx('byline-field-blocks-add-label', styles['add-label'])}
305
+ >
306
+ {t('fields.blocks.addBlock')}
307
+ </button>
276
308
  </div>
277
309
  </DraggableSortable>
278
310
  <Modal
@@ -283,7 +315,7 @@ export const BlocksField = ({
283
315
  setPendingInsertIndex(null)
284
316
  }}
285
317
  >
286
- <Modal.Container style={{ maxWidth: '600px' }}>
318
+ <Modal.Container style={{ maxWidth: '700px' }}>
287
319
  <Modal.Header className={cx('byline-field-blocks-modal-head', styles['modal-head'])}>
288
320
  <h3 className={cx('byline-field-blocks-modal-title', styles['modal-title'])}>
289
321
  {t('fields.blocks.modalTitle')}