Enhances user experience with dark‑mode detection, responsive layout, and streamlined role selection workflow
160 lines
12 KiB
HTML
160 lines
12 KiB
HTML
<!-- Alpine -->
|
||
|
||
|
||
<!-- CodeMirror 6 bundle via esm.sh -->
|
||
|
||
|
||
|
||
|
||
|
||
<div class="editor-wrap" x-data="editor()" x-init="init(); content = {% if alpine_model %}{{ alpine_model }}{% else %}'{{ initial_value|escapejs }}'{% endif %}; {% if alpine_model %}$watch('{{ alpine_model }}', value => { if (value !== undefined) { content = value; } }){% endif %}" @click.away="closePickers()">
|
||
<input type="hidden" name="{{ input_name }}" :value="content">
|
||
|
||
<!-- ════════════ MARKDOWN MODE ════════════ -->
|
||
<div>
|
||
<!-- Tabs -->
|
||
<div class="tabs">
|
||
<button type="button" class="tab-btn" :class="{active: tab==='write'}" @click="tab='write'">Write</button>
|
||
<button type="button" class="tab-btn" :class="{active: tab==='preview'}" @click="tab='preview'; renderPreview()">Preview</button>
|
||
</div>
|
||
|
||
<!-- Toolbar -->
|
||
<template x-if="tab==='write'">
|
||
<div class="toolbar">
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Heading" @click="wrap('### ','')"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><text x="0" y="13" font-family="monospace" font-size="13" font-weight="700">H</text></svg></button>
|
||
<div class="toolbar-sep"></div>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Bold (Ctrl+B)" @click="wrap('**','**')"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M4 2h4.5a3 3 0 012.2 5A3.5 3.5 0 019 14H4V2zm2 5h2.5a1 1 0 000-2H6v2zm0 5h3a1.5 1.5 0 000-3H6v3z"/></svg></button>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Italic (Ctrl+I)" @click="wrap('_','_')"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M6 2h5v2H9.5L7 12h1.5v2H3v-2h1.5L7 4H5.5V2z"/></svg></button>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Strikethrough" @click="wrap('~~','~~')"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1a4 4 0 00-3.3 6.2c.5.6 1.2 1 2 1.3H2v1.5h12V8.5H9.3c.8-.3 1.5-.7 2-1.3A4 4 0 008 1zm0 1.5a2.5 2.5 0 010 5 2.5 2.5 0 010-5z"/></svg></button>
|
||
<div class="toolbar-sep"></div>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Inline code" @click="wrap('`','`')"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M4.72 3.22a.75.75 0 011.06 1.06L2.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L.47 8.53a.75.75 0 010-1.06l4.25-4.25zm6.56 0a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 11-1.06-1.06L15.06 8l-3.78-3.72a.75.75 0 010-1.06z"/></svg></button>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Code block" @click="insertCodeBlock()"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75z"/></svg></button>
|
||
<div class="toolbar-sep"></div>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Link (Ctrl+K)" @click="insertLink()"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"/></svg></button>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Upload image" @click="this.$refs.fileInput.click()"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M1.75 2.5a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h.94l.03-.083 1.68-4.2a.75.75 0 011.404.017l1.984 5.566.986-1.972a.75.75 0 011.342 0l1.138 2.278H14.25a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25H1.75z"/></svg></button>
|
||
<div class="toolbar-sep"></div>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Bullet list" @click="insertList('- ')"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M2 4a1 1 0 100-2 1 1 0 000 2zm3.75-1.5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zm0 5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zm0 5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zM3 8a1 1 0 11-2 0 1 1 0 012 0zm-1 6a1 1 0 100-2 1 1 0 000 2z"/></svg></button>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Numbered list" @click="insertList('1. ')"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M5 3.25a.75.75 0 01.75-.75h8.5a.75.75 0 010 1.5h-8.5A.75.75 0 015 3.25zm0 5a.75.75 0 01.75-.75h8.5a.75.75 0 010 1.5h-8.5A.75.75 0 015 8.25zm0 5a.75.75 0 01.75-.75h8.5a.75.75 0 010 1.5h-8.5A.75.75 0 015 13.25z"/></svg></button>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Task list" @click="insertList('- [ ] ')"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M2.5 1.75v11.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V1.75a.25.25 0 00-.25-.25H2.75a.25.25 0 00-.25.25z"/></svg></button>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Quote" @click="insertList('> ')"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M1.75 2.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h6.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25H1.75z"/></svg></button>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Table" @click="insertTable()"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0114.25 16H1.75A1.75 1.75 0 010 14.25V1.75zM1.5 5v8.75c0 .138.112.25.25.25H5.5V5H1.5zm4 0v9H14.25a.25.25 0 00.25-.25V5H5.5zm0-1.5h9V1.75a.25.25 0 00-.25-.25H5.5V3.5zm-4 0H5V1.5H1.75a.25.25 0 00-.25.25V3.5z"/></svg></button>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Divider" @click="insertHR()"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M0 7.75A.75.75 0 01.75 7h14.5a.75.75 0 010 1.5H.75A.75.75 0 010 7.75z"/></svg></button>
|
||
<div class="toolbar-sep"></div>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Mention (@)" @click="insertMentionTrigger()"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M10.5 5a5 5 0 11-9.8 1.5 5 5 0 019.8-1.5zM8 8a3 3 0 10-2.41-4.8 3 3 0 002.41 4.8z"/></svg></button>
|
||
<button type="button" class="tool-btn" data-editor-tooltip="Emoji" @click.stop="toggleEmojiPicker($event)"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0a8 8 0 100 16A8 8 0 008 0zM1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM5 6.75a.75.75 0 111.5 0 .75.75 0 01-1.5 0zm4.5 0a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM5.32 10.12a.75.75 0 011.06-.04 2.5 2.5 0 003.24 0 .75.75 0 011.02 1.1 4 4 0 01-5.28 0 .75.75 0 01-.04-1.06z"/></svg></button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Write panel -->
|
||
<template x-if="tab==='write'">
|
||
<div class="write-panel"
|
||
:class="{'drag-over': isDragging}"
|
||
@dragenter.prevent="isDragging=true"
|
||
@dragover.prevent="isDragging=true"
|
||
@dragleave.prevent="isDragging=false"
|
||
@drop.prevent="handleDrop($event)">
|
||
<textarea
|
||
|
||
x-ref="ta"
|
||
x-model="content"
|
||
x-effect="content; $nextTick(() => autoResize())"
|
||
@keydown="handleKeydown($event)"
|
||
@input="handleInput($event)"
|
||
@paste="handlePaste($event)"
|
||
placeholder="Leave a comment… Type @ to mention someone"
|
||
spellcheck="false"
|
||
></textarea>
|
||
<div class="drag-overlay" x-show="isDragging">
|
||
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor"><path d="M1.75 2.5a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h.94l.03-.083 1.68-4.2a.75.75 0 011.404.017l1.984 5.566.986-1.972a.75.75 0 011.342 0l1.138 2.278H14.25a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25H1.75z"/></svg>
|
||
Drop image to upload
|
||
</div>
|
||
<div class="attach-bar">
|
||
<span class="attach-hint">Attach images by dragging, pasting, or</span>
|
||
<label class="attach-label" for="file-input">
|
||
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M8 2a.75.75 0 01.75.75v4.5h4.5a.75.75 0 010 1.5h-4.5v4.5a.75.75 0 01-1.5 0v-4.5h-4.5a.75.75 0 010-1.5h4.5v-4.5A.75.75 0 018 2z"/></svg>
|
||
selecting
|
||
</label>
|
||
<input type="file" x-ref="fileInput" accept="image/*" multiple @change="handleFileInput($event)">
|
||
<span class="char-count" x-text="content.length + ' chars'"></span>
|
||
</div>
|
||
<div class="img-previews" x-show="uploads.length > 0">
|
||
<template x-for="(img, idx) in uploads" :key="img.id">
|
||
<div class="img-thumb">
|
||
<img :src="img.dataUrl" :alt="img.name">
|
||
<button type="button" class="remove-btn" @click="removeUpload(idx)">×</button>
|
||
<div class="uploading-bar" x-show="img.uploading" :style="`width:${img.progress}%`"></div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Preview panel -->
|
||
<template x-if="tab==='preview'">
|
||
<div class="preview-panel">
|
||
<div x-show="!preview" class="preview-empty">Nothing to preview.</div>
|
||
<div class="md-preview" x-show="preview" x-html="preview"></div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
|
||
<!-- Mention dropdown (shared) -->
|
||
<div class="mention-dropdown"
|
||
x-show="showMention && filteredUsers.length > 0"
|
||
:style="`top:${mentionPos.top}px; left:${mentionPos.left}px`"
|
||
@click.stop>
|
||
<template x-for="(user, idx) in filteredUsers" :key="user.handle">
|
||
<div class="mention-item" :class="{active: mentionIndex === idx}"
|
||
@click="selectMention(user)" @mouseenter="mentionIndex = idx">
|
||
<div class="mention-avatar" :style="`background:${user.color}`">
|
||
<span x-text="user.name[0].toUpperCase()"></span>
|
||
</div>
|
||
<div class="mention-info">
|
||
<span class="mention-name" x-text="user.name"></span>
|
||
<span class="mention-handle" x-text="'@' + user.handle"></span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- Emoji picker (shared) -->
|
||
<div class="emoji-picker" x-show="showEmoji" :style="`top:${emojiPos.top}px; left:${emojiPos.left}px`" @click.stop>
|
||
<div class="emoji-search-wrap">
|
||
<input class="emoji-search" type="text" placeholder="Search emoji…" x-model="emojiSearch" @input="filterEmoji()" x-ref="emojiInput">
|
||
</div>
|
||
<template x-for="cat in filteredEmojiCats" :key="cat.name">
|
||
<div>
|
||
<div class="emoji-category" x-text="cat.name"></div>
|
||
<div class="emoji-grid">
|
||
<template x-for="e in cat.emojis" :key="e">
|
||
<button type="button" class="emoji-btn" @click="insertEmoji(e)" x-text="e"></button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
|
||
<div class="editor-wrap-toast" x-ref="toast">
|
||
<span x-ref="toastIcon"></span>
|
||
<span x-ref="toastMsg"></span>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="footer-bar">
|
||
<a href="#" class="md-hint">
|
||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"/></svg>
|
||
Markdown supported
|
||
</a>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|