fix: edit table and migrations
This commit is contained in:
99
bo/src/components/ui/RichEditor.vue
Normal file
99
bo/src/components/ui/RichEditor.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="rich-editor rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden focus-within:ring-1 focus-within:ring-sky-500 focus-within:border-sky-500 transition-colors">
|
||||
<!-- Toolbar -->
|
||||
<div class="flex items-center gap-0.5 px-2 py-1.5 border-b border-(--border-light) dark:border-(--border-dark) bg-gray-50 dark:bg-neutral-800 flex-wrap">
|
||||
<button type="button" @click="editor?.chain().focus().toggleBold().run()"
|
||||
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('bold') }"
|
||||
class="toolbar-btn font-bold">B</button>
|
||||
<button type="button" @click="editor?.chain().focus().toggleItalic().run()"
|
||||
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('italic') }"
|
||||
class="toolbar-btn italic">I</button>
|
||||
<button type="button" @click="editor?.chain().focus().toggleStrike().run()"
|
||||
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('strike') }"
|
||||
class="toolbar-btn line-through">S</button>
|
||||
<div class="w-px h-4 bg-gray-300 dark:bg-neutral-600 mx-1" />
|
||||
<button type="button" @click="editor?.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('heading', { level: 2 }) }"
|
||||
class="toolbar-btn text-xs font-semibold">H2</button>
|
||||
<button type="button" @click="editor?.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('heading', { level: 3 }) }"
|
||||
class="toolbar-btn text-xs font-semibold">H3</button>
|
||||
<div class="w-px h-4 bg-gray-300 dark:bg-neutral-600 mx-1" />
|
||||
<button type="button" @click="editor?.chain().focus().toggleBulletList().run()"
|
||||
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('bulletList') }"
|
||||
class="toolbar-btn">
|
||||
<UIcon name="i-lucide-list" class="text-sm" />
|
||||
</button>
|
||||
<button type="button" @click="editor?.chain().focus().toggleOrderedList().run()"
|
||||
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('orderedList') }"
|
||||
class="toolbar-btn">
|
||||
<UIcon name="i-lucide-list-ordered" class="text-sm" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-gray-300 dark:bg-neutral-600 mx-1" />
|
||||
<button type="button" @click="editor?.chain().focus().undo().run()" class="toolbar-btn">
|
||||
<UIcon name="i-lucide-undo-2" class="text-sm" />
|
||||
</button>
|
||||
<button type="button" @click="editor?.chain().focus().redo().run()" class="toolbar-btn">
|
||||
<UIcon name="i-lucide-redo-2" class="text-sm" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Editor area -->
|
||||
<EditorContent :editor="editor"
|
||||
class="min-h-32 px-3 py-2.5 text-sm text-black dark:text-white bg-white dark:bg-neutral-900 prose prose-sm dark:prose-invert max-w-none focus:outline-none" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, onBeforeUnmount } from 'vue'
|
||||
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const editor = useEditor({
|
||||
content: props.modelValue,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Placeholder.configure({ placeholder: props.placeholder ?? '' }),
|
||||
],
|
||||
editorProps: {
|
||||
attributes: { class: 'focus:outline-none' },
|
||||
},
|
||||
onUpdate({ editor }) {
|
||||
emit('update:modelValue', editor.getHTML())
|
||||
},
|
||||
})
|
||||
|
||||
// Sync external changes (e.g. store reset)
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (editor.value && editor.value.getHTML() !== val) {
|
||||
editor.value.commands.setContent(val, false)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => editor.value?.destroy())
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* .toolbar-btn {
|
||||
@apply flex items-center justify-center w-7 h-7 rounded text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-neutral-700 transition-colors text-sm;
|
||||
} */
|
||||
|
||||
/* Tiptap placeholder */
|
||||
.tiptap p.is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
/* color: theme('colors.gray.400'); */
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user