Add WYSIWYG Markdown editor (EasyMDE) to wiki pages
Deploy to LXC / deploy (push) Successful in 21s

Reusable MarkdownEditor component using EasyMDE:
- Toolbar: bold, italic, headings, quotes, lists, links, images,
  tables, code blocks, preview, side-by-side, fullscreen
- Dark mode support with custom CSS overrides
- Value syncs via bind:value for form submission
- Applied to both wiki new and edit pages
- Replaces the plain textarea + manual preview toggle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 16:03:37 +07:00
co-authored by Claude Opus 4.6
parent f937394b5e
commit bc73595018
5 changed files with 189 additions and 46 deletions
+111
View File
@@ -0,0 +1,111 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment';
interface Props {
name?: string;
value?: string;
placeholder?: string;
}
let { name = 'content', value = $bindable(''), placeholder = 'Write in Markdown...' }: Props = $props();
let textareaEl: HTMLTextAreaElement;
let editor: any = null;
onMount(async () => {
if (!browser) return;
const EasyMDE = (await import('easymde')).default;
await import('easymde/dist/easymde.min.css');
editor = new EasyMDE({
element: textareaEl,
initialValue: value,
spellChecker: false,
autofocus: false,
placeholder,
status: false,
minHeight: '300px',
toolbar: [
'bold', 'italic', 'heading', '|',
'quote', 'unordered-list', 'ordered-list', '|',
'link', 'image', 'table', 'code', '|',
'preview', 'side-by-side', 'fullscreen', '|',
'guide'
],
sideBySideFullscreen: false
});
editor.codemirror.on('change', () => {
value = editor.value();
});
});
onDestroy(() => {
if (editor) {
editor.toTextArea();
editor = null;
}
});
</script>
<div class="markdown-editor">
<textarea bind:this={textareaEl} id={name}></textarea>
<input type="hidden" {name} value={value} />
</div>
<style>
.markdown-editor :global(.EasyMDEContainer) {
border: none;
}
.markdown-editor :global(.EasyMDEContainer .CodeMirror) {
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-family: ui-monospace, monospace;
font-size: 0.875rem;
}
.markdown-editor :global(.editor-toolbar) {
border: 1px solid #d1d5db;
border-bottom: none;
border-radius: 0.375rem 0.375rem 0 0;
opacity: 1;
}
.markdown-editor :global(.editor-toolbar button) {
color: #6b7280 !important;
}
.markdown-editor :global(.editor-toolbar button:hover) {
background: #f3f4f6;
}
.markdown-editor :global(.editor-toolbar button.active) {
background: #e5e7eb;
}
/* Dark mode */
:global(.dark) .markdown-editor :global(.CodeMirror) {
background: #374151;
color: #f3f4f6;
border-color: #4b5563;
}
:global(.dark) .markdown-editor :global(.editor-toolbar) {
background: #1f2937;
border-color: #4b5563;
}
:global(.dark) .markdown-editor :global(.editor-toolbar button) {
color: #9ca3af !important;
}
:global(.dark) .markdown-editor :global(.editor-toolbar button:hover) {
background: #374151;
}
:global(.dark) .markdown-editor :global(.CodeMirror-cursor) {
border-color: #f3f4f6;
}
:global(.dark) .markdown-editor :global(.editor-preview) {
background: #374151;
color: #f3f4f6;
}
:global(.dark) .markdown-editor :global(.editor-preview-side) {
background: #374151;
color: #f3f4f6;
border-color: #4b5563;
}
</style>
+3 -22
View File
@@ -1,14 +1,11 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { marked } from 'marked';
import TagInput from '$lib/components/ui/TagInput.svelte';
import MarkdownEditor from '$lib/components/ui/MarkdownEditor.svelte';
let { data, form } = $props();
let content = $state(data.page.content);
let showPreview = $state(false);
const renderedContent = $derived(marked(content) as string);
</script>
<svelte:head>
@@ -54,24 +51,8 @@
</div>
<div>
<div class="mb-1 flex items-center justify-between">
<label for="content" class="text-sm font-medium text-gray-700 dark:text-gray-300">Content * (Markdown)</label>
<button type="button" onclick={() => (showPreview = !showPreview)}
class="rounded-md px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20">
{showPreview ? 'Editor' : 'Preview'}
</button>
</div>
{#if showPreview}
<div class="prose prose-sm dark:prose-invert min-h-[300px] max-w-none rounded-md border border-gray-300 bg-white p-4 dark:border-gray-600 dark:bg-gray-700">
{@html renderedContent}
</div>
<input type="hidden" name="content" value={content} />
{:else}
<textarea id="content" name="content" rows="20" required
bind:value={content}
class="w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white"></textarea>
{/if}
<span class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Content * (Markdown)</span>
<MarkdownEditor bind:value={content} />
</div>
<div class="flex items-center gap-3">
+3 -23
View File
@@ -1,14 +1,11 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { marked } from 'marked';
import TagInput from '$lib/components/ui/TagInput.svelte';
import MarkdownEditor from '$lib/components/ui/MarkdownEditor.svelte';
let { data, form } = $props();
let content = $state('');
let showPreview = $state(false);
const renderedContent = $derived(marked(content) as string);
</script>
<svelte:head>
@@ -54,25 +51,8 @@
<!-- Markdown editor -->
<div>
<div class="mb-1 flex items-center justify-between">
<label for="content" class="text-sm font-medium text-gray-700 dark:text-gray-300">Content * (Markdown)</label>
<button type="button" onclick={() => (showPreview = !showPreview)}
class="rounded-md px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20">
{showPreview ? 'Editor' : 'Preview'}
</button>
</div>
{#if showPreview}
<div class="prose prose-sm dark:prose-invert min-h-[300px] max-w-none rounded-md border border-gray-300 bg-white p-4 dark:border-gray-600 dark:bg-gray-700">
{@html renderedContent}
</div>
<input type="hidden" name="content" value={content} />
{:else}
<textarea id="content" name="content" rows="15" required
bind:value={content}
class="w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"
placeholder="# Getting Started&#10;&#10;Write your guide in Markdown...&#10;&#10;## Steps&#10;&#10;1. First step&#10;2. Second step&#10;&#10;**Bold text** and `code` are supported."></textarea>
{/if}
<span class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Content * (Markdown)</span>
<MarkdownEditor bind:value={content} placeholder="Write your guide in Markdown..." />
</div>
<div class="flex items-center gap-3">