Files
project-managment/templates/components/rich_text_editor.html
T
sankalp 71b700388b feat(templates): add polished login UI with Tailwind and Alpine.js
Enhances user experience with dark‑mode detection, responsive layout, and streamlined role selection workflow
2026-06-28 14:15:39 +05:45

160 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 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>