Ajoute deux boutons à la toolbar avec popover en palette pour appliquer une couleur de texte ou un surlignage sur la sélection. - Extensions TipTap : @tiptap/extension-text-style, @tiptap/extension-color, @tiptap/extension-highlight (multicolor). - Palette de 8 couleurs (texte) + 8 pastels (surlignage) + reset. - Indicateur de couleur active sous l'icône. - Fermeture du popover sur clic extérieur, Echap, ou clic dans l'éditeur. - Inclut les améliorations rendu/markdown du commit précédent (default outputFormat html, normalizeEditorInput, styles deep pour h2/h3/p/ul/ol/blockquote). - Tests : 4 nouveaux cas (15 au total). - Story et COMPONENTS.md à jour. Note : les couleurs ne sont pas sérialisables en markdown ; pour les conserver au save/reload utiliser output-format=\"html\". Co-Authored-By: RuFlo <ruv@ruv.net>
575 lines
20 KiB
Vue
575 lines
20 KiB
Vue
<template>
|
|
<div :class="mergedGroupClass">
|
|
<label
|
|
v-if="label"
|
|
:for="editorId"
|
|
:class="mergedLabelClass"
|
|
>
|
|
{{ label }}
|
|
</label>
|
|
|
|
<!-- Mode lecture seule (rendu uniquement) -->
|
|
<div
|
|
v-if="!editable"
|
|
:id="editorId"
|
|
:class="mergedReadonlyClass"
|
|
>
|
|
<EditorContent :editor="editor" />
|
|
</div>
|
|
|
|
<!-- Mode éditable -->
|
|
<div
|
|
v-else
|
|
:id="editorId"
|
|
:class="mergedEditorWrapperClass"
|
|
@click="focusEditor"
|
|
>
|
|
<div
|
|
class="flex flex-wrap items-center gap-0.5 border-b border-m-border bg-m-bg p-1"
|
|
@click.stop
|
|
>
|
|
<button
|
|
v-for="btn in toolbarButtons"
|
|
:key="btn.key"
|
|
type="button"
|
|
class="flex h-8 w-8 items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
|
|
:class="btn.isActive() ? 'bg-m-primary/15 text-m-primary' : ''"
|
|
:title="btn.title"
|
|
:disabled="disabled || readonly"
|
|
:aria-label="btn.title"
|
|
:aria-pressed="btn.isActive()"
|
|
@mousedown.prevent
|
|
@click="btn.action()"
|
|
>
|
|
<IconifyIcon :icon="btn.icon" :width="18" :height="18" />
|
|
</button>
|
|
|
|
<span class="mx-1 h-5 w-px bg-m-border" aria-hidden="true" />
|
|
|
|
<div class="relative">
|
|
<button
|
|
type="button"
|
|
class="flex h-8 w-8 flex-col items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
|
|
:class="colorPickerOpen ? 'bg-m-primary/15 text-m-primary' : ''"
|
|
title="Couleur du texte"
|
|
aria-label="Couleur du texte"
|
|
:aria-expanded="colorPickerOpen"
|
|
:disabled="disabled || readonly"
|
|
@mousedown.prevent
|
|
@click="toggleColorPicker"
|
|
>
|
|
<IconifyIcon icon="mdi:format-color-text" :width="18" :height="18" />
|
|
<span
|
|
class="-mt-0.5 block h-1 w-4 rounded-sm"
|
|
:style="{ backgroundColor: currentTextColor ?? 'transparent' }"
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
<div
|
|
v-if="colorPickerOpen"
|
|
class="absolute left-0 top-full z-10 mt-1 flex w-44 flex-col gap-2 rounded-md border border-m-border bg-white p-2 shadow-lg"
|
|
role="dialog"
|
|
aria-label="Palette couleur du texte"
|
|
>
|
|
<div class="grid grid-cols-4 gap-1">
|
|
<button
|
|
v-for="swatch in textColorSwatches"
|
|
:key="swatch.value"
|
|
type="button"
|
|
class="h-7 w-7 rounded border border-m-border transition-transform hover:scale-110"
|
|
:class="currentTextColor === swatch.value ? 'ring-2 ring-m-primary ring-offset-1' : ''"
|
|
:style="{ backgroundColor: swatch.value }"
|
|
:title="swatch.label"
|
|
:aria-label="swatch.label"
|
|
@mousedown.prevent
|
|
@click="applyTextColor(swatch.value)"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="flex items-center justify-center gap-1 rounded border border-m-border px-2 py-1 text-xs text-m-text transition-colors hover:bg-m-bg"
|
|
@mousedown.prevent
|
|
@click="applyTextColor(null)"
|
|
>
|
|
<IconifyIcon icon="mdi:format-color-marker-cancel" :width="14" :height="14" />
|
|
Aucune couleur
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative">
|
|
<button
|
|
type="button"
|
|
class="flex h-8 w-8 flex-col items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
|
|
:class="highlightPickerOpen ? 'bg-m-primary/15 text-m-primary' : ''"
|
|
title="Surlignage"
|
|
aria-label="Surlignage"
|
|
:aria-expanded="highlightPickerOpen"
|
|
:disabled="disabled || readonly"
|
|
@mousedown.prevent
|
|
@click="toggleHighlightPicker"
|
|
>
|
|
<IconifyIcon icon="mdi:marker" :width="18" :height="18" />
|
|
<span
|
|
class="-mt-0.5 block h-1 w-4 rounded-sm"
|
|
:style="{ backgroundColor: currentHighlightColor ?? 'transparent' }"
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
<div
|
|
v-if="highlightPickerOpen"
|
|
class="absolute left-0 top-full z-10 mt-1 flex w-44 flex-col gap-2 rounded-md border border-m-border bg-white p-2 shadow-lg"
|
|
role="dialog"
|
|
aria-label="Palette de surlignage"
|
|
>
|
|
<div class="grid grid-cols-4 gap-1">
|
|
<button
|
|
v-for="swatch in highlightSwatches"
|
|
:key="swatch.value"
|
|
type="button"
|
|
class="h-7 w-7 rounded border border-m-border transition-transform hover:scale-110"
|
|
:class="currentHighlightColor === swatch.value ? 'ring-2 ring-m-primary ring-offset-1' : ''"
|
|
:style="{ backgroundColor: swatch.value }"
|
|
:title="swatch.label"
|
|
:aria-label="swatch.label"
|
|
@mousedown.prevent
|
|
@click="applyHighlight(swatch.value)"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="flex items-center justify-center gap-1 rounded border border-m-border px-2 py-1 text-xs text-m-text transition-colors hover:bg-m-bg"
|
|
@mousedown.prevent
|
|
@click="applyHighlight(null)"
|
|
>
|
|
<IconifyIcon icon="mdi:format-color-marker-cancel" :width="14" :height="14" />
|
|
Aucun surlignage
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<span class="mx-1 h-5 w-px bg-m-border" aria-hidden="true" />
|
|
|
|
<button
|
|
type="button"
|
|
class="flex h-8 w-8 items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
|
|
title="Annuler"
|
|
aria-label="Annuler"
|
|
:disabled="disabled || readonly || !editor?.can().undo()"
|
|
@mousedown.prevent
|
|
@click="editor?.chain().focus().undo().run()"
|
|
>
|
|
<IconifyIcon icon="mdi:undo" :width="18" :height="18" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="flex h-8 w-8 items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
|
|
title="Rétablir"
|
|
aria-label="Rétablir"
|
|
:disabled="disabled || readonly || !editor?.can().redo()"
|
|
@mousedown.prevent
|
|
@click="editor?.chain().focus().redo().run()"
|
|
>
|
|
<IconifyIcon icon="mdi:redo" :width="18" :height="18" />
|
|
</button>
|
|
</div>
|
|
|
|
<EditorContent
|
|
:editor="editor"
|
|
class="malio-rich-text flex flex-1 cursor-text"
|
|
:style="{ minHeight }"
|
|
:aria-invalid="hasError || undefined"
|
|
:aria-describedby="describedBy"
|
|
/>
|
|
</div>
|
|
|
|
<p
|
|
v-if="hint || hasError || hasSuccess"
|
|
:id="`${editorId}-describedby`"
|
|
:class="[
|
|
hasError
|
|
? 'text-m-danger'
|
|
: hasSuccess
|
|
? 'text-m-success'
|
|
: 'text-m-muted',
|
|
'mt-1 text-xs ml-[2px]',
|
|
]"
|
|
>
|
|
{{ error || success || hint }}
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onBeforeUnmount, onMounted, ref, shallowRef, useId, watch } from 'vue'
|
|
import { Icon as IconifyIcon } from '@iconify/vue'
|
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
|
import StarterKit from '@tiptap/starter-kit'
|
|
import Placeholder from '@tiptap/extension-placeholder'
|
|
import { TextStyle } from '@tiptap/extension-text-style'
|
|
import Color from '@tiptap/extension-color'
|
|
import Highlight from '@tiptap/extension-highlight'
|
|
import { Markdown } from 'tiptap-markdown'
|
|
import { twMerge } from 'tailwind-merge'
|
|
|
|
defineOptions({ name: 'MalioInputRichText', inheritAttrs: false })
|
|
|
|
type OutputFormat = 'markdown' | 'html'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
id?: string
|
|
label?: string
|
|
modelValue?: string | null | undefined
|
|
placeholder?: string
|
|
minHeight?: string
|
|
editable?: boolean
|
|
disabled?: boolean
|
|
readonly?: boolean
|
|
hint?: string
|
|
error?: string
|
|
success?: string
|
|
outputFormat?: OutputFormat
|
|
groupClass?: string
|
|
labelClass?: string
|
|
editorClass?: string
|
|
}>(),
|
|
{
|
|
id: '',
|
|
label: '',
|
|
modelValue: undefined,
|
|
placeholder: '',
|
|
minHeight: '160px',
|
|
editable: true,
|
|
disabled: false,
|
|
readonly: false,
|
|
hint: '',
|
|
error: '',
|
|
success: '',
|
|
outputFormat: 'html',
|
|
groupClass: '',
|
|
labelClass: '',
|
|
editorClass: '',
|
|
},
|
|
)
|
|
|
|
const emit = defineEmits<{
|
|
(event: 'update:modelValue', value: string): void
|
|
}>()
|
|
|
|
const generatedId = useId()
|
|
const editor = shallowRef<Editor>()
|
|
const isFocused = shallowRef(false)
|
|
|
|
const editorId = computed(() => props.id?.toString() || `malio-input-rich-text-${generatedId}`)
|
|
const hasError = computed(() => !!props.error)
|
|
const hasSuccess = computed(() => !!props.success && !hasError.value)
|
|
const isInteractionLocked = computed(() => props.disabled || props.readonly)
|
|
|
|
const describedBy = computed(() =>
|
|
hasError.value || hasSuccess.value || props.hint ? `${editorId.value}-describedby` : undefined,
|
|
)
|
|
|
|
const mergedGroupClass = computed(() => twMerge('w-full', props.groupClass))
|
|
|
|
const mergedLabelClass = computed(() =>
|
|
twMerge(
|
|
'mb-1 block text-sm font-medium',
|
|
hasError.value
|
|
? 'text-m-danger'
|
|
: hasSuccess.value
|
|
? 'text-m-success'
|
|
: isFocused.value
|
|
? 'text-m-primary'
|
|
: 'text-m-text',
|
|
props.disabled ? 'text-black/60' : '',
|
|
props.labelClass,
|
|
),
|
|
)
|
|
|
|
const mergedEditorWrapperClass = computed(() =>
|
|
twMerge(
|
|
'rich-text-wrapper flex flex-col overflow-hidden rounded-md border bg-white transition-colors',
|
|
hasError.value
|
|
? 'border-m-danger focus-within:border-m-danger'
|
|
: hasSuccess.value
|
|
? 'border-m-success focus-within:border-m-success'
|
|
: isFocused.value
|
|
? 'border-m-primary'
|
|
: 'border-m-muted hover:border-m-text/60',
|
|
props.disabled ? 'cursor-not-allowed bg-m-bg/50 opacity-70' : '',
|
|
props.editorClass,
|
|
),
|
|
)
|
|
|
|
const mergedReadonlyClass = computed(() =>
|
|
twMerge(
|
|
'malio-rich-text prose prose-sm max-w-none rounded-md border border-m-border bg-white p-3',
|
|
'prose-headings:font-semibold prose-a:text-m-primary',
|
|
'prose-code:rounded prose-code:bg-m-bg prose-code:px-1.5 prose-code:py-0.5 prose-code:before:content-none prose-code:after:content-none',
|
|
'prose-pre:bg-m-text prose-pre:text-white',
|
|
props.editorClass,
|
|
),
|
|
)
|
|
|
|
const focusEditor = () => {
|
|
if (isInteractionLocked.value) return
|
|
closePickers()
|
|
editor.value?.commands.focus()
|
|
}
|
|
|
|
const htmlPattern = /<\/?[a-z][\s\S]*>/i
|
|
|
|
const normalizeEditorInput = (value: string | null | undefined): string => {
|
|
const content = (value ?? '').replace(/\r\n?/g, '\n')
|
|
if (htmlPattern.test(content)) return content
|
|
return content.split('\n').join('\n\n').replace(/\n{3,}/g, '\n\n')
|
|
}
|
|
|
|
const promptForLink = () => {
|
|
if (!editor.value) return
|
|
const previous = editor.value.getAttributes('link').href as string | undefined
|
|
const url = window.prompt('URL du lien (vide pour retirer)', previous ?? '')
|
|
if (url === null) return
|
|
if (url === '') {
|
|
editor.value.chain().focus().extendMarkRange('link').unsetLink().run()
|
|
return
|
|
}
|
|
editor.value.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
|
}
|
|
|
|
type ColorSwatch = { label: string; value: string }
|
|
|
|
const textColorSwatches: ColorSwatch[] = [
|
|
{ label: 'Rouge', value: '#bf2600' },
|
|
{ label: 'Orange', value: '#ff8b00' },
|
|
{ label: 'Jaune', value: '#ffc400' },
|
|
{ label: 'Vert', value: '#00875a' },
|
|
{ label: 'Turquoise', value: '#00a3bf' },
|
|
{ label: 'Bleu', value: '#0747a6' },
|
|
{ label: 'Violet', value: '#5243aa' },
|
|
{ label: 'Gris', value: '#42526e' },
|
|
]
|
|
|
|
const highlightSwatches: ColorSwatch[] = [
|
|
{ label: 'Rouge', value: '#fdd0c8' },
|
|
{ label: 'Orange', value: '#ffe2c2' },
|
|
{ label: 'Jaune', value: '#fff0b3' },
|
|
{ label: 'Vert', value: '#c6edd0' },
|
|
{ label: 'Turquoise', value: '#c1ecf0' },
|
|
{ label: 'Bleu', value: '#cce0ff' },
|
|
{ label: 'Violet', value: '#dfd8fa' },
|
|
{ label: 'Gris', value: '#dfe1e6' },
|
|
]
|
|
|
|
const colorPickerOpen = ref(false)
|
|
const highlightPickerOpen = ref(false)
|
|
|
|
const closePickers = () => {
|
|
colorPickerOpen.value = false
|
|
highlightPickerOpen.value = false
|
|
}
|
|
|
|
const toggleColorPicker = () => {
|
|
highlightPickerOpen.value = false
|
|
colorPickerOpen.value = !colorPickerOpen.value
|
|
}
|
|
|
|
const toggleHighlightPicker = () => {
|
|
colorPickerOpen.value = false
|
|
highlightPickerOpen.value = !highlightPickerOpen.value
|
|
}
|
|
|
|
const applyTextColor = (value: string | null) => {
|
|
if (!editor.value) return
|
|
if (value === null) {
|
|
editor.value.chain().focus().unsetColor().run()
|
|
} else {
|
|
editor.value.chain().focus().setColor(value).run()
|
|
}
|
|
colorPickerOpen.value = false
|
|
}
|
|
|
|
const applyHighlight = (value: string | null) => {
|
|
if (!editor.value) return
|
|
if (value === null) {
|
|
editor.value.chain().focus().unsetHighlight().run()
|
|
} else {
|
|
editor.value.chain().focus().setHighlight({ color: value }).run()
|
|
}
|
|
highlightPickerOpen.value = false
|
|
}
|
|
|
|
const currentTextColor = computed(() => {
|
|
const attrs = editor.value?.getAttributes('textStyle') as { color?: string } | undefined
|
|
return attrs?.color ?? null
|
|
})
|
|
|
|
const currentHighlightColor = computed(() => {
|
|
const attrs = editor.value?.getAttributes('highlight') as { color?: string } | undefined
|
|
return attrs?.color ?? null
|
|
})
|
|
|
|
const toolbarButtons = computed(() => {
|
|
const e = editor.value
|
|
return [
|
|
{ key: 'bold', icon: 'mdi:format-bold', title: 'Gras', isActive: () => !!e?.isActive('bold'), action: () => e?.chain().focus().toggleBold().run() },
|
|
{ key: 'italic', icon: 'mdi:format-italic', title: 'Italique', isActive: () => !!e?.isActive('italic'), action: () => e?.chain().focus().toggleItalic().run() },
|
|
{ key: 'strike', icon: 'mdi:format-strikethrough', title: 'Barré', isActive: () => !!e?.isActive('strike'), action: () => e?.chain().focus().toggleStrike().run() },
|
|
{ key: 'h2', icon: 'mdi:format-header-2', title: 'Titre H2', isActive: () => !!e?.isActive('heading', { level: 2 }), action: () => e?.chain().focus().toggleHeading({ level: 2 }).run() },
|
|
{ key: 'h3', icon: 'mdi:format-header-3', title: 'Titre H3', isActive: () => !!e?.isActive('heading', { level: 3 }), action: () => e?.chain().focus().toggleHeading({ level: 3 }).run() },
|
|
{ key: 'bulletList', icon: 'mdi:format-list-bulleted', title: 'Liste à puces', isActive: () => !!e?.isActive('bulletList'), action: () => e?.chain().focus().toggleBulletList().run() },
|
|
{ key: 'orderedList', icon: 'mdi:format-list-numbered', title: 'Liste numérotée', isActive: () => !!e?.isActive('orderedList'), action: () => e?.chain().focus().toggleOrderedList().run() },
|
|
{ key: 'blockquote', icon: 'mdi:format-quote-close', title: 'Citation', isActive: () => !!e?.isActive('blockquote'), action: () => e?.chain().focus().toggleBlockquote().run() },
|
|
{ key: 'code', icon: 'mdi:code-tags', title: 'Code inline', isActive: () => !!e?.isActive('code'), action: () => e?.chain().focus().toggleCode().run() },
|
|
{ key: 'codeBlock', icon: 'mdi:code-braces-box', title: 'Bloc de code', isActive: () => !!e?.isActive('codeBlock'), action: () => e?.chain().focus().toggleCodeBlock().run() },
|
|
{ key: 'link', icon: 'mdi:link-variant', title: 'Lien', isActive: () => !!e?.isActive('link'), action: promptForLink },
|
|
]
|
|
})
|
|
|
|
const getCurrentValue = (): string => {
|
|
if (!editor.value) return ''
|
|
if (props.outputFormat === 'html') return editor.value.getHTML()
|
|
const storage = (editor.value.storage as unknown as Record<string, { getMarkdown?: () => string } | undefined>).markdown
|
|
return storage?.getMarkdown ? storage.getMarkdown() : editor.value.getHTML()
|
|
}
|
|
|
|
const handleDocumentMousedown = (event: MouseEvent) => {
|
|
if (!colorPickerOpen.value && !highlightPickerOpen.value) return
|
|
const target = event.target as Node | null
|
|
if (!target) return
|
|
const popovers = document.querySelectorAll(`#${editorId.value} [role="dialog"]`)
|
|
const triggers = document.querySelectorAll(`#${editorId.value} [aria-expanded]`)
|
|
for (const node of [...popovers, ...triggers]) {
|
|
if (node.contains(target)) return
|
|
}
|
|
closePickers()
|
|
}
|
|
|
|
const handleDocumentKeydown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape' && (colorPickerOpen.value || highlightPickerOpen.value)) {
|
|
closePickers()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('mousedown', handleDocumentMousedown)
|
|
document.addEventListener('keydown', handleDocumentKeydown)
|
|
|
|
editor.value = new Editor({
|
|
content: normalizeEditorInput(props.modelValue),
|
|
editable: props.editable && !props.disabled && !props.readonly,
|
|
extensions: [
|
|
StarterKit.configure({
|
|
heading: { levels: [2, 3] },
|
|
link: {
|
|
openOnClick: false,
|
|
autolink: true,
|
|
HTMLAttributes: { rel: 'noopener noreferrer nofollow', target: '_blank' },
|
|
},
|
|
}),
|
|
TextStyle,
|
|
Color.configure({ types: ['textStyle'] }),
|
|
Highlight.configure({ multicolor: true }),
|
|
Placeholder.configure({
|
|
placeholder: props.placeholder,
|
|
}),
|
|
Markdown.configure({
|
|
html: true,
|
|
tightLists: true,
|
|
bulletListMarker: '-',
|
|
linkify: true,
|
|
breaks: false,
|
|
transformPastedText: true,
|
|
transformCopiedText: true,
|
|
}),
|
|
],
|
|
editorProps: {
|
|
attributes: {
|
|
class: 'prose prose-sm max-w-none w-full p-3 focus:outline-none prose-headings:font-semibold prose-a:text-m-primary prose-code:rounded prose-code:bg-m-bg prose-code:px-1.5 prose-code:py-0.5 prose-code:before:content-none prose-code:after:content-none prose-pre:bg-m-text prose-pre:text-white',
|
|
},
|
|
},
|
|
onUpdate: () => {
|
|
emit('update:modelValue', getCurrentValue())
|
|
},
|
|
onFocus: () => {
|
|
isFocused.value = true
|
|
},
|
|
onBlur: () => {
|
|
isFocused.value = false
|
|
},
|
|
})
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
document.removeEventListener('mousedown', handleDocumentMousedown)
|
|
document.removeEventListener('keydown', handleDocumentKeydown)
|
|
editor.value?.destroy()
|
|
})
|
|
|
|
watch(() => props.modelValue, (incoming) => {
|
|
if (!editor.value) return
|
|
if ((incoming ?? '') === getCurrentValue()) return
|
|
editor.value.commands.setContent(normalizeEditorInput(incoming), { emitUpdate: false })
|
|
})
|
|
|
|
watch(() => [props.editable, props.disabled, props.readonly], () => {
|
|
editor.value?.setEditable(props.editable && !props.disabled && !props.readonly)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.malio-rich-text :deep(.ProseMirror) {
|
|
outline: none;
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
.malio-rich-text :deep(.ProseMirror > *:first-child) {
|
|
margin-top: 0;
|
|
}
|
|
.malio-rich-text :deep(.ProseMirror > *:last-child) {
|
|
margin-bottom: 0;
|
|
}
|
|
.malio-rich-text :deep(.ProseMirror p.is-editor-empty:first-child::before) {
|
|
content: attr(data-placeholder);
|
|
float: left;
|
|
color: rgb(var(--m-muted));
|
|
pointer-events: none;
|
|
height: 0;
|
|
}
|
|
.malio-rich-text :deep(h2) {
|
|
margin: 0.75rem 0 0.5rem;
|
|
font-size: 1.5rem;
|
|
line-height: 2rem;
|
|
font-weight: 700;
|
|
color: rgb(var(--m-text));
|
|
}
|
|
.malio-rich-text :deep(h3) {
|
|
margin: 0.65rem 0 0.4rem;
|
|
font-size: 1.25rem;
|
|
line-height: 1.75rem;
|
|
font-weight: 700;
|
|
color: rgb(var(--m-text));
|
|
}
|
|
.malio-rich-text :deep(p) {
|
|
margin: 0.45rem 0;
|
|
}
|
|
.malio-rich-text :deep(ul),
|
|
.malio-rich-text :deep(ol) {
|
|
margin: 0.5rem 0;
|
|
padding-left: 1.5rem;
|
|
}
|
|
.malio-rich-text :deep(ul) {
|
|
list-style: disc;
|
|
}
|
|
.malio-rich-text :deep(ol) {
|
|
list-style: decimal;
|
|
}
|
|
.malio-rich-text :deep(blockquote) {
|
|
margin: 0.75rem 0;
|
|
border-left: 3px solid rgb(var(--m-border));
|
|
padding-left: 0.75rem;
|
|
color: rgb(var(--m-muted));
|
|
}
|
|
</style>
|