@byline/richtext-lexical 2.6.0 → 2.7.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.
- package/dist/field/editor-field.js +3 -1
- package/dist/field/extensions/inline-image/inline-image-modal.css +39 -13
- package/dist/field/extensions/inline-image/inline-image-modal.d.ts +1 -0
- package/dist/field/extensions/inline-image/inline-image-modal.js +41 -17
- package/dist/lexical-editor.js +3 -1
- package/package.json +5 -5
- package/src/field/editor-field.tsx +1 -1
- package/src/field/extensions/inline-image/inline-image-modal.css +46 -16
- package/src/field/extensions/inline-image/inline-image-modal.tsx +57 -25
- package/src/lexical-editor.tsx +1 -1
|
@@ -8,7 +8,9 @@ const EditorComponent = /*#__PURE__*/ lazy(()=>import("./editor-component.js").t
|
|
|
8
8
|
function EditorField(props) {
|
|
9
9
|
return /*#__PURE__*/ jsx(Suspense, {
|
|
10
10
|
fallback: /*#__PURE__*/ jsx(Shimmer, {
|
|
11
|
-
|
|
11
|
+
variant: "text",
|
|
12
|
+
lines: 20,
|
|
13
|
+
lineHeight: "1.15rem"
|
|
12
14
|
}),
|
|
13
15
|
children: /*#__PURE__*/ jsx(EditorComponent, {
|
|
14
16
|
...props
|
|
@@ -1,25 +1,51 @@
|
|
|
1
|
-
.inline-image-
|
|
2
|
-
|
|
1
|
+
.inline-image-modal-picker {
|
|
2
|
+
align-items: center;
|
|
3
|
+
gap: .75rem;
|
|
4
|
+
display: flex;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.inline-image-modal-thumb, .inline-image-modal-thumb-placeholder {
|
|
8
|
+
border: 1px solid var(--border-color);
|
|
9
|
+
border-radius: .25rem;
|
|
10
|
+
flex-shrink: 0;
|
|
11
|
+
width: 8rem;
|
|
12
|
+
height: 8rem;
|
|
3
13
|
}
|
|
4
14
|
|
|
5
|
-
.inline-image-
|
|
6
|
-
|
|
15
|
+
.inline-image-modal-thumb {
|
|
16
|
+
object-fit: cover;
|
|
7
17
|
}
|
|
8
18
|
|
|
9
|
-
.inline-image-
|
|
10
|
-
|
|
19
|
+
.inline-image-modal-thumb-placeholder {
|
|
20
|
+
background-color: var(--surface-subtle);
|
|
21
|
+
color: var(--text-subtle);
|
|
22
|
+
justify-content: center;
|
|
23
|
+
align-items: center;
|
|
24
|
+
font-size: .75rem;
|
|
25
|
+
display: flex;
|
|
11
26
|
}
|
|
12
27
|
|
|
13
|
-
.inline-image-
|
|
14
|
-
|
|
28
|
+
.inline-image-modal-picker-details {
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
flex: 1;
|
|
31
|
+
align-items: flex-start;
|
|
32
|
+
gap: .5rem;
|
|
33
|
+
min-width: 0;
|
|
34
|
+
display: flex;
|
|
15
35
|
}
|
|
16
36
|
|
|
17
|
-
.inline-image-
|
|
18
|
-
|
|
37
|
+
.inline-image-modal-change-btn {
|
|
38
|
+
flex-shrink: 0;
|
|
39
|
+
min-width: 70px;
|
|
19
40
|
}
|
|
20
41
|
|
|
21
|
-
.inline-image-
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
.inline-image-modal-title {
|
|
43
|
+
white-space: nowrap;
|
|
44
|
+
text-overflow: ellipsis;
|
|
45
|
+
min-width: 0;
|
|
46
|
+
color: var(--text-subtle);
|
|
47
|
+
align-self: stretch;
|
|
48
|
+
font-size: .875rem;
|
|
49
|
+
overflow: hidden;
|
|
24
50
|
}
|
|
25
51
|
|
|
@@ -7,6 +7,7 @@ import { Button, Checkbox, CloseIcon, ErrorText, IconButton, Input, Label, Modal
|
|
|
7
7
|
import { useModalFormState } from "../../shared/useModalFormState.js";
|
|
8
8
|
import { isAltTextValid, positionOptions } from "./fields.js";
|
|
9
9
|
import { deriveImageSizes, getPreferredSize } from "./utils.js";
|
|
10
|
+
import "./inline-image-modal.css";
|
|
10
11
|
function emptyState() {
|
|
11
12
|
return {
|
|
12
13
|
documentRelation: null,
|
|
@@ -24,6 +25,22 @@ function fromInlineImageData(data) {
|
|
|
24
25
|
showCaption: data.showCaption ?? false
|
|
25
26
|
};
|
|
26
27
|
}
|
|
28
|
+
const LABEL_FIELD_KEYS = [
|
|
29
|
+
'title',
|
|
30
|
+
'name',
|
|
31
|
+
'subject',
|
|
32
|
+
'label'
|
|
33
|
+
];
|
|
34
|
+
function deriveLabel(fields) {
|
|
35
|
+
for (const key of LABEL_FIELD_KEYS){
|
|
36
|
+
const value = fields[key];
|
|
37
|
+
if ('string' == typeof value && value.trim().length > 0) return value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const PICKER_EXTRA_FIELDS = [
|
|
41
|
+
'altText',
|
|
42
|
+
...LABEL_FIELD_KEYS
|
|
43
|
+
];
|
|
27
44
|
const InlineImageModal = ({ isOpen, collection, data: dataFromProps, onSubmit, onClose })=>{
|
|
28
45
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
29
46
|
const [altError, setAltError] = useState(null);
|
|
@@ -58,7 +75,8 @@ const InlineImageModal = ({ isOpen, collection, data: dataFromProps, onSubmit, o
|
|
|
58
75
|
const fields = selection.record?.fields ?? {};
|
|
59
76
|
const image = fields.image;
|
|
60
77
|
const title = 'string' == typeof fields.title ? fields.title : void 0;
|
|
61
|
-
const altTextFromMedia = 'string' == typeof fields.altText ? fields.altText : void 0;
|
|
78
|
+
const altTextFromMedia = 'string' == typeof fields.altText && fields.altText.trim().length > 0 ? fields.altText : void 0;
|
|
79
|
+
const derivedLabel = deriveLabel(fields);
|
|
62
80
|
const sizes = image ? deriveImageSizes(image) : [];
|
|
63
81
|
setState((s)=>{
|
|
64
82
|
const document = {};
|
|
@@ -74,7 +92,7 @@ const InlineImageModal = ({ isOpen, collection, data: dataFromProps, onSubmit, o
|
|
|
74
92
|
targetCollectionPath: collection,
|
|
75
93
|
document: Object.keys(document).length > 0 ? document : void 0
|
|
76
94
|
},
|
|
77
|
-
altText: s.altText.length > 0 ? s.altText : altTextFromMedia ?? ''
|
|
95
|
+
altText: s.altText.length > 0 ? s.altText : altTextFromMedia ?? derivedLabel ?? ''
|
|
78
96
|
};
|
|
79
97
|
});
|
|
80
98
|
setImageError(null);
|
|
@@ -139,28 +157,33 @@ const InlineImageModal = ({ isOpen, collection, data: dataFromProps, onSubmit, o
|
|
|
139
157
|
children: "Image"
|
|
140
158
|
}),
|
|
141
159
|
/*#__PURE__*/ jsxs("div", {
|
|
142
|
-
className: "
|
|
160
|
+
className: "inline-image-modal-picker",
|
|
143
161
|
children: [
|
|
144
162
|
pickedThumbUrl ? /*#__PURE__*/ jsx("img", {
|
|
145
163
|
src: pickedThumbUrl,
|
|
146
164
|
alt: pickedTitle ?? '',
|
|
147
|
-
className: "
|
|
165
|
+
className: "inline-image-modal-thumb"
|
|
148
166
|
}) : /*#__PURE__*/ jsx("div", {
|
|
149
|
-
className: "
|
|
167
|
+
className: "inline-image-modal-thumb-placeholder",
|
|
150
168
|
children: "—"
|
|
151
169
|
}),
|
|
152
|
-
/*#__PURE__*/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
170
|
+
/*#__PURE__*/ jsxs("div", {
|
|
171
|
+
className: "inline-image-modal-picker-details",
|
|
172
|
+
children: [
|
|
173
|
+
pickedTitle && /*#__PURE__*/ jsx("span", {
|
|
174
|
+
className: "inline-image-modal-title",
|
|
175
|
+
children: pickedTitle
|
|
176
|
+
}),
|
|
177
|
+
/*#__PURE__*/ jsx(Button, {
|
|
178
|
+
size: "sm",
|
|
179
|
+
className: "inline-image-modal-change-btn",
|
|
180
|
+
variant: "outlined",
|
|
181
|
+
intent: "noeffect",
|
|
182
|
+
type: "button",
|
|
183
|
+
onClick: ()=>setPickerOpen(true),
|
|
184
|
+
children: state.documentRelation ? 'Change image…' : `Pick ${targetDef?.labels.singular ?? 'image'}…`
|
|
185
|
+
})
|
|
186
|
+
]
|
|
164
187
|
})
|
|
165
188
|
]
|
|
166
189
|
}),
|
|
@@ -254,6 +277,7 @@ const InlineImageModal = ({ isOpen, collection, data: dataFromProps, onSubmit, o
|
|
|
254
277
|
/*#__PURE__*/ jsx(RelationPicker, {
|
|
255
278
|
targetCollectionPath: collection,
|
|
256
279
|
targetDefinition: targetDef,
|
|
280
|
+
extraSelectFields: PICKER_EXTRA_FIELDS,
|
|
257
281
|
isOpen: pickerOpen,
|
|
258
282
|
onSelect: handlePickerSelect,
|
|
259
283
|
onDismiss: ()=>setPickerOpen(false)
|
package/dist/lexical-editor.js
CHANGED
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.7.0",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.9.0"
|
|
9
9
|
},
|
|
@@ -72,10 +72,10 @@
|
|
|
72
72
|
"npm-run-all": "^4.1.5",
|
|
73
73
|
"prism-react-renderer": "^2.4.1",
|
|
74
74
|
"react-error-boundary": "^6.1.1",
|
|
75
|
-
"@byline/
|
|
76
|
-
"@byline/
|
|
77
|
-
"@byline/core": "2.
|
|
78
|
-
"@byline/
|
|
75
|
+
"@byline/client": "2.7.0",
|
|
76
|
+
"@byline/ui": "2.7.0",
|
|
77
|
+
"@byline/core": "2.7.0",
|
|
78
|
+
"@byline/admin": "2.7.0"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
81
|
"react": "^19.0.0",
|
|
@@ -44,7 +44,7 @@ const EditorComponent = lazy(() =>
|
|
|
44
44
|
|
|
45
45
|
export function EditorField(props: EditorFieldProps): React.JSX.Element {
|
|
46
46
|
return (
|
|
47
|
-
<Suspense fallback={<Shimmer
|
|
47
|
+
<Suspense fallback={<Shimmer variant="text" lines={20} lineHeight="1.15rem" />}>
|
|
48
48
|
<EditorComponent {...props} />
|
|
49
49
|
</Suspense>
|
|
50
50
|
)
|
|
@@ -1,29 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Inline image modal — image picker row.
|
|
3
|
+
*
|
|
4
|
+
* Only the picker row (thumbnail + change button + title) needs bespoke
|
|
5
|
+
* layout; the rest of the modal is composed from shared @byline/ui
|
|
6
|
+
* components. Colours use the design tokens so the row is theme-aware.
|
|
7
|
+
*/
|
|
4
8
|
|
|
5
|
-
.inline-image-
|
|
9
|
+
.inline-image-modal-picker {
|
|
6
10
|
display: flex;
|
|
7
|
-
|
|
11
|
+
align-items: center;
|
|
12
|
+
gap: 0.75rem;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.inline-image-modal-thumb,
|
|
16
|
+
.inline-image-modal-thumb-placeholder {
|
|
17
|
+
flex-shrink: 0;
|
|
18
|
+
width: 8rem;
|
|
19
|
+
height: 8rem;
|
|
20
|
+
border-radius: 0.25rem;
|
|
21
|
+
border: 1px solid var(--border-color);
|
|
8
22
|
}
|
|
9
23
|
|
|
10
|
-
.inline-image-
|
|
11
|
-
|
|
24
|
+
.inline-image-modal-thumb {
|
|
25
|
+
object-fit: cover;
|
|
12
26
|
}
|
|
13
27
|
|
|
14
|
-
.inline-image-
|
|
15
|
-
|
|
28
|
+
.inline-image-modal-thumb-placeholder {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
background-color: var(--surface-subtle);
|
|
33
|
+
color: var(--text-subtle);
|
|
34
|
+
font-size: 0.75rem;
|
|
16
35
|
}
|
|
17
36
|
|
|
18
|
-
.inline-image-
|
|
19
|
-
|
|
37
|
+
.inline-image-modal-picker-details {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex: 1;
|
|
40
|
+
min-width: 0;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
align-items: flex-start;
|
|
43
|
+
gap: 0.5rem;
|
|
20
44
|
}
|
|
21
45
|
|
|
22
|
-
.inline-image-
|
|
23
|
-
|
|
46
|
+
.inline-image-modal-change-btn {
|
|
47
|
+
flex-shrink: 0;
|
|
48
|
+
min-width: 70px;
|
|
24
49
|
}
|
|
25
50
|
|
|
26
|
-
.inline-image-
|
|
27
|
-
|
|
28
|
-
|
|
51
|
+
.inline-image-modal-title {
|
|
52
|
+
align-self: stretch;
|
|
53
|
+
min-width: 0;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
text-overflow: ellipsis;
|
|
57
|
+
font-size: 0.875rem;
|
|
58
|
+
color: var(--text-subtle);
|
|
29
59
|
}
|
|
@@ -34,6 +34,8 @@ import type { DocumentRelation } from '../../nodes/document-relation'
|
|
|
34
34
|
import type { Position } from './node-types'
|
|
35
35
|
import type { InlineImageData, InlineImageModalProps } from './types'
|
|
36
36
|
|
|
37
|
+
import './inline-image-modal.css'
|
|
38
|
+
|
|
37
39
|
interface FormState {
|
|
38
40
|
documentRelation: DocumentRelation | null
|
|
39
41
|
altText: string
|
|
@@ -60,6 +62,30 @@ function fromInlineImageData(data: InlineImageData | undefined): FormState {
|
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Human-readable label fields a media/document collection might use, in
|
|
67
|
+
* priority order. Used only to *seed* the alt-text field when the picked
|
|
68
|
+
* record has no explicit `altText`. Intentionally excludes filenames — a
|
|
69
|
+
* label like `IMG_2024.jpg` makes poor alt text. The user can always edit.
|
|
70
|
+
*/
|
|
71
|
+
const LABEL_FIELD_KEYS = ['title', 'name', 'subject', 'label'] as const
|
|
72
|
+
|
|
73
|
+
function deriveLabel(fields: Record<string, any>): string | undefined {
|
|
74
|
+
for (const key of LABEL_FIELD_KEYS) {
|
|
75
|
+
const value = fields[key]
|
|
76
|
+
if (typeof value === 'string' && value.trim().length > 0) return value
|
|
77
|
+
}
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extra fields the relation picker must load (beyond its display columns)
|
|
83
|
+
* so `handlePickerSelect` can seed the alt-text field: the media's own
|
|
84
|
+
* `altText`, then the human-readable label fallbacks. Module-level so the
|
|
85
|
+
* reference is stable across renders (it feeds the picker's fetch effect).
|
|
86
|
+
*/
|
|
87
|
+
const PICKER_EXTRA_FIELDS: string[] = ['altText', ...LABEL_FIELD_KEYS]
|
|
88
|
+
|
|
63
89
|
export const InlineImageModal: React.FC<InlineImageModalProps> = ({
|
|
64
90
|
isOpen,
|
|
65
91
|
collection,
|
|
@@ -110,7 +136,13 @@ export const InlineImageModal: React.FC<InlineImageModalProps> = ({
|
|
|
110
136
|
const fields = selection.record?.fields ?? {}
|
|
111
137
|
const image = fields.image as StoredFileValue | undefined
|
|
112
138
|
const title = typeof fields.title === 'string' ? fields.title : undefined
|
|
113
|
-
|
|
139
|
+
// Treat a present-but-blank `altText` as absent so the title fallback
|
|
140
|
+
// below still applies.
|
|
141
|
+
const altTextFromMedia =
|
|
142
|
+
typeof fields.altText === 'string' && fields.altText.trim().length > 0
|
|
143
|
+
? fields.altText
|
|
144
|
+
: undefined
|
|
145
|
+
const derivedLabel = deriveLabel(fields)
|
|
114
146
|
const sizes = image ? deriveImageSizes(image) : []
|
|
115
147
|
|
|
116
148
|
setState((s) => {
|
|
@@ -128,10 +160,11 @@ export const InlineImageModal: React.FC<InlineImageModalProps> = ({
|
|
|
128
160
|
targetCollectionPath: collection,
|
|
129
161
|
document: Object.keys(document).length > 0 ? document : undefined,
|
|
130
162
|
},
|
|
131
|
-
// Pre-fill alt-text
|
|
132
|
-
// the
|
|
133
|
-
//
|
|
134
|
-
|
|
163
|
+
// Pre-fill alt-text on first pick when the form's alt-text is still
|
|
164
|
+
// empty: prefer the media's own `altText`, then fall back to a
|
|
165
|
+
// human-readable label (title / name / subject / label). Editorial
|
|
166
|
+
// wins over the source record once the user starts typing.
|
|
167
|
+
altText: s.altText.length > 0 ? s.altText : (altTextFromMedia ?? derivedLabel ?? ''),
|
|
135
168
|
}
|
|
136
169
|
})
|
|
137
170
|
setImageError(null)
|
|
@@ -182,33 +215,31 @@ export const InlineImageModal: React.FC<InlineImageModalProps> = ({
|
|
|
182
215
|
<div className="flex flex-col gap-4">
|
|
183
216
|
<div className="flex flex-col gap-2">
|
|
184
217
|
<span className="text-sm font-medium">Image</span>
|
|
185
|
-
<div className="
|
|
218
|
+
<div className="inline-image-modal-picker">
|
|
186
219
|
{pickedThumbUrl ? (
|
|
187
220
|
<img
|
|
188
221
|
src={pickedThumbUrl}
|
|
189
222
|
alt={pickedTitle ?? ''}
|
|
190
|
-
className="
|
|
223
|
+
className="inline-image-modal-thumb"
|
|
191
224
|
/>
|
|
192
225
|
) : (
|
|
193
|
-
<div className="
|
|
194
|
-
—
|
|
195
|
-
</div>
|
|
196
|
-
)}
|
|
197
|
-
<Button
|
|
198
|
-
size="sm"
|
|
199
|
-
className="min-w-[70px]"
|
|
200
|
-
variant="outlined"
|
|
201
|
-
intent="noeffect"
|
|
202
|
-
type="button"
|
|
203
|
-
onClick={() => setPickerOpen(true)}
|
|
204
|
-
>
|
|
205
|
-
{state.documentRelation
|
|
206
|
-
? 'Change image…'
|
|
207
|
-
: `Pick ${targetDef?.labels.singular ?? 'image'}…`}
|
|
208
|
-
</Button>
|
|
209
|
-
{pickedTitle && (
|
|
210
|
-
<span className="text-sm text-gray-200 truncate">{pickedTitle}</span>
|
|
226
|
+
<div className="inline-image-modal-thumb-placeholder">—</div>
|
|
211
227
|
)}
|
|
228
|
+
<div className="inline-image-modal-picker-details">
|
|
229
|
+
{pickedTitle && <span className="inline-image-modal-title">{pickedTitle}</span>}
|
|
230
|
+
<Button
|
|
231
|
+
size="sm"
|
|
232
|
+
className="inline-image-modal-change-btn"
|
|
233
|
+
variant="outlined"
|
|
234
|
+
intent="noeffect"
|
|
235
|
+
type="button"
|
|
236
|
+
onClick={() => setPickerOpen(true)}
|
|
237
|
+
>
|
|
238
|
+
{state.documentRelation
|
|
239
|
+
? 'Change image…'
|
|
240
|
+
: `Pick ${targetDef?.labels.singular ?? 'image'}…`}
|
|
241
|
+
</Button>
|
|
242
|
+
</div>
|
|
212
243
|
</div>
|
|
213
244
|
{imageError && <ErrorText id="image-error" text={imageError} />}
|
|
214
245
|
</div>
|
|
@@ -293,6 +324,7 @@ export const InlineImageModal: React.FC<InlineImageModalProps> = ({
|
|
|
293
324
|
<RelationPicker
|
|
294
325
|
targetCollectionPath={collection}
|
|
295
326
|
targetDefinition={targetDef}
|
|
327
|
+
extraSelectFields={PICKER_EXTRA_FIELDS}
|
|
296
328
|
isOpen={pickerOpen}
|
|
297
329
|
onSelect={handlePickerSelect}
|
|
298
330
|
onDismiss={() => setPickerOpen(false)}
|
package/src/lexical-editor.tsx
CHANGED
|
@@ -142,7 +142,7 @@ function EditorPlaceholder() {
|
|
|
142
142
|
return (
|
|
143
143
|
<div className="byline-field-richtext">
|
|
144
144
|
<div className="byline-field-richtext-body">
|
|
145
|
-
<Shimmer
|
|
145
|
+
<Shimmer variant="text" lines={20} lineHeight="1.15rem" />
|
|
146
146
|
</div>
|
|
147
147
|
</div>
|
|
148
148
|
)
|