Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
lib
react-ag-qeditor
Commits
78d6243d
Commit
78d6243d
authored
Apr 17, 2026
by
Яков
Browse files
add button-url
parent
47ffb026
Changes
4
Show whitespace changes
Inline
Side-by-side
src/QEditor.jsx
View file @
78d6243d
...
...
@@ -12,7 +12,7 @@ import TableCell from '@tiptap/extension-table-cell'
import
TableRow
from
'
@tiptap/extension-table-row
'
import
TableHeader
from
'
@tiptap/extension-table-header
'
import
Focus
from
'
@tiptap/extension-focus
'
import
{
Input
,
Modal
,
Form
,
Button
,
message
}
from
"
antd
"
;
import
{
Input
,
Modal
,
Form
,
Button
,
message
,
Select
as
AntdSelect
}
from
"
antd
"
;
import
Link
from
'
@tiptap/extension-link
'
// import Image from '@tiptap/extension-image'
import
TextAlign
from
'
@tiptap/extension-text-align
'
...
...
@@ -38,6 +38,7 @@ import TableExtension from './extensions/TableExtension'
import
ToggleBlock
from
'
./extensions/ToggleBlock
'
import
InteractiveImage
from
'
./extensions/InteractiveImage
'
import
FontSize
from
'
./extensions/FontSize
'
import
ButtonLinkExtension
from
'
./extensions/ButtonLink
'
// import Image from '@tiptap/extension-image'
// import ImageResize from 'tiptap-extension-resize-image';
...
...
@@ -52,6 +53,7 @@ import { isMobile } from 'react-device-detect'
import
{
ExportPdf
}
from
'
./extensions/ExportPdf
'
import
{
mergeAttributes
}
from
"
@tiptap/core
"
;
import
Upload
from
"
rc-upload
"
;
import
{
NodeSelection
}
from
'
prosemirror-state
'
// const CustomImage = Image.extend({
// options: {inline: true},
...
...
@@ -92,6 +94,34 @@ const QEditor = ({
})
=>
{
global
.
uploadUrl
=
uploadOptions
.
url
const
defaultButtonLinkData
=
{
text
:
'
Перейти
'
,
href
:
''
,
fontSize
:
'
16px
'
,
textColor
:
'
#ffffff
'
,
backgroundColor
:
'
#1790FF
'
}
const
getSelectedButtonLinkData
=
()
=>
{
if
(
!
editor
||
!
(
editor
.
state
.
selection
instanceof
NodeSelection
))
{
return
null
}
const
selectedNode
=
editor
.
state
.
selection
.
node
if
(
!
selectedNode
||
selectedNode
.
type
.
name
!==
'
buttonLink
'
)
{
return
null
}
return
{
text
:
selectedNode
.
attrs
.
text
||
defaultButtonLinkData
.
text
,
href
:
selectedNode
.
attrs
.
href
||
''
,
fontSize
:
selectedNode
.
attrs
.
fontSize
||
defaultButtonLinkData
.
fontSize
,
textColor
:
selectedNode
.
attrs
.
textColor
||
defaultButtonLinkData
.
textColor
,
backgroundColor
:
selectedNode
.
attrs
.
backgroundColor
||
defaultButtonLinkData
.
backgroundColor
}
}
const
[
innerModalType
,
setInnerModalType
]
=
useState
(
null
)
const
[
embedContent
,
setEmbedContent
]
=
useState
(
''
)
const
[
uploaderUid
,
setUploaderUid
]
=
useState
(
'
uid
'
+
new
Date
())
...
...
@@ -106,6 +136,7 @@ const QEditor = ({
const
[
oldFocusFromTo
,
setOldFocusFromTo
]
=
useState
(
null
)
const
[
isUploading
,
setIsUploading
]
=
useState
(
false
)
const
[
recordType
,
setRecordType
]
=
useState
({
video
:
true
})
const
[
buttonLinkData
,
setButtonLinkData
]
=
useState
(
defaultButtonLinkData
)
let
formRef
=
useRef
(
null
);
// eslint-disable-next-line no-unused-vars
...
...
@@ -167,6 +198,11 @@ const QEditor = ({
}
const
modalOpener
=
(
type
,
title
)
=>
{
if
(
type
===
'
buttonLink
'
)
{
const
selectedButtonLinkData
=
getSelectedButtonLinkData
()
setButtonLinkData
(
selectedButtonLinkData
||
defaultButtonLinkData
)
}
setModalTitle
(
title
)
setInnerModalType
(
type
)
setModalIsOpen
(
true
)
...
...
@@ -203,12 +239,42 @@ const QEditor = ({
'
#ffe672
'
]
}
const
fontSizes
=
[
'
default
'
,
'
12px
'
,
'
14px
'
,
'
16px
'
,
'
18px
'
,
'
20px
'
,
'
24px
'
,
'
28px
'
,
'
32px
'
]
const
fontSizes
=
[
'
12px
'
,
'
14px
'
,
'
16px
'
,
'
18px
'
,
'
20px
'
,
'
24px
'
,
'
28px
'
,
'
32px
'
]
const
validateLinkHref
=
(
href
)
=>
{
if
(
!
href
||
typeof
href
!==
'
string
'
)
{
return
false
}
try
{
const
normalizedHref
=
/^
[
a-zA-Z
][
a-zA-Z
\d
+
\-
.
]
*:/
.
test
(
href
)
?
href
:
`https://
${
href
}
`
const
parsedUrl
=
new
URL
(
normalizedHref
)
return
[
'
http:
'
,
'
https:
'
].
includes
(
parsedUrl
.
protocol
)
}
catch
(
error
)
{
return
false
}
}
const
normalizeLinkHref
=
(
href
)
=>
{
if
(
!
href
||
typeof
href
!==
'
string
'
)
{
return
''
}
return
/^
[
a-zA-Z
][
a-zA-Z
\d
+
\-
.
]
*:/
.
test
(
href
)
?
href
:
`https://
${
href
}
`
}
const
toolsLib
=
{
link
:
{
title
:
'
Вставить ссылку
'
,
onClick
:
()
=>
{
const
selection
=
{
from
:
editor
.
state
.
selection
.
from
,
to
:
editor
.
state
.
selection
.
to
,
empty
:
editor
.
state
.
selection
.
empty
}
const
previousUrl
=
editor
.
getAttributes
(
'
link
'
).
href
const
url
=
window
.
prompt
(
'
Введите URL
'
,
previousUrl
)
...
...
@@ -219,14 +285,41 @@ const QEditor = ({
// empty
if
(
url
===
''
)
{
editor
.
chain
().
focus
().
extendMarkRange
(
'
link
'
).
unsetLink
().
run
()
editor
.
chain
().
focus
().
setTextSelection
({
from
:
selection
.
from
,
to
:
selection
.
to
}).
extendMarkRange
(
'
link
'
).
unsetLink
().
run
()
return
}
const
normalizedUrl
=
normalizeLinkHref
(
url
)
if
(
!
validateLinkHref
(
normalizedUrl
))
{
window
.
alert
(
'
Некорректный URL
'
)
return
}
if
(
selection
.
empty
)
{
editor
.
chain
().
focus
().
setTextSelection
(
selection
.
from
).
insertContent
({
type
:
'
text
'
,
text
:
url
,
marks
:
[
{
type
:
'
link
'
,
attrs
:
{
href
:
normalizedUrl
,
target
:
'
_blank
'
}
}
]
}).
run
()
return
}
// update link
editor
.
chain
().
focus
().
extendMarkRange
(
'
link
'
).
setLink
({
href
:
url
,
target
:
'
_blank
'
}).
run
()
editor
.
chain
().
focus
().
setTextSelection
({
from
:
selection
.
from
,
to
:
selection
.
to
}).
extendMarkRange
(
'
link
'
).
setLink
({
href
:
normalizedUrl
,
target
:
'
_blank
'
}).
run
()
}
},
buttonLink
:
{
title
:
'
Кнопка-ссылка
'
,
onClick
:
()
=>
modalOpener
(
'
buttonLink
'
,
'
Добавить кнопку
'
)
},
file
:
{
title
:
'
Прикрепить файл
'
,
onClick
:
()
=>
modalOpener
(
'
file
'
,
'
Прикрепить файл
'
)
...
...
@@ -539,7 +632,7 @@ const QEditor = ({
linkOnPaste
:
true
,
defaultProtocol
:
'
https
'
,
protocols
:
[
'
http
'
,
'
https
'
],
validate
:
(
href
)
=>
console
.
log
(
h
ref
)
,
validate
:
validateLinkH
ref
,
}),
Video
,
Iframe
,
...
...
@@ -565,6 +658,7 @@ const QEditor = ({
}),
TextStyle
,
FontSize
,
ButtonLinkExtension
,
Color
.
configure
({
types
:
[
'
textStyle
'
]
}),
...
...
@@ -769,6 +863,100 @@ const QEditor = ({
{
getUploader
({
accept
:
'
*
'
,
afterParams
:
[
'
no_convert=1
'
]})
}
</
Fragment
>
)
case
'
buttonLink
'
:
return
(
<
div
className
=
'atma-editor-button-link-form'
>
<
label
className
=
'atma-editor-field'
>
<
span
>
Текст кнопки
</
span
>
<
Input
value
=
{
buttonLinkData
.
text
}
placeholder
=
'Перейти'
onChange
=
{
(
event
)
=>
{
setButtonLinkData
({
...
buttonLinkData
,
text
:
event
.
target
.
value
})
}
}
/>
</
label
>
<
label
className
=
'atma-editor-field'
>
<
span
>
Ссылка
</
span
>
<
Input
value
=
{
buttonLinkData
.
href
}
placeholder
=
'https://example.com'
onChange
=
{
(
event
)
=>
{
setButtonLinkData
({
...
buttonLinkData
,
href
:
event
.
target
.
value
})
}
}
/>
</
label
>
<
div
className
=
'atma-editor-button-link-grid'
>
<
label
className
=
'atma-editor-field atma-editor-field-select'
>
<
span
>
Размер шрифта
</
span
>
<
AntdSelect
value
=
{
buttonLinkData
.
fontSize
}
options
=
{
fontSizes
.
map
((
fontSize
)
=>
({
value
:
fontSize
,
label
:
fontSize
}))
}
getPopupContainer
=
{
(
triggerNode
)
=>
triggerNode
.
parentNode
}
dropdownStyle
=
{
{
zIndex
:
100003
}
}
onChange
=
{
(
value
)
=>
{
setButtonLinkData
({
...
buttonLinkData
,
fontSize
:
value
})
}
}
/>
</
label
>
<
label
className
=
'atma-editor-field atma-editor-field-color'
>
<
span
>
Цвет текста
</
span
>
<
input
type
=
'color'
value
=
{
buttonLinkData
.
textColor
}
className
=
'atma-editor-color-input'
onChange
=
{
(
event
)
=>
{
setButtonLinkData
({
...
buttonLinkData
,
textColor
:
event
.
target
.
value
})
}
}
/>
</
label
>
<
label
className
=
'atma-editor-field atma-editor-field-color'
>
<
span
>
Цвет кнопки
</
span
>
<
input
type
=
'color'
value
=
{
buttonLinkData
.
backgroundColor
}
className
=
'atma-editor-color-input'
onChange
=
{
(
event
)
=>
{
setButtonLinkData
({
...
buttonLinkData
,
backgroundColor
:
event
.
target
.
value
})
}
}
/>
</
label
>
</
div
>
<
div
className
=
'atma-editor-button-link-preview-wrap'
>
<
span
>
Предпросмотр
</
span
>
<
a
href
=
'/'
onClick
=
{
(
event
)
=>
event
.
preventDefault
()
}
className
=
'atma-editor-button-link-preview'
style
=
{
{
color
:
buttonLinkData
.
textColor
,
backgroundColor
:
buttonLinkData
.
backgroundColor
,
fontSize
:
buttonLinkData
.
fontSize
||
'
16px
'
}
}
>
{
buttonLinkData
.
text
||
'
Кнопка
'
}
</
a
>
</
div
>
</
div
>
)
case
'
voicemessage
'
:
return
(
<
Fragment
>
...
...
@@ -1063,6 +1251,9 @@ const QEditor = ({
)
isDisabled
=
!
regex
.
test
(
embedContent
)
break
case
'
buttonLink
'
:
isDisabled
=
!
buttonLinkData
.
text
.
trim
()
||
!
validateLinkHref
(
buttonLinkData
.
href
)
break
}
return
isDisabled
...
...
@@ -1084,6 +1275,7 @@ const QEditor = ({
clearBlobUrl
()
setUploaderUid
(
`uid
${
new
Date
()}
`
)
setUploadedPaths
([])
setButtonLinkData
(
defaultButtonLinkData
)
setModalIsOpen
(
false
)
}
},
...
...
@@ -1096,6 +1288,7 @@ const QEditor = ({
clearBlobUrl
()
setUploaderUid
(
`uid
${
new
Date
()}
`
)
setUploadedPaths
([])
setButtonLinkData
(
defaultButtonLinkData
)
setModalIsOpen
(
false
)
}
}
...
...
@@ -1317,6 +1510,26 @@ const QEditor = ({
).
run
()
})
break
case
'
buttonLink
'
:
{
const
buttonLinkAttrs
=
{
text
:
buttonLinkData
.
text
.
trim
(),
href
:
normalizeLinkHref
(
buttonLinkData
.
href
),
fontSize
:
buttonLinkData
.
fontSize
.
trim
()
||
'
16px
'
,
textColor
:
buttonLinkData
.
textColor
,
backgroundColor
:
buttonLinkData
.
backgroundColor
}
if
(
editor
.
state
.
selection
instanceof
NodeSelection
&&
editor
.
state
.
selection
.
node
&&
editor
.
state
.
selection
.
node
.
type
.
name
===
'
buttonLink
'
)
{
editor
.
chain
().
focus
().
updateAttributes
(
'
buttonLink
'
,
buttonLinkAttrs
).
run
()
}
else
{
editor
.
chain
().
focus
().
setButtonLink
(
buttonLinkAttrs
).
insertContent
(
'
'
).
run
()
}
}
break
}
setModalIsOpen
(
false
)
clearBlobUrl
()
...
...
@@ -1324,6 +1537,7 @@ const QEditor = ({
setEmbedContent
(
''
)
setUploadedPaths
([])
setModalTitle
(
''
)
setButtonLinkData
(
defaultButtonLinkData
)
}
catch
(
err
)
{
console
.
log
(
err
)
setModalIsOpen
(
false
)
...
...
@@ -1332,6 +1546,7 @@ const QEditor = ({
setEmbedContent
(
''
)
setUploadedPaths
([])
setModalTitle
(
''
)
setButtonLinkData
(
defaultButtonLinkData
)
}
}
},
...
...
@@ -1348,6 +1563,14 @@ const QEditor = ({
typpyOptions
=
{
{
followCursor
:
true
}
}
editor
=
{
editor
}
shouldShow
=
{
({...
o
})
=>
{
if
(
o
.
state
.
selection
instanceof
NodeSelection
)
{
const
selectedNode
=
o
.
state
.
selection
.
node
if
(
selectedNode
&&
selectedNode
.
type
&&
selectedNode
.
type
.
name
===
'
buttonLink
'
)
{
return
false
}
}
let
items
=
[]
if
(
o
.
from
!==
o
.
to
&&
...
...
src/components/ToolBar.js
View file @
78d6243d
...
...
@@ -44,6 +44,7 @@ const toolsInit = {
items
:
[
'
emoji
'
,
'
link
'
,
'
buttonLink
'
,
'
file
'
,
'
image
'
,
'
interactiveImage
'
,
...
...
src/extensions/ButtonLink.js
0 → 100644
View file @
78d6243d
import
{
Node
}
from
'
@tiptap/core
'
import
{
Plugin
}
from
'
prosemirror-state
'
import
{
NodeSelection
}
from
'
prosemirror-state
'
const
ButtonLink
=
Node
.
create
({
name
:
'
buttonLink
'
,
group
:
'
inline
'
,
inline
:
true
,
atom
:
true
,
selectable
:
true
,
addAttributes
()
{
return
{
text
:
{
default
:
'
Кнопка
'
},
href
:
{
default
:
''
},
target
:
{
default
:
'
_blank
'
},
textColor
:
{
default
:
'
#ffffff
'
},
backgroundColor
:
{
default
:
'
#1790FF
'
},
fontSize
:
{
default
:
'
16px
'
}
}
},
parseHTML
()
{
return
[
{
tag
:
'
a[data-button-link="true"]
'
,
getAttrs
:
(
element
)
=>
({
text
:
element
.
textContent
||
'
Кнопка
'
,
href
:
element
.
getAttribute
(
'
href
'
)
||
''
,
target
:
element
.
getAttribute
(
'
target
'
)
||
'
_blank
'
,
textColor
:
element
.
getAttribute
(
'
data-text-color
'
)
||
'
#ffffff
'
,
backgroundColor
:
element
.
getAttribute
(
'
data-background-color
'
)
||
'
#1790FF
'
,
fontSize
:
element
.
getAttribute
(
'
data-font-size
'
)
||
'
16px
'
})
}
]
},
renderHTML
({
HTMLAttributes
})
{
const
{
text
,
href
,
target
,
textColor
,
backgroundColor
,
fontSize
}
=
HTMLAttributes
return
[
'
a
'
,
{
href
,
target
,
rel
:
'
noopener noreferrer nofollow
'
,
'
data-button-link
'
:
'
true
'
,
'
data-text-color
'
:
textColor
,
'
data-background-color
'
:
backgroundColor
,
'
data-font-size
'
:
fontSize
,
style
:
[
'
display: inline-block
'
,
'
padding: 10px 18px
'
,
'
border-radius: 10px
'
,
'
text-decoration: none
'
,
`font-size:
${
fontSize
}
`
,
`color:
${
textColor
}
`
,
`background-color:
${
backgroundColor
}
`
].
join
(
'
;
'
)
},
text
]
},
addCommands
()
{
return
{
setButtonLink
:
(
attrs
)
=>
({
commands
})
=>
{
return
commands
.
insertContent
({
type
:
this
.
name
,
attrs
})
}
}
},
addProseMirrorPlugins
()
{
return
[
new
Plugin
({
props
:
{
handleDOMEvents
:
{
click
:
(
view
,
event
)
=>
{
const
target
=
event
.
target
instanceof
HTMLElement
?
event
.
target
.
closest
(
'
a[data-button-link="true"]
'
)
:
null
if
(
!
target
||
!
view
.
dom
.
contains
(
target
))
{
return
false
}
event
.
preventDefault
()
return
true
},
touchstart
:
(
view
,
event
)
=>
{
const
target
=
event
.
target
instanceof
HTMLElement
?
event
.
target
.
closest
(
'
a[data-button-link="true"]
'
)
:
null
if
(
!
target
||
!
view
.
dom
.
contains
(
target
))
{
return
false
}
event
.
preventDefault
()
return
true
},
mousedown
:
(
view
,
event
)
=>
{
const
target
=
event
.
target
instanceof
HTMLElement
?
event
.
target
.
closest
(
'
a[data-button-link="true"]
'
)
:
null
if
(
!
target
||
!
view
.
dom
.
contains
(
target
))
{
return
false
}
const
position
=
view
.
posAtDOM
(
target
,
0
)
const
node
=
view
.
state
.
doc
.
nodeAt
(
position
)
if
(
!
node
||
node
.
type
.
name
!==
this
.
name
)
{
return
false
}
event
.
preventDefault
()
view
.
dispatch
(
view
.
state
.
tr
.
setSelection
(
NodeSelection
.
create
(
view
.
state
.
doc
,
position
)
)
)
view
.
focus
()
return
true
}
},
handleClickOn
:
(
view
,
pos
,
node
,
nodePos
,
event
,
direct
)
=>
{
if
(
!
direct
||
node
.
type
.
name
!==
this
.
name
)
{
return
false
}
event
.
preventDefault
()
const
transaction
=
view
.
state
.
tr
.
setSelection
(
NodeSelection
.
create
(
view
.
state
.
doc
,
nodePos
)
)
view
.
dispatch
(
transaction
)
view
.
focus
()
return
true
}
}
})
]
}
})
export
default
ButtonLink
src/index.scss
View file @
78d6243d
...
...
@@ -529,6 +529,15 @@ body{
}
}
a[data-button-link="true"]{
cursor: pointer;
}
.ProseMirror-selectednode[data-button-link="true"]{
outline: 2px solid #1790FF;
outline-offset: 2px;
}
}
&-bubble{
...
...
@@ -637,6 +646,68 @@ body{
}
}
&-field{
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
span{
font-size: 13px;
color: #4f4f4f;
}
&-color{
align-items: flex-start;
}
}
&-color-input{
width: 72px;
min-width: 72px;
height: 36px;
border: 1px solid #d9d9d9;
border-radius: 8px;
background: #fff;
padding: 3px;
}
&-button-link{
&-form{
min-width: 420px;
}
&-grid{
display: grid;
grid-template-columns: minmax(0, 1.3fr) repeat(2, auto);
gap: 16px;
align-items: start;
}
&-preview-wrap{
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 8px;
span{
font-size: 13px;
color: #4f4f4f;
}
}
&-preview{
display: inline-flex;
align-items: center;
justify-content: center;
align-self: flex-start;
min-height: 42px;
border-radius: 10px;
padding: 10px 18px;
text-decoration: none;
}
}
&-uploader{
&-drop{
display: flex;
...
...
@@ -913,6 +984,9 @@ body{
.qicon{
&.qbuttonLink{
background-image: url("data:image/svg+xml,%3Csvg width='
18
' height='
18
' viewBox='
0
0
18
18
' fill='
none
' xmlns='
http
:
//
www
.w3.org
/
2000
/
svg
'%3E%3Crect x='
2
.25
' y='
4
' width='
13
.5
' height='
10
' rx='
2
.6
' stroke='
%231D1D1F
' stroke-width='
1
.4
'/%3E%3Cpath d='
M5
.2
9H8
.8
' stroke='
%231D1D1F
' stroke-width='
1
.4
' stroke-linecap='
round
'/%3E%3Cpath d='
M10
.3
7
.4H11.7C12.42
7
.4
13
7
.98
13
8
.7C13
9
.42
12
.42
10
11
.7
10H10
.3
' stroke='
%231D1D1F
' stroke-width='
1
.2
' stroke-linecap='
round
'/%3E%3Cpath d='
M9
.8
10
.9L11.1
9
.6
' stroke='
%231D1D1F
' stroke-width='
1
.2
' stroke-linecap='
round
'/%3E%3C/svg%3E");
}
&.qfontSize{
background-image: url("data:image/svg+xml,%3Csvg width='
18
' height='
18
' viewBox='
0
0
18
18
' fill='
none
' xmlns='
http
:
//
www
.w3.org
/
2000
/
svg
'%3E%3Cpath d='
M4
.2
13
.8L6.84
5
.4H7.98L10.62
13
.8H9.51L8.88
11
.76H5.91L5.28
13
.8H4.2ZM6.15
10
.86H8.64L7.41
6
.84L6.15
10
.86ZM11.58
13
.8V8.43H12.6V9.12C13.02
8
.58
13
.59
8
.31
14
.34
8
.31C14.73
8
.31
15
.09
8
.4
15
.39
8
.58C15.69
8
.76
15
.93
9
.03
16
.08
9
.39C16.23
9
.75
16
.32
10
.17
16
.32
10
.65C16.32
11
.16
16
.23
11
.61
16
.05
12C15
.87
12
.39
15
.6
12
.69
15
.24
12
.9C14.88
13
.11
14
.49
13
.2
14
.07
13
.2C13.47
13
.2
12
.99
12
.99
12
.66
12
.6V13.8H11.58ZM12.6
10
.74C12.6
11
.37
12
.72
11
.82
12
.96
12
.12C13.2
12
.42
13
.5
12
.57
13
.89
12
.57C14.28
12
.57
14
.61
12
.42
14
.88
12
.09C15.15
11
.76
15
.27
11
.28
15
.27
10
.62C15.27
10
.02
15
.15
9
.57
14
.91
9
.27C14.67
8
.97
14
.37
8
.82
14
.01
8
.82C13.65
8
.82
13
.32
8
.97
13
.05
9
.3C12.75
9
.6
12
.6
10
.08
12
.6
10
.74Z
' fill='
%231D1D1F
'/%3E%3C/svg%3E");
}
...
...
@@ -1409,8 +1483,21 @@ body{
color: #8c8c8c;
font-size: 13px;
}
.atma-editor-field-select {
width: 110px
}
@media (max-width: 768px) {
.atma-editor-button-link-form{
min-width: 0;
}
.atma-editor-button-link-grid{
grid-template-columns: 1fr 1fr;
}
.atma-editor-field-color{
align-items: flex-start;
}
.atma-editor-toolbar{
width: 100%;
max-width: 100%;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment