Skip to content

Commit bea77f2

Browse files
authored
refactor(richtext-lexical): new upload node design (#13901)
This changes the design of lexical upload nodes to better show the actual media instead of the metadata. ## Updated Design https://github.com/user-attachments/assets/49096378-35c2-4eb0-b4b6-5f138d49bdad Light mode: <img width="780" height="962" alt="Screenshot 2025-09-24 at 10 11 32@2x" src="https://github.com/user-attachments/assets/7611e659-3914-46e9-9c8c-db88c180227b" /> ## Previous Design > Before: > > <img width="1358" height="860" alt="Screenshot 2025-09-22 at 16 01 16@2x" src="https://github.com/user-attachments/assets/7831761c-6c3c-4072-82ed-68b88e3842b7" /> > > After: > > <img width="1776" height="1632" alt="Screenshot 2025-09-22 at 16 01 00@2x" src="https://github.com/user-attachments/assets/b434b6d5-a965-4c2b-adba-c1bf2a3be4bc" /> > > > https://github.com/user-attachments/assets/f2749a38-c191-4b50-a521-8f722ed42a8f > --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211429812808983
1 parent abbe38f commit bea77f2

6 files changed

Lines changed: 191 additions & 138 deletions

File tree

packages/richtext-lexical/src/features/upload/client/component/index.scss

Lines changed: 107 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -4,143 +4,163 @@
44
.lexical-upload {
55
@extend %body;
66
@include shadow-sm;
7-
max-width: calc(var(--base) * 15);
8-
display: flex;
9-
align-items: center;
10-
background: var(--theme-input-bg);
7+
118
border-radius: $style-radius-m;
129
border: 1px solid var(--theme-elevation-100);
1310
position: relative;
1411
font-family: var(--font-body);
1512
margin-block: base(0.5);
1613

17-
.btn {
18-
margin: 0;
19-
}
20-
2114
&:hover {
2215
border: 1px solid var(--theme-elevation-150);
2316
}
2417

18+
img,
19+
svg {
20+
border-radius: $style-radius-s;
21+
width: auto;
22+
}
23+
24+
&--landscape {
25+
img,
26+
svg {
27+
max-width: 450px;
28+
min-width: 450px;
29+
}
30+
}
31+
&--portrait {
32+
img,
33+
svg {
34+
max-height: 450px;
35+
min-height: 450px;
36+
}
37+
}
38+
39+
button {
40+
margin: 0;
41+
overflow: hidden;
42+
white-space: nowrap;
43+
text-overflow: ellipsis;
44+
}
45+
2546
&__card {
2647
@include soft-shadow-bottom;
2748
display: flex;
2849
flex-direction: column;
2950
width: 100%;
3051
}
3152

32-
&__topRow {
33-
display: flex;
34-
}
53+
&__floater {
54+
@include shadow-lg;
55+
position: absolute;
3556

36-
&__thumbnail {
37-
width: calc(var(--base) * 3.25);
38-
height: auto;
39-
position: relative;
40-
overflow: hidden;
41-
flex-shrink: 0;
42-
border-top-left-radius: $style-radius-m;
57+
/* hidden by default */
58+
opacity: 0;
59+
transition:
60+
opacity 0.15s ease,
61+
transform 0.15s ease;
62+
pointer-events: none;
63+
}
4364

44-
img,
45-
svg {
46-
position: absolute;
47-
object-fit: cover;
48-
width: 100%;
49-
height: 100%;
50-
background-color: var(--theme-elevation-800);
51-
}
65+
&:hover .lexical-upload__floater,
66+
&__media:focus-within .lexical-upload__floater {
67+
opacity: 1;
68+
pointer-events: auto;
5269
}
5370

54-
&__topRowRightPanel {
55-
flex-grow: 1;
71+
/* --- Floating Action Buttons (top-right) ------------------------------------- */
72+
&__overlay {
5673
display: flex;
57-
align-items: center;
58-
padding: calc(var(--base) * 0.75);
59-
justify-content: space-between;
60-
max-width: calc(100% - #{calc(var(--base) * 3.25)});
74+
top: calc(var(--base) * 0.5);
75+
right: calc(var(--base) * 0.5);
76+
padding: calc(var(--base) * 0.2) calc(var(--base) * 0.2);
77+
78+
background: var(--theme-elevation-50);
79+
border-radius: $style-radius-m;
80+
transform: translateY(-6px);
81+
}
82+
&:hover .lexical-upload__overlay,
83+
&__media:focus-within .lexical-upload__overlay {
84+
transform: translateY(0);
6185
}
6286

6387
&__actions {
6488
display: flex;
6589
align-items: center;
66-
flex-shrink: 0;
67-
margin-left: calc(var(--base) * 0.5);
90+
flex-wrap: nowrap;
91+
gap: calc(var(--base) * 0.3);
6892

69-
.lexical-upload__doc-drawer-toggler {
70-
pointer-events: all;
71-
}
72-
73-
& > *:not(:last-child) {
74-
margin-right: calc(var(--base) * 0.25);
75-
}
76-
}
77-
78-
&__removeButton {
79-
margin: 0;
80-
81-
line {
82-
stroke-width: $style-stroke-width-m;
83-
}
84-
85-
&:disabled {
86-
color: var(--theme-elevation-300);
87-
pointer-events: none;
93+
.btn:hover {
94+
background: var(--theme-elevation-100);
8895
}
8996
}
97+
/* --- Floating Metadata (bottom-center) ------------------------------------- */
98+
&__metaOverlay {
99+
display: inline-flex;
100+
left: 50%;
101+
bottom: 0;
102+
width: 100%;
103+
padding: calc(var(--base) * 0.5) calc(var(--base) * 0.75);
104+
transform: translateX(-50%);
90105

91-
&__upload-drawer-toggler {
92-
background-color: transparent;
93-
border: none;
94-
padding: 0;
95-
margin: 0;
96-
outline: none;
97-
line-height: inherit;
98-
}
106+
flex-wrap: wrap;
107+
gap: calc(var(--base) * 0.5);
108+
row-gap: 0;
99109

100-
&__doc-drawer-toggler {
101-
text-decoration: underline;
110+
background: color-mix(in oklab, var(--theme-elevation-50) 55%, transparent);
111+
border-radius: 0 0 $style-radius-s $style-radius-s;
112+
backdrop-filter: saturate(1.2) blur(8px);
102113
}
103114

104-
&__doc-drawer-toggler,
105-
&__list-drawer-toggler,
106-
&__upload-drawer-toggler {
107-
& > * {
108-
margin: 0;
115+
html[data-theme='light'] & {
116+
&__metaOverlay {
117+
background: color-mix(in oklab, var(--theme-elevation-800) 55%, transparent);
118+
color: var(--theme-elevation-50);
109119
}
110120

111-
&:disabled {
121+
&__collectionLabel {
112122
color: var(--theme-elevation-300);
113-
pointer-events: none;
114123
}
115124
}
116125

117-
&__collectionLabel {
118-
overflow: hidden;
119-
text-overflow: ellipsis;
120-
white-space: nowrap;
121-
}
122-
123-
&__bottomRow {
124-
padding: calc(var(--base) * 0.5);
125-
border-top: 1px solid var(--theme-elevation-100);
126-
}
127-
128-
h5 {
126+
&__filename {
129127
white-space: nowrap;
130128
text-overflow: ellipsis;
131129
overflow: hidden;
130+
text-decoration: underline;
131+
cursor: pointer;
132132
}
133133

134-
&__wrap {
135-
padding: calc(var(--base) * 0.5) calc(var(--base) * 0.5) calc(var(--base) * 0.5) var(--base);
136-
text-align: left;
134+
&__collectionLabel {
135+
color: var(--theme-elevation-500);
136+
font-size: 0.9em;
137137
overflow: hidden;
138138
text-overflow: ellipsis;
139+
white-space: nowrap;
139140
}
140141

141142
@include small-break {
142-
&__topRowRightPanel {
143-
padding: calc(var(--base) * 0.75) calc(var(--base) * 0.5);
143+
img,
144+
svg {
145+
// Allow images to shrink < 450px on small screens to
146+
// maintain aspect ratio
147+
min-width: unset;
148+
min-height: unset;
149+
}
150+
151+
&__metaOverlay {
152+
gap: 0;
153+
padding: calc(var(--base) * 0.5) calc(var(--base) * 0.6);
154+
flex-direction: column;
155+
156+
button {
157+
display: flex;
158+
justify-content: flex-start;
159+
}
160+
}
161+
162+
&__collectionLabel {
163+
max-width: 100%;
144164
}
145165
}
146166
}

packages/richtext-lexical/src/features/upload/client/component/index.tsx

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
usePayloadAPI,
1313
useTranslation,
1414
} from '@payloadcms/ui'
15-
import { $getNodeByKey } from 'lexical'
15+
import { $getNodeByKey, type ElementFormatType } from 'lexical'
1616
import { isImage } from 'payload/shared'
1717
import React, { useCallback, useId, useReducer, useRef, useState } from 'react'
1818

@@ -37,6 +37,7 @@ const initialParams = {
3737

3838
export type ElementProps = {
3939
data: UploadData
40+
format?: ElementFormatType
4041
nodeKey: string
4142
}
4243

@@ -139,33 +140,42 @@ const Component: React.FC<ElementProps> = (props) => {
139140
[editor, nodeKey],
140141
)
141142

143+
const aspectRatio =
144+
thumbnailSRC && data?.width && data?.height
145+
? data.width > data.height
146+
? 'landscape'
147+
: 'portrait'
148+
: 'landscape'
149+
142150
return (
143-
<div className={baseClass} contentEditable={false} ref={uploadRef}>
151+
<div
152+
className={`${baseClass} ${baseClass}--${aspectRatio}`}
153+
data-filename={data?.filename}
154+
ref={uploadRef}
155+
>
144156
<div className={`${baseClass}__card`}>
145-
<div className={`${baseClass}__topRow`}>
146-
<div className={`${baseClass}__thumbnail`}>
147-
<Thumbnail
148-
collectionSlug={relationTo}
149-
fileSrc={isImage(data?.mimeType) ? thumbnailSRC : null}
150-
/>
151-
</div>
152-
<div className={`${baseClass}__topRowRightPanel`}>
153-
<div className={`${baseClass}__collectionLabel`}>
154-
{getTranslation(relatedCollection.labels.singular, i18n)}
155-
</div>
156-
{editor.isEditable() && (
157-
<div className={`${baseClass}__actions`}>
157+
<div className={`${baseClass}__media`}>
158+
<Thumbnail
159+
collectionSlug={relationTo}
160+
fileSrc={isImage(data?.mimeType) ? thumbnailSRC : null}
161+
height={data?.height}
162+
size="none"
163+
width={data?.width}
164+
/>
165+
166+
{editor.isEditable() && (
167+
<div className={`${baseClass}__overlay ${baseClass}__floater`}>
168+
<div className={`${baseClass}__actions`} role="toolbar">
158169
{hasExtraFields ? (
159170
<Button
160171
buttonStyle="icon-label"
161172
className={`${baseClass}__upload-drawer-toggler`}
162173
disabled={readOnly}
163174
el="button"
164175
icon="edit"
165-
onClick={() => {
166-
toggleDrawer()
167-
}}
176+
onClick={toggleDrawer}
168177
round
178+
size="medium"
169179
tooltip={t('fields:editRelationship')}
170180
/>
171181
) : null}
@@ -182,8 +192,10 @@ const Component: React.FC<ElementProps> = (props) => {
182192
})
183193
}}
184194
round
195+
size="medium"
185196
tooltip={t('fields:swapUpload')}
186197
/>
198+
187199
<Button
188200
buttonStyle="icon-label"
189201
className={`${baseClass}__removeButton`}
@@ -194,18 +206,26 @@ const Component: React.FC<ElementProps> = (props) => {
194206
removeUpload()
195207
}}
196208
round
209+
size="medium"
197210
tooltip={t('fields:removeUpload')}
198211
/>
199212
</div>
200-
)}
201-
</div>
213+
</div>
214+
)}
202215
</div>
203-
<div className={`${baseClass}__bottomRow`}>
216+
217+
<div className={`${baseClass}__metaOverlay ${baseClass}__floater`}>
204218
<DocumentDrawerToggler className={`${baseClass}__doc-drawer-toggler`}>
205-
<strong>{data?.filename}</strong>
219+
<strong className={`${baseClass}__filename`}>
220+
{data?.filename || t('general:untitled')}
221+
</strong>
206222
</DocumentDrawerToggler>
223+
<div className={`${baseClass}__collectionLabel`}>
224+
{getTranslation(relatedCollection.labels.singular, i18n)}
225+
</div>
207226
</div>
208227
</div>
228+
209229
{value ? <DocumentDrawer onSave={updateUpload} /> : null}
210230
{hasExtraFields ? (
211231
<FieldsDrawer

packages/richtext-lexical/src/features/upload/client/nodes/UploadNode.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class UploadNode extends UploadServerNode {
6565
if ((this.__data as Internal_UploadData).pending) {
6666
return <PendingUploadComponent />
6767
}
68-
return <RawUploadComponent data={this.__data} nodeKey={this.getKey()} />
68+
return <RawUploadComponent data={this.__data} format={this.__format} nodeKey={this.getKey()} />
6969
}
7070

7171
override exportJSON(): SerializedUploadNode {

0 commit comments

Comments
 (0)