add rich text editor

Replace the admin rich text textarea with a Tiptap editor while keeping the existing HTML string save contract. Add public-page styles for the editor output.
This commit is contained in:
Yisroel Baum 2026-06-21 21:22:39 +03:00
parent cdc29b795a
commit b4b3927ab1
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
5 changed files with 1735 additions and 13 deletions

View file

@ -0,0 +1,970 @@
<script setup lang="ts">
import Highlight from '@tiptap/extension-highlight'
import Link from '@tiptap/extension-link'
import {
Table,
TableCell,
TableHeader,
TableRow,
} from '@tiptap/extension-table'
import TextAlign from '@tiptap/extension-text-align'
import Underline from '@tiptap/extension-underline'
import StarterKit from '@tiptap/starter-kit'
import type { Editor, JSONContent } from '@tiptap/core'
import { EditorContent, useEditor } from '@tiptap/vue-3'
import {
AlignCenter,
AlignJustify,
AlignLeft,
AlignRight,
Bold,
Columns3,
Highlighter,
Italic,
Link2,
List,
ListOrdered,
Minus,
Plus,
Quote,
Redo2,
Rows3,
Strikethrough,
Table2,
TableCellsMerge,
TableCellsSplit,
Trash2,
Underline as UnderlineIcon,
Undo2,
Unlink2,
} from 'lucide-vue-next'
import { computed, watch } from 'vue'
const props = defineProps<{
modelValue: string
disabled: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
type HeadingLevel = 2 | 3 | 4
type TextAlignment = 'left' | 'center' | 'right' | 'justify'
const allowedLinkProtocols = ['http:', 'https:', 'mailto:', 'tel:']
const editor = useEditor({
content: props.modelValue,
editable: !props.disabled,
extensions: [
StarterKit.configure({
heading: {
levels: [2, 3, 4],
},
link: false,
underline: false,
}),
Underline,
Highlight,
Link.configure({
autolink: true,
defaultProtocol: 'https',
HTMLAttributes: {
rel: 'noreferrer',
target: '_blank',
},
isAllowedUri: (linkUrl) => {
return isAllowedLinkUrl(linkUrl)
},
linkOnPaste: true,
openOnClick: false,
protocols: ['http', 'https', 'mailto', 'tel'],
shouldAutoLink: (linkUrl) => {
return isAllowedLinkUrl(linkUrl)
},
}),
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
TableCell,
],
editorProps: {
attributes: {
'aria-label': 'Rich text editor',
class: 'rich-text-editor__surface',
'data-cy': 'admin-element-rich-text',
},
},
onUpdate: ({ editor: updatedEditor }) => {
emit('update:modelValue', getEditorHtml(updatedEditor))
},
})
const currentBlockType = computed(() => {
const editorInstance = editor.value
if (editorInstance === undefined) {
return 'paragraph'
}
if (editorInstance.isActive('heading', { level: 2 })) {
return 'heading-2'
}
if (editorInstance.isActive('heading', { level: 3 })) {
return 'heading-3'
}
if (editorInstance.isActive('heading', { level: 4 })) {
return 'heading-4'
}
return 'paragraph'
})
const isInTable = computed(() => {
const editorInstance = editor.value
if (editorInstance === undefined) {
return false
}
return editorInstance.isActive('table')
|| editorInstance.isActive('tableCell')
|| editorInstance.isActive('tableHeader')
|| editorInstance.isActive('tableRow')
})
watch(
() => props.modelValue,
(currentValue) => {
const editorInstance = editor.value
if (editorInstance === undefined) {
return
}
if (getEditorHtml(editorInstance) === currentValue) {
return
}
editorInstance.commands.setContent(currentValue, {
emitUpdate: false,
})
},
)
watch(
() => props.disabled,
(isDisabled) => {
const editorInstance = editor.value
if (editorInstance === undefined) {
return
}
editorInstance.setEditable(!isDisabled)
},
)
function getEditorHtml(editorInstance: Editor): string {
if (editorInstance.isEmpty) {
return ''
}
return editorInstance.getHTML()
}
function isAllowedLinkUrl(linkUrl: string): boolean {
const normalizedUrl = normalizeLinkUrl(linkUrl)
return normalizedUrl !== null
}
function normalizeLinkUrl(linkUrl: string): string | null {
const trimmedUrl = linkUrl.trim()
if (trimmedUrl === '') {
return null
}
let parsedUrl: URL
try {
parsedUrl = new URL(trimmedUrl)
} catch {
return null
}
if (!allowedLinkProtocols.includes(parsedUrl.protocol)) {
return null
}
return parsedUrl.toString()
}
function isEditorDisabled(): boolean {
return props.disabled || editor.value === undefined
}
function isMarkActive(markName: string): boolean {
const editorInstance = editor.value
if (editorInstance === undefined) {
return false
}
return editorInstance.isActive(markName)
}
function isAlignmentActive(alignment: TextAlignment): boolean {
const editorInstance = editor.value
if (editorInstance === undefined) {
return false
}
return editorInstance.isActive({ textAlign: alignment })
}
function handleBlockTypeChange(changeEvent: Event): void {
const target = changeEvent.target
if (!(target instanceof HTMLSelectElement)) {
return
}
if (target.value === 'paragraph') {
setParagraph()
return
}
if (target.value === 'heading-2') {
setHeading(2)
return
}
if (target.value === 'heading-3') {
setHeading(3)
return
}
if (target.value === 'heading-4') {
setHeading(4)
}
}
function setParagraph(): void {
editor.value?.chain().focus().setParagraph().run()
}
function setHeading(headingLevel: HeadingLevel): void {
editor.value?.chain().focus().setHeading({ level: headingLevel }).run()
}
function toggleBold(): void {
editor.value?.chain().focus().toggleBold().run()
}
function toggleItalic(): void {
editor.value?.chain().focus().toggleItalic().run()
}
function toggleUnderline(): void {
editor.value?.chain().focus().toggleUnderline().run()
}
function toggleStrike(): void {
editor.value?.chain().focus().toggleStrike().run()
}
function toggleHighlight(): void {
editor.value?.chain().focus().toggleHighlight().run()
}
function setTextAlign(alignment: TextAlignment): void {
editor.value?.chain().focus().setTextAlign(alignment).run()
}
function toggleBulletList(): void {
editor.value?.chain().focus().toggleBulletList().run()
}
function toggleOrderedList(): void {
editor.value?.chain().focus().toggleOrderedList().run()
}
function toggleBlockquote(): void {
editor.value?.chain().focus().toggleBlockquote().run()
}
function setHorizontalRule(): void {
editor.value?.chain().focus().setHorizontalRule().run()
}
function applyLink(): void {
const editorInstance = editor.value
if (editorInstance === undefined) {
return
}
const linkUrl = window.prompt('Link URL', currentLinkHref(editorInstance))
if (linkUrl === null) {
return
}
const normalizedUrl = normalizeLinkUrl(linkUrl)
if (normalizedUrl === null) {
return
}
if (editorInstance.state.selection.empty) {
insertLinkedText(editorInstance, normalizedUrl)
return
}
editorInstance
.chain()
.focus()
.extendMarkRange('link')
.setLink({ href: normalizedUrl })
.run()
}
function currentLinkHref(editorInstance: Editor): string {
const linkAttributes = editorInstance.getAttributes('link') as Record<
string,
unknown
>
const href = linkAttributes.href
return typeof href === 'string' ? href : ''
}
function insertLinkedText(editorInstance: Editor, linkUrl: string): void {
const linkedText: JSONContent = {
marks: [
{
attrs: {
href: linkUrl,
},
type: 'link',
},
],
text: linkUrl,
type: 'text',
}
editorInstance.chain().focus().insertContent(linkedText).run()
}
function unsetLink(): void {
editor.value?.chain().focus().unsetLink().run()
}
function undo(): void {
editor.value?.chain().focus().undo().run()
}
function redo(): void {
editor.value?.chain().focus().redo().run()
}
function insertTable(): void {
editor.value
?.chain()
.focus()
.insertTable({
cols: 3,
rows: 3,
withHeaderRow: true,
})
.run()
}
function addColumnAfter(): void {
editor.value?.chain().focus().addColumnAfter().run()
}
function deleteColumn(): void {
editor.value?.chain().focus().deleteColumn().run()
}
function addRowAfter(): void {
editor.value?.chain().focus().addRowAfter().run()
}
function deleteRow(): void {
editor.value?.chain().focus().deleteRow().run()
}
function toggleHeaderRow(): void {
editor.value?.chain().focus().toggleHeaderRow().run()
}
function mergeOrSplitCells(): void {
editor.value?.chain().focus().mergeOrSplit().run()
}
function splitCell(): void {
editor.value?.chain().focus().splitCell().run()
}
function deleteTable(): void {
editor.value?.chain().focus().deleteTable().run()
}
</script>
<template>
<div class="rich-text-editor">
<div class="rich-text-editor__toolbar" aria-label="Rich text controls">
<select
class="rich-text-editor__select"
data-cy="admin-rich-text-block-type"
:disabled="isEditorDisabled()"
:value="currentBlockType"
aria-label="Block type"
@change="handleBlockTypeChange"
>
<option value="paragraph">Paragraph</option>
<option value="heading-2">Heading 2</option>
<option value="heading-3">Heading 3</option>
<option value="heading-4">Heading 4</option>
</select>
<div class="rich-text-editor__button-group">
<button
class="rich-text-editor__button"
data-cy="admin-rich-text-bold"
type="button"
:aria-pressed="isMarkActive('bold')"
:class="{ 'rich-text-editor__button--active': isMarkActive('bold') }"
:disabled="isEditorDisabled()"
aria-label="Bold"
title="Bold"
@click="toggleBold"
>
<Bold :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:aria-pressed="isMarkActive('italic')"
:class="{
'rich-text-editor__button--active': isMarkActive('italic'),
}"
:disabled="isEditorDisabled()"
aria-label="Italic"
title="Italic"
@click="toggleItalic"
>
<Italic :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:aria-pressed="isMarkActive('underline')"
:class="{
'rich-text-editor__button--active': isMarkActive('underline'),
}"
:disabled="isEditorDisabled()"
aria-label="Underline"
title="Underline"
@click="toggleUnderline"
>
<UnderlineIcon :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:aria-pressed="isMarkActive('strike')"
:class="{
'rich-text-editor__button--active': isMarkActive('strike'),
}"
:disabled="isEditorDisabled()"
aria-label="Strike"
title="Strike"
@click="toggleStrike"
>
<Strikethrough :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:aria-pressed="isMarkActive('highlight')"
:class="{
'rich-text-editor__button--active': isMarkActive('highlight'),
}"
:disabled="isEditorDisabled()"
aria-label="Highlight"
title="Highlight"
@click="toggleHighlight"
>
<Highlighter :size="16" aria-hidden="true" />
</button>
</div>
<div class="rich-text-editor__button-group">
<button
class="rich-text-editor__button"
type="button"
:aria-pressed="isAlignmentActive('left')"
:class="{
'rich-text-editor__button--active': isAlignmentActive('left'),
}"
:disabled="isEditorDisabled()"
aria-label="Align left"
title="Align left"
@click="setTextAlign('left')"
>
<AlignLeft :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:aria-pressed="isAlignmentActive('center')"
:class="{
'rich-text-editor__button--active': isAlignmentActive('center'),
}"
:disabled="isEditorDisabled()"
aria-label="Align center"
title="Align center"
@click="setTextAlign('center')"
>
<AlignCenter :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:aria-pressed="isAlignmentActive('right')"
:class="{
'rich-text-editor__button--active': isAlignmentActive('right'),
}"
:disabled="isEditorDisabled()"
aria-label="Align right"
title="Align right"
@click="setTextAlign('right')"
>
<AlignRight :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:aria-pressed="isAlignmentActive('justify')"
:class="{
'rich-text-editor__button--active': isAlignmentActive('justify'),
}"
:disabled="isEditorDisabled()"
aria-label="Justify"
title="Justify"
@click="setTextAlign('justify')"
>
<AlignJustify :size="16" aria-hidden="true" />
</button>
</div>
<div class="rich-text-editor__button-group">
<button
class="rich-text-editor__button"
data-cy="admin-rich-text-bullet-list"
type="button"
:aria-pressed="isMarkActive('bulletList')"
:class="{
'rich-text-editor__button--active': isMarkActive('bulletList'),
}"
:disabled="isEditorDisabled()"
aria-label="Bullet list"
title="Bullet list"
@click="toggleBulletList"
>
<List :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:aria-pressed="isMarkActive('orderedList')"
:class="{
'rich-text-editor__button--active': isMarkActive('orderedList'),
}"
:disabled="isEditorDisabled()"
aria-label="Ordered list"
title="Ordered list"
@click="toggleOrderedList"
>
<ListOrdered :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:aria-pressed="isMarkActive('blockquote')"
:class="{
'rich-text-editor__button--active': isMarkActive('blockquote'),
}"
:disabled="isEditorDisabled()"
aria-label="Blockquote"
title="Blockquote"
@click="toggleBlockquote"
>
<Quote :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:disabled="isEditorDisabled()"
aria-label="Horizontal rule"
title="Horizontal rule"
@click="setHorizontalRule"
>
<Minus :size="16" aria-hidden="true" />
</button>
</div>
<div class="rich-text-editor__button-group">
<button
class="rich-text-editor__button"
data-cy="admin-rich-text-link"
type="button"
:aria-pressed="isMarkActive('link')"
:class="{ 'rich-text-editor__button--active': isMarkActive('link') }"
:disabled="isEditorDisabled()"
aria-label="Link"
title="Link"
@click="applyLink"
>
<Link2 :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:disabled="isEditorDisabled()"
aria-label="Remove link"
title="Remove link"
@click="unsetLink"
>
<Unlink2 :size="16" aria-hidden="true" />
</button>
</div>
<div class="rich-text-editor__button-group">
<button
class="rich-text-editor__button"
data-cy="admin-rich-text-insert-table"
type="button"
:disabled="isEditorDisabled()"
aria-label="Insert table"
title="Insert table"
@click="insertTable"
>
<Table2 :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:disabled="isEditorDisabled() || !isInTable"
aria-label="Add column"
title="Add column"
@click="addColumnAfter"
>
<Columns3 :size="16" aria-hidden="true" />
<Plus :size="11" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:disabled="isEditorDisabled() || !isInTable"
aria-label="Delete column"
title="Delete column"
@click="deleteColumn"
>
<Columns3 :size="16" aria-hidden="true" />
<Trash2 :size="11" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:disabled="isEditorDisabled() || !isInTable"
aria-label="Add row"
title="Add row"
@click="addRowAfter"
>
<Rows3 :size="16" aria-hidden="true" />
<Plus :size="11" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:disabled="isEditorDisabled() || !isInTable"
aria-label="Delete row"
title="Delete row"
@click="deleteRow"
>
<Rows3 :size="16" aria-hidden="true" />
<Trash2 :size="11" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:disabled="isEditorDisabled() || !isInTable"
aria-label="Toggle header row"
title="Toggle header row"
@click="toggleHeaderRow"
>
<Rows3 :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:disabled="isEditorDisabled() || !isInTable"
aria-label="Merge cells"
title="Merge cells"
@click="mergeOrSplitCells"
>
<TableCellsMerge :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:disabled="isEditorDisabled() || !isInTable"
aria-label="Split cell"
title="Split cell"
@click="splitCell"
>
<TableCellsSplit :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:disabled="isEditorDisabled() || !isInTable"
aria-label="Delete table"
title="Delete table"
@click="deleteTable"
>
<Trash2 :size="16" aria-hidden="true" />
</button>
</div>
<div class="rich-text-editor__button-group">
<button
class="rich-text-editor__button"
type="button"
:disabled="isEditorDisabled()"
aria-label="Undo"
title="Undo"
@click="undo"
>
<Undo2 :size="16" aria-hidden="true" />
</button>
<button
class="rich-text-editor__button"
type="button"
:disabled="isEditorDisabled()"
aria-label="Redo"
title="Redo"
@click="redo"
>
<Redo2 :size="16" aria-hidden="true" />
</button>
</div>
</div>
<EditorContent :editor="editor" class="rich-text-editor__content" />
</div>
</template>
<style scoped>
.rich-text-editor {
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-white);
}
.rich-text-editor:focus-within {
border-color: var(--color-slate);
}
.rich-text-editor__toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.45rem;
padding: 0.55rem;
border-bottom: 1px solid var(--color-border);
background: #fbfaf6;
}
.rich-text-editor__select {
min-height: 2.2rem;
padding: 0 0.55rem;
border: 1px solid var(--color-border);
border-radius: 4px;
color: var(--color-text);
background: var(--color-white);
font-family: var(--font-sans);
font-size: 0.86rem;
}
.rich-text-editor__button-group {
display: inline-flex;
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-white);
}
.rich-text-editor__button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.15rem;
height: 2.15rem;
padding: 0 0.45rem;
border: 0;
border-right: 1px solid var(--color-border);
color: var(--color-text-muted);
background: transparent;
}
.rich-text-editor__button:last-child {
border-right: 0;
}
.rich-text-editor__button:hover:not(:disabled),
.rich-text-editor__button:focus-visible,
.rich-text-editor__button--active {
color: var(--color-white);
background: var(--color-slate);
outline: none;
}
.rich-text-editor__button:disabled,
.rich-text-editor__select:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.rich-text-editor__content {
background: var(--color-white);
}
.rich-text-editor__content :deep(.rich-text-editor__surface) {
min-height: 15rem;
padding: 0.85rem 0.95rem;
color: var(--color-text);
font-family: var(--font-sans);
font-size: 0.98rem;
line-height: 1.65;
outline: none;
}
.rich-text-editor__content :deep(.rich-text-editor__surface > *:first-child) {
margin-top: 0;
}
.rich-text-editor__content :deep(.rich-text-editor__surface > *:last-child) {
margin-bottom: 0;
}
.rich-text-editor__content :deep(h2),
.rich-text-editor__content :deep(h3),
.rich-text-editor__content :deep(h4) {
margin: 1.1rem 0 0.45rem;
color: #2c2c2c;
font-family: var(--font-serif);
font-weight: 500;
line-height: 1.25;
}
.rich-text-editor__content :deep(h2) {
font-size: 1.55rem;
}
.rich-text-editor__content :deep(h3) {
font-size: 1.3rem;
}
.rich-text-editor__content :deep(h4) {
font-size: 1.1rem;
}
.rich-text-editor__content :deep(p) {
margin: 0 0 0.75rem;
}
.rich-text-editor__content :deep(ul),
.rich-text-editor__content :deep(ol) {
margin: 0 0 0.85rem;
padding-left: 1.45rem;
}
.rich-text-editor__content :deep(blockquote) {
margin: 1rem 0;
padding: 0.1rem 0 0.1rem 1rem;
border-left: 3px solid var(--color-olive);
color: var(--color-text-muted);
}
.rich-text-editor__content :deep(mark) {
padding: 0 0.16em;
background: #fff3a7;
}
.rich-text-editor__content :deep(a) {
color: var(--color-slate);
font-weight: 600;
text-decoration: underline;
text-underline-offset: 0.16em;
}
.rich-text-editor__content :deep(hr) {
margin: 1.2rem 0;
border: 0;
border-top: 1px solid var(--color-border);
}
.rich-text-editor__content :deep(.tableWrapper) {
max-width: 100%;
margin: 1rem 0;
overflow-x: auto;
}
.rich-text-editor__content :deep(table) {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.rich-text-editor__content :deep(th),
.rich-text-editor__content :deep(td) {
min-width: 4rem;
padding: 0.45rem 0.55rem;
border: 1px solid var(--color-border);
vertical-align: top;
}
.rich-text-editor__content :deep(th) {
background: #f6f3eb;
font-weight: 700;
}
.rich-text-editor__content :deep(.selectedCell) {
position: relative;
}
.rich-text-editor__content :deep(.selectedCell::after) {
position: absolute;
inset: 0;
z-index: 2;
pointer-events: none;
content: '';
background: rgb(43 63 78 / 12%);
}
@media (max-width: 768px) {
.rich-text-editor__toolbar {
align-items: stretch;
}
.rich-text-editor__select {
width: 100%;
}
}
</style>

View file

@ -2,6 +2,7 @@
import { storeToRefs } from 'pinia'
import { computed, reactive, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import RichTextEditor from '@/components/RichTextEditor.vue'
import SiteHeader from '@/components/SiteHeader.vue'
import { useElementsStore } from '@/stores/elements'
@ -325,15 +326,10 @@ function resetInput(changeEvent: Event): void {
</p>
</section>
<label class="admin-element-page__field">
<span class="admin-element-page__label">Rich Text HTML</span>
<textarea
v-model="form.richText"
class="admin-element-page__textarea admin-element-page__code"
data-cy="admin-element-rich-text"
rows="9"
/>
</label>
<div class="admin-element-page__field">
<span class="admin-element-page__label">Rich Text</span>
<RichTextEditor v-model="form.richText" :disabled="isSaving" />
</div>
<section class="admin-element-page__media-field">
<span class="admin-element-page__label">Short PDF</span>
@ -559,10 +555,6 @@ function resetInput(changeEvent: Event): void {
resize: vertical;
}
.admin-element-page__code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.admin-element-page__icon-preview {
width: 6rem;
height: 6rem;

View file

@ -352,11 +352,93 @@ function isShortYoutubeHost(hostname: string): boolean {
margin-bottom: 0;
}
.element-page__rich-text :deep(h2),
.element-page__rich-text :deep(h3),
.element-page__rich-text :deep(h4) {
margin: 1.6rem 0 0.65rem;
color: #2c2c2c;
font-family: var(--font-serif);
font-weight: 500;
line-height: 1.25;
}
.element-page__rich-text :deep(h2) {
font-size: 1.75rem;
}
.element-page__rich-text :deep(h3) {
font-size: 1.45rem;
}
.element-page__rich-text :deep(h4) {
font-size: 1.2rem;
}
.element-page__rich-text :deep(ul),
.element-page__rich-text :deep(ol) {
margin: 0 0 1rem;
padding-left: 1.45rem;
}
.element-page__rich-text :deep(li) {
margin: 0.25rem 0;
}
.element-page__rich-text :deep(blockquote) {
margin: 1.35rem 0;
padding: 0.2rem 0 0.2rem 1.1rem;
border-left: 3px solid var(--color-olive);
color: var(--color-text-muted);
}
.element-page__rich-text :deep(strong) {
color: #2c2c2c;
font-weight: 700;
}
.element-page__rich-text :deep(mark) {
padding: 0 0.16em;
background: #fff3a7;
}
.element-page__rich-text :deep(a) {
color: var(--color-slate);
font-weight: 600;
text-decoration: underline;
text-underline-offset: 0.16em;
}
.element-page__rich-text :deep(hr) {
margin: 1.5rem 0;
border: 0;
border-top: 1px solid var(--color-border);
}
.element-page__rich-text :deep(.tableWrapper) {
max-width: 100%;
margin: 1.4rem 0;
overflow-x: auto;
}
.element-page__rich-text :deep(table) {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.element-page__rich-text :deep(th),
.element-page__rich-text :deep(td) {
min-width: 4rem;
padding: 0.55rem 0.65rem;
border: 1px solid var(--color-border);
vertical-align: top;
}
.element-page__rich-text :deep(th) {
background: #f6f3eb;
font-weight: 700;
}
.element-page__youtube {
aspect-ratio: 16 / 9;
margin-top: 1.75rem;