Replace contenteditable div with textarea for real markdown editing. insertAtCursor, wrapSelection, replaceLineAtCursor. 18 slash commands map to real markdown syntax. Bubble menu Bold/Italic/Strike/Code/Highlight wrap selection. Ctrl+B/I shortcuts. Word+char count.
869 lines
40 KiB
HTML
869 lines
40 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>ObsiGate — Editor POC v2 (Markdown Textarea)</title>
|
|
<style>
|
|
:root {
|
|
--bg-primary: #0f1117;
|
|
--bg-secondary: #161822;
|
|
--bg-tertiary: #1c1f2e;
|
|
--bg-elevated: #222639;
|
|
--bg-hover: #2a2d3e;
|
|
--text-primary: #e1e4ed;
|
|
--text-secondary: #9ca3b8;
|
|
--text-muted: #6b7280;
|
|
--border: #2d3148;
|
|
--border-light: #3a3f58;
|
|
--accent: #4a7ab5;
|
|
--accent-glow: rgba(74, 122, 181, 0.4);
|
|
--accent-bg: rgba(74, 122, 181, 0.12);
|
|
--accent-ai: #8b5cf6;
|
|
--accent-ai-glow: rgba(139, 92, 246, 0.4);
|
|
--accent-ai-bg: rgba(139, 92, 246, 0.12);
|
|
--success: #34d399;
|
|
--warning: #fbbf24;
|
|
--radius-sm: 6px;
|
|
--radius: 8px;
|
|
--shadow-popover: 0 8px 32px rgba(0,0,0,0.4);
|
|
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
--font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: var(--font-sans);
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
height: 100vh;
|
|
display: flex; flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ---- Zone 1: Top Bar ---- */
|
|
.poc-topbar {
|
|
height: 48px; min-height: 48px;
|
|
display: flex; align-items: center; gap: 12px;
|
|
padding: 0 16px;
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border);
|
|
z-index: 100;
|
|
}
|
|
.poc-topbar-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
|
|
.poc-hamburger {
|
|
width: 32px; height: 32px; border-radius: var(--radius-sm);
|
|
border: none; background: transparent; color: var(--text-secondary);
|
|
cursor: pointer; font-size: 18px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.poc-hamburger:hover { background: var(--bg-hover); color: var(--text-primary); }
|
|
.poc-brand { font-weight: 700; font-size: 13px; color: var(--accent); letter-spacing: 0.5px; }
|
|
.poc-title-input {
|
|
flex: 1; min-width: 100px;
|
|
background: transparent; border: 1px solid transparent;
|
|
color: var(--text-primary); font-size: 14px;
|
|
padding: 4px 8px; border-radius: var(--radius-sm);
|
|
outline: none; font-family: var(--font-sans);
|
|
}
|
|
.poc-title-input:hover { border-color: var(--border); }
|
|
.poc-title-input:focus { border-color: var(--accent); background: var(--bg-tertiary); }
|
|
.poc-topbar-right { display: flex; align-items: center; gap: 6px; }
|
|
.poc-btn {
|
|
height: 32px; padding: 0 12px; border-radius: var(--radius-sm);
|
|
border: 1px solid transparent; background: transparent;
|
|
color: var(--text-secondary); cursor: pointer; font-size: 13px;
|
|
display: flex; align-items: center; gap: 6px;
|
|
white-space: nowrap; font-family: var(--font-sans);
|
|
transition: all 120ms ease;
|
|
}
|
|
.poc-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
|
|
.poc-btn-ai {
|
|
background: var(--accent-ai-bg); color: var(--accent-ai);
|
|
border-color: rgba(139,92,246,0.25); font-weight: 600;
|
|
box-shadow: 0 0 12px var(--accent-ai-glow);
|
|
}
|
|
.poc-btn-ai:hover { background: rgba(139,92,246,0.2); box-shadow: 0 0 20px var(--accent-ai-glow); }
|
|
.poc-btn-ai.active { background: var(--accent-ai); color: #fff; }
|
|
.poc-save-indicator { display: flex; align-items: center; gap: 5px; font-size: 12px; color: var(--success); }
|
|
.poc-save-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--success); }
|
|
|
|
/* ---- Zone 4: Workspace + AI Panel ---- */
|
|
.poc-workspace { display: flex; flex: 1; overflow: hidden; }
|
|
.poc-editor-area {
|
|
flex: 1; overflow-y: auto; overflow-x: hidden;
|
|
position: relative;
|
|
}
|
|
.poc-ai-panel {
|
|
width: 340px; min-width: 340px;
|
|
background: var(--bg-secondary); border-left: 1px solid var(--border);
|
|
display: flex; flex-direction: column;
|
|
transition: transform 200ms ease, min-width 200ms ease, width 200ms ease;
|
|
overflow: hidden;
|
|
}
|
|
.poc-ai-panel.hidden { transform: translateX(100%); min-width: 0; width: 0; border-left: none; }
|
|
.poc-ai-panel-header { padding: 14px 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
|
|
.poc-ai-panel-title { font-weight: 700; font-size: 14px; color: var(--accent-ai); }
|
|
.poc-ai-panel-close { width: 28px; height: 28px; border-radius: var(--radius-sm); border: none; background: transparent; color: var(--text-muted); cursor: pointer; font-size: 16px; }
|
|
.poc-ai-panel-close:hover { background: var(--bg-hover); color: var(--text-primary); }
|
|
.poc-ai-chat { flex: 1; padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
|
|
.poc-ai-suggestions { padding: 12px 16px; border-top: 1px solid var(--border); }
|
|
.poc-ai-suggestion {
|
|
display: block; width: 100%; text-align: left;
|
|
padding: 8px 12px; margin-bottom: 4px; border-radius: var(--radius-sm);
|
|
border: none; background: transparent; color: var(--text-secondary);
|
|
cursor: pointer; font-size: 13px; font-family: var(--font-sans);
|
|
}
|
|
.poc-ai-suggestion:hover { background: var(--bg-hover); color: var(--text-primary); }
|
|
.poc-ai-input-row { padding: 10px 16px; border-top: 1px solid var(--border); display: flex; gap: 8px; }
|
|
.poc-ai-input {
|
|
flex: 1; padding: 8px 12px; border-radius: var(--radius-sm);
|
|
border: 1px solid var(--border); background: var(--bg-tertiary);
|
|
color: var(--text-primary); font-size: 13px; outline: none; font-family: var(--font-sans);
|
|
}
|
|
.poc-ai-input:focus { border-color: var(--accent-ai); }
|
|
.poc-ai-send { padding: 8px 14px; border-radius: var(--radius-sm); border: none; background: var(--accent-ai); color: #fff; cursor: pointer; font-size: 13px; font-weight: 600; }
|
|
|
|
/* ---- Textarea ---- */
|
|
.poc-editor-wrapper {
|
|
max-width: 800px; margin: 40px auto; padding: 0 40px 200px 60px;
|
|
position: relative;
|
|
}
|
|
.poc-textarea {
|
|
width: 100%; min-height: 600px; resize: vertical;
|
|
background: transparent; border: none; outline: none;
|
|
color: var(--text-primary);
|
|
font-family: var(--font-mono);
|
|
font-size: 14px; line-height: 1.8;
|
|
padding: 0;
|
|
tab-size: 2;
|
|
}
|
|
.poc-textarea::placeholder { color: var(--text-muted); }
|
|
|
|
/* ---- Zone 3: Quick Insert ---- */
|
|
.poc-quick-insert {
|
|
position: absolute; left: 6px;
|
|
width: 26px; height: 26px; border-radius: 50%;
|
|
border: 1px solid var(--border); background: var(--bg-secondary);
|
|
color: var(--text-muted); cursor: pointer;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 16px; font-weight: 700;
|
|
transition: all 100ms ease;
|
|
z-index: 10; top: 12px;
|
|
}
|
|
.poc-quick-insert:hover { color: var(--text-primary); border-color: var(--accent); background: var(--accent-bg); }
|
|
|
|
/* ---- Zone 2a: Slash Menu ---- */
|
|
.poc-slash-menu {
|
|
position: absolute; z-index: 200;
|
|
width: 320px; max-height: 380px; overflow-y: auto;
|
|
background: var(--bg-elevated); border: 1px solid var(--border-light);
|
|
border-radius: var(--radius); box-shadow: var(--shadow-popover);
|
|
display: none;
|
|
}
|
|
.poc-slash-menu.visible { display: block; animation: pocFadeSlideIn 120ms ease; }
|
|
@keyframes pocFadeSlideIn { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }
|
|
.poc-slash-search { padding: 10px 12px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg-elevated); border-radius: var(--radius) var(--radius) 0 0; }
|
|
.poc-slash-search input {
|
|
width: 100%; padding: 6px 10px; border-radius: var(--radius-sm);
|
|
border: 1px solid var(--border); background: var(--bg-tertiary);
|
|
color: var(--text-primary); font-size: 13px; outline: none; font-family: var(--font-sans);
|
|
}
|
|
.poc-slash-section-title { padding: 8px 14px 4px; font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
.poc-slash-item { display: flex; align-items: center; gap: 10px; padding: 8px 14px; cursor: pointer; font-size: 13px; color: var(--text-secondary); transition: background 80ms ease; }
|
|
.poc-slash-item:hover, .poc-slash-item.active { background: var(--bg-hover); color: var(--text-primary); }
|
|
.poc-slash-item-icon { width: 28px; height: 28px; border-radius: var(--radius-sm); background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; font-size: 15px; flex-shrink: 0; }
|
|
.poc-slash-item-body { flex: 1; min-width: 0; }
|
|
.poc-slash-item-title { font-weight: 600; font-size: 13px; color: var(--text-primary); }
|
|
.poc-slash-item-desc { font-size: 11px; color: var(--text-muted); margin-top: 1px; }
|
|
.poc-slash-divider { height: 1px; margin: 4px 14px; background: var(--border); }
|
|
|
|
/* ---- Zone 2b: Bubble Menu ---- */
|
|
.poc-bubble-menu {
|
|
position: fixed; z-index: 150;
|
|
display: none; align-items: center; gap: 2px;
|
|
padding: 4px 6px; background: var(--bg-elevated);
|
|
border: 1px solid var(--border-light); border-radius: var(--radius);
|
|
box-shadow: var(--shadow-popover);
|
|
}
|
|
.poc-bubble-menu.visible { display: flex; animation: pocFadeScaleIn 120ms ease; }
|
|
@keyframes pocFadeScaleIn { from { opacity: 0; transform: translateY(4px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
|
|
.poc-bubble-btn {
|
|
width: 30px; height: 30px; border-radius: var(--radius-sm);
|
|
border: none; background: transparent; color: var(--text-secondary);
|
|
cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center;
|
|
transition: all 100ms ease;
|
|
}
|
|
.poc-bubble-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
|
|
.poc-bubble-sep { width: 1px; height: 20px; background: var(--border); margin: 0 4px; }
|
|
.poc-bubble-btn-ai { color: var(--accent-ai); }
|
|
.poc-bubble-btn-ai:hover { background: var(--accent-ai-bg); }
|
|
.poc-bubble-more-menu {
|
|
position: absolute; top: 100%; right: 0; margin-top: 6px;
|
|
width: 200px; background: var(--bg-elevated); border: 1px solid var(--border-light);
|
|
border-radius: var(--radius); box-shadow: var(--shadow-popover);
|
|
display: none; z-index: 160; padding: 4px;
|
|
}
|
|
.poc-bubble-more-menu.visible { display: block; }
|
|
.poc-bubble-more-item { display: flex; align-items: center; gap: 8px; padding: 7px 10px; font-size: 13px; color: var(--text-secondary); cursor: pointer; border-radius: var(--radius-sm); }
|
|
.poc-bubble-more-item:hover { background: var(--bg-hover); color: var(--text-primary); }
|
|
|
|
/* ---- Zone 5: Drag & Drop ---- */
|
|
.poc-drop-overlay {
|
|
position: fixed; inset: 0; z-index: 500;
|
|
display: none; align-items: center; justify-content: center;
|
|
background: rgba(15,17,23,0.85); border: 3px dashed var(--accent);
|
|
pointer-events: none;
|
|
}
|
|
.poc-drop-overlay.visible { display: flex; }
|
|
.poc-drop-message { text-align: center; color: var(--accent); }
|
|
.poc-drop-icon { font-size: 48px; margin-bottom: 12px; }
|
|
.poc-drop-title { font-size: 20px; font-weight: 700; margin-bottom: 6px; }
|
|
.poc-drop-subtitle { font-size: 14px; color: var(--text-secondary); }
|
|
|
|
/* ---- Status Bar ---- */
|
|
.poc-statusbar {
|
|
height: 28px; min-height: 28px;
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 0 16px; background: var(--bg-secondary);
|
|
border-top: 1px solid var(--border);
|
|
font-size: 11px; color: var(--text-muted);
|
|
}
|
|
.poc-statusbar-left, .poc-statusbar-right { display: flex; align-items: center; gap: 16px; }
|
|
.poc-status-ai-active { color: var(--accent-ai); font-weight: 600; display: none; align-items: center; gap: 4px; }
|
|
.poc-status-ai-active.visible { display: flex; }
|
|
|
|
/* ---- Toast ---- */
|
|
.poc-toast {
|
|
position: fixed; bottom: 48px; left: 50%; transform: translateX(-50%);
|
|
background: var(--bg-elevated); border: 1px solid var(--border-light);
|
|
border-radius: var(--radius); padding: 10px 20px;
|
|
font-size: 13px; color: var(--text-primary);
|
|
box-shadow: var(--shadow-popover); z-index: 600; display: none;
|
|
animation: pocFadeSlideIn 150ms ease;
|
|
}
|
|
.poc-toast.visible { display: block; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Zone 1: Top Bar -->
|
|
<div class="poc-topbar">
|
|
<div class="poc-topbar-left">
|
|
<button class="poc-hamburger" title="Menu">☰</button>
|
|
<span class="poc-brand">ObsiGate</span>
|
|
<input class="poc-title-input" id="poc-title" value="Rapport d'analyse trimestriel" placeholder="Titre du document..." spellcheck="false">
|
|
</div>
|
|
<div class="poc-topbar-right">
|
|
<button class="poc-btn poc-btn-ai" id="poc-btn-ai" title="AI Assistant (Ctrl+J)">🔮 AI</button>
|
|
<button class="poc-btn" id="poc-btn-preview" title="Toggle preview">👁 Preview</button>
|
|
<button class="poc-btn" title="Actions">⚙</button>
|
|
<div class="poc-save-indicator" id="poc-save-indicator"><span class="poc-save-dot"></span> Saved</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Workspace -->
|
|
<div class="poc-workspace">
|
|
|
|
<!-- Editor Area -->
|
|
<div class="poc-editor-area" id="poc-editor-area">
|
|
|
|
<!-- Zone 5: Drag & Drop -->
|
|
<div class="poc-drop-overlay" id="poc-drop-overlay">
|
|
<div class="poc-drop-message">
|
|
<div class="poc-drop-icon">🖼</div>
|
|
<div class="poc-drop-title">Drop files here</div>
|
|
<div class="poc-drop-subtitle">Images, PDFs, documents</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Editor: Textarea + Quick Insert -->
|
|
<div class="poc-editor-wrapper" id="poc-editor-wrapper">
|
|
<button class="poc-quick-insert" id="poc-quick-insert-btn" title="Quick insert">⊕</button>
|
|
<textarea class="poc-textarea" id="poc-textarea" placeholder="Type / for commands..." spellcheck="false"># Introduction
|
|
|
|
Ce document demontre l'architecture **multi-zone toolbar** proposee pour l'editeur nouvelle generation d'ObsiGate. Le design s'inspire de Notion, Medium, Dropbox Paper et Cursor.
|
|
|
|
## Principes cles
|
|
|
|
Les editeurs modernes n'utilisent plus une seule barre d'outils fixe. Les controles sont distribues a travers **5 zones contextuelles** qui apparaissent uniquement quand elles sont pertinentes.
|
|
|
|
Le principe fondamental : *"Montrer le bon controle, au bon endroit, au bon moment."*
|
|
|
|
## Prochaines etapes
|
|
|
|
- Finaliser le design des 5 zones
|
|
- Brancher l'API AI du backend
|
|
- Tests utilisateurs
|
|
|
|
</textarea>
|
|
|
|
<!-- Zone 3: Quick Insert Menu -->
|
|
<div class="poc-bubble-more-menu" id="poc-quick-insert-menu" style="position:absolute;left:40px;top:40px;">
|
|
<div class="poc-bubble-more-item" data-qi="image">🎨 Image</div>
|
|
<div class="poc-bubble-more-item" data-qi="pdf">📄 PDF</div>
|
|
<div class="poc-bubble-more-item" data-qi="code"></> Code Block</div>
|
|
<div class="poc-bubble-more-item" data-qi="table">📊 Table</div>
|
|
<div class="poc-bubble-more-item" data-qi="ai">✨ AI Actions</div>
|
|
</div>
|
|
|
|
<!-- Zone 2a: Slash Menu -->
|
|
<div class="poc-slash-menu" id="poc-slash-menu">
|
|
<div class="poc-slash-search"><input type="text" id="poc-slash-search-input" placeholder="Filter commands..." autocomplete="off"></div>
|
|
<div class="poc-slash-section-title">Formatting</div>
|
|
<div class="poc-slash-item" data-action="text"><span class="poc-slash-item-icon">📝</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Text</div><div class="poc-slash-item-desc">Just start writing plain text</div></div></div>
|
|
<div class="poc-slash-item" data-action="h1"><span class="poc-slash-item-icon">H1</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Heading 1</div><div class="poc-slash-item-desc">Big section heading</div></div></div>
|
|
<div class="poc-slash-item" data-action="h2"><span class="poc-slash-item-icon">H2</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Heading 2</div><div class="poc-slash-item-desc">Medium section heading</div></div></div>
|
|
<div class="poc-slash-item" data-action="h3"><span class="poc-slash-item-icon">H3</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Heading 3</div><div class="poc-slash-item-desc">Small section heading</div></div></div>
|
|
<div class="poc-slash-item" data-action="bullet"><span class="poc-slash-item-icon">•</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Bullet List</div><div class="poc-slash-item-desc">Simple unordered list</div></div></div>
|
|
<div class="poc-slash-item" data-action="numbered"><span class="poc-slash-item-icon">1.</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Numbered List</div><div class="poc-slash-item-desc">Ordered list</div></div></div>
|
|
<div class="poc-slash-item" data-action="quote"><span class="poc-slash-item-icon">❝</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Quote</div><div class="poc-slash-item-desc">Block quote section</div></div></div>
|
|
<div class="poc-slash-item" data-action="code"><span class="poc-slash-item-icon"></></span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Code Block</div><div class="poc-slash-item-desc">Syntax-highlighted code</div></div></div>
|
|
<div class="poc-slash-item" data-action="divider"><span class="poc-slash-item-icon">─</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Divider</div><div class="poc-slash-item-desc">Horizontal rule</div></div></div>
|
|
<div class="poc-slash-item" data-action="checklist"><span class="poc-slash-item-icon">☐</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Checklist</div><div class="poc-slash-item-desc">Task list with checkboxes</div></div></div>
|
|
<div class="poc-slash-divider"></div>
|
|
<div class="poc-slash-section-title">Insert</div>
|
|
<div class="poc-slash-item" data-action="image"><span class="poc-slash-item-icon">🎨</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Insert Image</div><div class="poc-slash-item-desc">Upload or embed an image</div></div></div>
|
|
<div class="poc-slash-item" data-action="pdf"><span class="poc-slash-item-icon">📄</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Insert PDF</div><div class="poc-slash-item-desc">Attach a PDF document</div></div></div>
|
|
<div class="poc-slash-item" data-action="table"><span class="poc-slash-item-icon">📊</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Insert Table</div><div class="poc-slash-item-desc">Data table</div></div></div>
|
|
<div class="poc-slash-item" data-action="link"><span class="poc-slash-item-icon">🔗</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">Insert Link</div><div class="poc-slash-item-desc">URL link</div></div></div>
|
|
<div class="poc-slash-divider"></div>
|
|
<div class="poc-slash-section-title">AI Actions</div>
|
|
<div class="poc-slash-item" data-action="ai-continue"><span class="poc-slash-item-icon" style="color:var(--accent-ai)">✨</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">AI: Continue writing</div><div class="poc-slash-item-desc">Let AI continue from here</div></div></div>
|
|
<div class="poc-slash-item" data-action="ai-improve"><span class="poc-slash-item-icon" style="color:var(--accent-ai)">✨</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">AI: Improve writing</div><div class="poc-slash-item-desc">Enhance style and clarity</div></div></div>
|
|
<div class="poc-slash-item" data-action="ai-summarize"><span class="poc-slash-item-icon" style="color:var(--accent-ai)">✨</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">AI: Summarize</div><div class="poc-slash-item-desc">Condense this document</div></div></div>
|
|
<div class="poc-slash-item" data-action="ai-translate"><span class="poc-slash-item-icon" style="color:var(--accent-ai)">✨</span><div class="poc-slash-item-body"><div class="poc-slash-item-title">AI: Translate</div><div class="poc-slash-item-desc">Translate to another language</div></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Zone 2b: Bubble Menu -->
|
|
<div class="poc-bubble-menu" id="poc-bubble-menu">
|
|
<button class="poc-bubble-btn" data-action="bold" title="Bold (Ctrl+B)"><b>B</b></button>
|
|
<button class="poc-bubble-btn" data-action="italic" title="Italic (Ctrl+I)"><i>I</i></button>
|
|
<button class="poc-bubble-btn" data-action="strike" title="Strikethrough"><s>S</s></button>
|
|
<button class="poc-bubble-btn" data-action="code" title="Inline Code"></></button>
|
|
<button class="poc-bubble-btn" data-action="link" title="Link (Ctrl+K)">🔗</button>
|
|
<button class="poc-bubble-btn" data-action="highlight" title="Highlight">☀</button>
|
|
<span class="poc-bubble-sep"></span>
|
|
<button class="poc-bubble-btn poc-bubble-btn-ai" id="poc-bubble-more" title="More AI">⋯</button>
|
|
<div class="poc-bubble-more-menu" id="poc-bubble-more-menu">
|
|
<div class="poc-bubble-more-item" data-action="ai-improve">✨ Improve writing</div>
|
|
<div class="poc-bubble-more-item" data-action="ai-grammar">✅ Fix grammar</div>
|
|
<div class="poc-bubble-more-item" data-action="ai-translate">🌐 Translate to...</div>
|
|
<div class="poc-bubble-more-item" data-action="ai-continue">➕ Continue writing</div>
|
|
<div class="poc-bubble-more-item" data-action="ai-summarize">📝 Summarize</div>
|
|
<div class="poc-bubble-more-item" data-action="ai-tone">🎶 Change tone</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Zone 4: AI Panel -->
|
|
<div class="poc-ai-panel hidden" id="poc-ai-panel">
|
|
<div class="poc-ai-panel-header">
|
|
<span class="poc-ai-panel-title">✨ AI Assistant</span>
|
|
<button class="poc-ai-panel-close" id="poc-ai-panel-close" title="Close">×</button>
|
|
</div>
|
|
<div class="poc-ai-chat" id="poc-ai-chat">
|
|
<div style="color:var(--text-muted);font-size:13px;padding:12px">The AI assistant knows your document. Ask questions, request summaries, or get writing suggestions.</div>
|
|
</div>
|
|
<div class="poc-ai-suggestions">
|
|
<button class="poc-ai-suggestion">✨ Improve overall style</button>
|
|
<button class="poc-ai-suggestion">➕ Add a conclusion section</button>
|
|
<button class="poc-ai-suggestion">🌐 Translate to English</button>
|
|
<button class="poc-ai-suggestion">📄 Generate PDF summary</button>
|
|
</div>
|
|
<div class="poc-ai-input-row">
|
|
<input class="poc-ai-input" id="poc-ai-input" placeholder="Ask AI something...">
|
|
<button class="poc-ai-send" id="poc-ai-send">Send</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Bar -->
|
|
<div class="poc-statusbar">
|
|
<div class="poc-statusbar-left">
|
|
<span>Type <strong>/</strong> for slash menu</span>
|
|
<span>Ctrl+J for AI panel</span>
|
|
<span class="poc-status-ai-active" id="poc-status-ai">✨ AI processing...</span>
|
|
</div>
|
|
<div class="poc-statusbar-right">
|
|
<span id="poc-word-count">42 words</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast -->
|
|
<div class="poc-toast" id="poc-toast"></div>
|
|
|
|
|
|
<!-- ==========================================================================
|
|
JAVASCRIPT — Real markdown editing with all 5 zones
|
|
========================================================================== -->
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
// ---- DOM refs ----
|
|
var ta = document.getElementById('poc-textarea');
|
|
var editorArea = document.getElementById('poc-editor-area');
|
|
var wrapper = document.getElementById('poc-editor-wrapper');
|
|
var slashMenu = document.getElementById('poc-slash-menu');
|
|
var slashSearch = document.getElementById('poc-slash-search-input');
|
|
var bubbleMenu = document.getElementById('poc-bubble-menu');
|
|
var bubbleMoreMenu = document.getElementById('poc-bubble-more-menu');
|
|
var qiMenu = document.getElementById('poc-quick-insert-menu');
|
|
var aiPanel = document.getElementById('poc-ai-panel');
|
|
var btnAI = document.getElementById('poc-btn-ai');
|
|
var dropOverlay = document.getElementById('poc-drop-overlay');
|
|
var saveIndicator = document.getElementById('poc-save-indicator');
|
|
var wordCount = document.getElementById('poc-word-count');
|
|
var aiStatus = document.getElementById('poc-status-ai');
|
|
var toast = document.getElementById('poc-toast');
|
|
|
|
var slashVisible = false;
|
|
var slashItems = [];
|
|
var slashActiveIdx = -1;
|
|
var bubbleVisible = false;
|
|
var saveTimer = null;
|
|
|
|
// ---- TEXTAREA CURSOR UTILS ----
|
|
function getCaretPos(el) {
|
|
return { start: el.selectionStart, end: el.selectionEnd };
|
|
}
|
|
|
|
function setCaretPos(el, pos) {
|
|
el.setSelectionRange(pos, pos);
|
|
el.focus();
|
|
}
|
|
|
|
function insertAtCursor(el, text, selectOffset) {
|
|
var pos = getCaretPos(el);
|
|
var before = el.value.substring(0, pos.start);
|
|
var after = el.value.substring(pos.end);
|
|
el.value = before + text + after;
|
|
var newPos = selectOffset != null ? pos.start + selectOffset : pos.start + text.length;
|
|
setCaretPos(el, newPos);
|
|
triggerChange();
|
|
}
|
|
|
|
function wrapSelection(el, before, after) {
|
|
var pos = getCaretPos(el);
|
|
if (pos.start === pos.end) {
|
|
// No selection — insert markers and place cursor between them
|
|
insertAtCursor(el, before + after, before.length);
|
|
return;
|
|
}
|
|
var txt = el.value.substring(pos.start, pos.end);
|
|
el.value = el.value.substring(0, pos.start) + before + txt + (after || before) + el.value.substring(pos.end);
|
|
setCaretPos(el, pos.start + before.length + txt.length + (after || before).length);
|
|
triggerChange();
|
|
}
|
|
|
|
function getLineAtCursor(el) {
|
|
var pos = getCaretPos(el);
|
|
var val = el.value;
|
|
var lineStart = val.lastIndexOf('\n', pos.start - 1) + 1;
|
|
var lineEnd = val.indexOf('\n', pos.start);
|
|
if (lineEnd === -1) lineEnd = val.length;
|
|
return { text: val.substring(lineStart, lineEnd), start: lineStart, end: lineEnd };
|
|
}
|
|
|
|
function replaceLineAtCursor(el, newText) {
|
|
var line = getLineAtCursor(el);
|
|
el.value = el.value.substring(0, line.start) + newText + el.value.substring(line.end);
|
|
setCaretPos(el, line.start + newText.length);
|
|
triggerChange();
|
|
}
|
|
|
|
function triggerChange() {
|
|
updateWordCount();
|
|
clearTimeout(saveTimer);
|
|
saveIndicator.innerHTML = '<span style="color:var(--warning);font-size:12px">Saving...</span>';
|
|
saveTimer = setTimeout(function() {
|
|
saveIndicator.innerHTML = '<span class="poc-save-dot"></span> Saved';
|
|
}, 600);
|
|
}
|
|
|
|
// ---- Zone 2a: Slash Command Menu ----
|
|
function initSlashItems() {
|
|
slashItems = Array.from(slashMenu.querySelectorAll('.poc-slash-item'));
|
|
}
|
|
|
|
// Mappings: slash action -> markdown insertion
|
|
var SLASH_INSERT = {
|
|
text: function() { /* just remove the slash */ removeTrailingSlash(); },
|
|
h1: function() { replaceLineAtCursor(ta, '# '); },
|
|
h2: function() { replaceLineAtCursor(ta, '## '); },
|
|
h3: function() { replaceLineAtCursor(ta, '### '); },
|
|
bullet: function() { replaceLineAtCursor(ta, '- '); },
|
|
numbered: function() { replaceLineAtCursor(ta, '1. '); },
|
|
quote: function() { replaceLineAtCursor(ta, '> '); },
|
|
code: function() { insertAtCursor(ta, '```\n\n```', 4); },
|
|
divider: function() { insertAtCursor(ta, '\n---\n'); },
|
|
checklist: function() { replaceLineAtCursor(ta, '- [ ] '); },
|
|
image: function() { insertAtCursor(ta, '', 2); },
|
|
pdf: function() { insertAtCursor(ta, '[PDF: title](path)', 5); },
|
|
table: function() { insertAtCursor(ta, '| Col 1 | Col 2 |\n|-------|-------|\n| | |', 2); },
|
|
link: function() { insertAtCursor(ta, '[text](url)', 1); },
|
|
'ai-continue': function() { simAI('continue writing'); },
|
|
'ai-improve': function() { simAI('improve writing'); },
|
|
'ai-summarize': function() { simAI('summarize'); },
|
|
'ai-translate': function() { simAI('translate'); }
|
|
};
|
|
|
|
function removeTrailingSlash() {
|
|
var line = getLineAtCursor(ta);
|
|
var stripped = line.text.replace(/^\/\s*/, '');
|
|
replaceLineAtCursor(ta, stripped);
|
|
}
|
|
|
|
function showSlashMenu() {
|
|
var pos = getCaretPos(ta);
|
|
var line = getLineAtCursor(ta);
|
|
|
|
// Only trigger if / is at (or near) beginning of line
|
|
var beforeCaret = line.text.substring(0, pos.start - line.start);
|
|
if (!/^\/\S*$/.test(beforeCaret)) return;
|
|
|
|
// Position menu near the caret
|
|
var wrapperRect = wrapper.getBoundingClientRect();
|
|
slashMenu.style.left = '40px';
|
|
slashMenu.style.top = '80px';
|
|
slashMenu.classList.add('visible');
|
|
slashVisible = true;
|
|
slashActiveIdx = -1;
|
|
slashSearch.value = '';
|
|
slashSearch.focus();
|
|
filterSlashItems('');
|
|
}
|
|
|
|
function hideSlashMenu() {
|
|
slashMenu.classList.remove('visible');
|
|
slashVisible = false;
|
|
resetSlashItems();
|
|
}
|
|
|
|
function filterSlashItems(q) {
|
|
var query = q.toLowerCase().trim();
|
|
slashItems.forEach(function(item) {
|
|
var title = (item.querySelector('.poc-slash-item-title') || {}).textContent || '';
|
|
var desc = (item.querySelector('.poc-slash-item-desc') || {}).textContent || '';
|
|
item.style.display = (!query || title.toLowerCase().indexOf(query) >= 0 || desc.toLowerCase().indexOf(query) >= 0) ? '' : 'none';
|
|
item.classList.remove('active');
|
|
});
|
|
updateSlashSectionVisibility();
|
|
slashActiveIdx = -1;
|
|
}
|
|
|
|
function updateSlashSectionVisibility() {
|
|
slashMenu.querySelectorAll('.poc-slash-section-title').forEach(function(title) {
|
|
var next = title.nextElementSibling;
|
|
var allHidden = true;
|
|
while (next && !next.classList.contains('poc-slash-section-title') && !next.classList.contains('poc-slash-divider')) {
|
|
if (next.style.display !== 'none') { allHidden = false; break; }
|
|
next = next.nextElementSibling;
|
|
}
|
|
title.style.display = allHidden ? 'none' : '';
|
|
});
|
|
}
|
|
|
|
function resetSlashItems() {
|
|
slashItems.forEach(function(item) { item.style.display = ''; item.classList.remove('active'); });
|
|
slashMenu.querySelectorAll('.poc-slash-section-title').forEach(function(t) { t.style.display = ''; });
|
|
}
|
|
|
|
function getVisibleSlashItems() {
|
|
return slashItems.filter(function(item) { return item.style.display !== 'none'; });
|
|
}
|
|
|
|
function selectSlashAction(action) {
|
|
hideSlashMenu();
|
|
var fn = SLASH_INSERT[action];
|
|
if (fn) { fn(); }
|
|
else { showToast('Action: ' + action); }
|
|
ta.focus();
|
|
}
|
|
|
|
// ---- Zone 2b: Bubble Menu ----
|
|
function showBubbleMenu() {
|
|
var selStart = ta.selectionStart;
|
|
var selEnd = ta.selectionEnd;
|
|
if (selStart === selEnd) { hideBubbleMenu(); return; }
|
|
|
|
// Approximate position from textarea selection
|
|
var taRect = ta.getBoundingClientRect();
|
|
// We can't get selection coordinates from a textarea directly.
|
|
// Position roughly above the textarea at center horizontally.
|
|
var top = taRect.top - 48;
|
|
var left = taRect.left + (taRect.width / 2) - (bubbleMenu.offsetWidth / 2);
|
|
|
|
if (top < 60) top = taRect.bottom + 4;
|
|
if (left < 8) left = 8;
|
|
if (left + bubbleMenu.offsetWidth > window.innerWidth - 8) left = window.innerWidth - bubbleMenu.offsetWidth - 8;
|
|
|
|
bubbleMenu.style.left = left + 'px';
|
|
bubbleMenu.style.top = top + 'px';
|
|
bubbleMenu.classList.add('visible');
|
|
bubbleVisible = true;
|
|
bubbleMoreMenu.classList.remove('visible');
|
|
}
|
|
|
|
function hideBubbleMenu() {
|
|
bubbleMenu.classList.remove('visible');
|
|
bubbleVisible = false;
|
|
bubbleMoreMenu.classList.remove('visible');
|
|
}
|
|
|
|
function execBubbleAction(action) {
|
|
switch(action) {
|
|
case 'bold': wrapSelection(ta, '**'); break;
|
|
case 'italic': wrapSelection(ta, '*'); break;
|
|
case 'strike': wrapSelection(ta, '~~'); break;
|
|
case 'code': wrapSelection(ta, '`'); break;
|
|
case 'highlight': wrapSelection(ta, '=='); break;
|
|
case 'link':
|
|
var sel = ta.value.substring(ta.selectionStart, ta.selectionEnd);
|
|
var url = prompt('Enter URL:', 'https://');
|
|
if (url) wrapSelection(ta, '[', '](' + url + ')');
|
|
break;
|
|
case 'ai-improve': simAI('improve writing'); break;
|
|
case 'ai-grammar': simAI('fix grammar'); break;
|
|
case 'ai-translate': simAI('translate'); break;
|
|
case 'ai-continue': simAI('continue writing'); break;
|
|
case 'ai-summarize': simAI('summarize selection'); break;
|
|
case 'ai-tone': simAI('change tone'); break;
|
|
}
|
|
hideBubbleMenu();
|
|
ta.focus();
|
|
}
|
|
|
|
// ---- Zone 3: Quick Insert ----
|
|
function showQuickInsertMenu() {
|
|
var rect = document.getElementById('poc-quick-insert-btn').getBoundingClientRect();
|
|
qiMenu.style.left = (rect.right + 4) + 'px';
|
|
qiMenu.style.top = rect.top + 'px';
|
|
qiMenu.style.display = qiMenu.style.display === 'block' ? 'none' : 'block';
|
|
}
|
|
|
|
// ---- Zone 4: AI Panel ----
|
|
function toggleAIPanel() {
|
|
var hidden = aiPanel.classList.toggle('hidden');
|
|
btnAI.classList.toggle('active', !hidden);
|
|
if (!hidden) document.getElementById('poc-ai-input').focus();
|
|
}
|
|
|
|
function sendAIMessage() {
|
|
var input = document.getElementById('poc-ai-input');
|
|
var chat = document.getElementById('poc-ai-chat');
|
|
var text = input.value.trim();
|
|
if (!text) return;
|
|
|
|
var userMsg = document.createElement('div');
|
|
userMsg.style.cssText = 'background:var(--bg-tertiary);padding:8px 12px;border-radius:var(--radius-sm);font-size:13px;align-self:flex-end;max-width:80%;';
|
|
userMsg.textContent = text;
|
|
chat.appendChild(userMsg);
|
|
input.value = '';
|
|
showAIProcessing();
|
|
|
|
setTimeout(function() {
|
|
var aiMsg = document.createElement('div');
|
|
aiMsg.style.cssText = 'background:var(--accent-ai-bg);padding:8px 12px;border-radius:var(--radius-sm);font-size:13px;color:var(--accent-ai);max-width:80%;';
|
|
aiMsg.textContent = 'AI: "' + text + '" (simulated — connect to backend)';
|
|
chat.appendChild(aiMsg);
|
|
chat.scrollTop = chat.scrollHeight;
|
|
hideAIProcessing();
|
|
}, 800);
|
|
}
|
|
|
|
function showAIProcessing() { aiStatus.classList.add('visible'); }
|
|
function hideAIProcessing() { aiStatus.classList.remove('visible'); }
|
|
function simAI(action) {
|
|
showAIProcessing();
|
|
showToast('AI: ' + action + ' (simulated)');
|
|
setTimeout(hideAIProcessing, 1200);
|
|
}
|
|
|
|
// ---- Zone 5: Drag & Drop ----
|
|
var dragCounter = 0;
|
|
document.addEventListener('dragenter', function(e) { e.preventDefault(); dragCounter++; if (dragCounter === 1) dropOverlay.classList.add('visible'); });
|
|
document.addEventListener('dragleave', function(e) { e.preventDefault(); dragCounter--; if (dragCounter === 0) dropOverlay.classList.remove('visible'); });
|
|
document.addEventListener('dragover', function(e) { e.preventDefault(); });
|
|
document.addEventListener('drop', function(e) {
|
|
e.preventDefault(); dragCounter = 0; dropOverlay.classList.remove('visible');
|
|
var files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
var names = Array.from(files).map(function(f) { return f.name; }).join(', ');
|
|
showToast('File(s) dropped: ' + names + ' (upload simulated)');
|
|
}
|
|
});
|
|
|
|
// ---- Event handlers ----
|
|
ta.addEventListener('keydown', function(e) {
|
|
// Slash menu trigger: type / at beginning of line
|
|
if (e.key === '/') {
|
|
var line = getLineAtCursor(ta);
|
|
var pos = getCaretPos(ta);
|
|
var beforeCaret = line.text.substring(0, pos.start - line.start);
|
|
if (beforeCaret === '' || beforeCaret.match(/^\s*$/)) {
|
|
// Will open slash menu after the character is inserted
|
|
setTimeout(showSlashMenu, 30);
|
|
}
|
|
}
|
|
|
|
// Slash menu keyboard nav
|
|
if (slashVisible) {
|
|
if (e.key === 'Escape') { e.preventDefault(); hideSlashMenu(); }
|
|
else if (e.key === 'ArrowDown') { e.preventDefault(); navigateSlash(1); }
|
|
else if (e.key === 'ArrowUp') { e.preventDefault(); navigateSlash(-1); }
|
|
else if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
var visible = getVisibleSlashItems();
|
|
if (slashActiveIdx >= 0 && slashActiveIdx < visible.length) {
|
|
selectSlashAction(visible[slashActiveIdx].getAttribute('data-action'));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ctrl+J toggle AI panel
|
|
if (e.ctrlKey && e.key === 'j') { e.preventDefault(); toggleAIPanel(); }
|
|
|
|
// Ctrl+B/I/K with wrapping for bubble menu equivalent
|
|
if (e.ctrlKey && e.key === 'b') { e.preventDefault(); wrapSelection(ta, '**'); }
|
|
if (e.ctrlKey && e.key === 'i') { e.preventDefault(); wrapSelection(ta, '*'); }
|
|
});
|
|
|
|
ta.addEventListener('keyup', function(e) {
|
|
// Check if we should show slash menu (user typed / and stopped)
|
|
if (e.key === '/') {
|
|
setTimeout(function() {
|
|
var line = getLineAtCursor(ta);
|
|
var pos = getCaretPos(ta);
|
|
var beforeCaret = line.text.substring(0, pos.start - line.start);
|
|
if (/^\/\w*$/.test(beforeCaret)) {
|
|
showSlashMenu();
|
|
}
|
|
}, 50);
|
|
}
|
|
// Close slash menu if / is no longer at start of line
|
|
if (slashVisible) {
|
|
var line = getLineAtCursor(ta);
|
|
var pos = getCaretPos(ta);
|
|
var beforeCaret = line.text.substring(0, pos.start - line.start);
|
|
if (!/^\/\S*$/.test(beforeCaret)) hideSlashMenu();
|
|
}
|
|
});
|
|
|
|
ta.addEventListener('mouseup', function() {
|
|
setTimeout(showBubbleMenu, 60);
|
|
});
|
|
|
|
ta.addEventListener('input', triggerChange);
|
|
|
|
slashSearch.addEventListener('input', function() {
|
|
filterSlashItems(slashSearch.value);
|
|
});
|
|
|
|
slashMenu.addEventListener('click', function(e) {
|
|
var item = e.target.closest('.poc-slash-item');
|
|
if (item) selectSlashAction(item.getAttribute('data-action'));
|
|
});
|
|
|
|
function navigateSlash(dir) {
|
|
var visible = getVisibleSlashItems();
|
|
if (visible.length === 0) return;
|
|
slashActiveIdx += dir;
|
|
if (slashActiveIdx < 0) slashActiveIdx = visible.length - 1;
|
|
if (slashActiveIdx >= visible.length) slashActiveIdx = 0;
|
|
visible.forEach(function(item, i) { item.classList.toggle('active', i === slashActiveIdx); });
|
|
}
|
|
|
|
// Bubble menu click handlers
|
|
bubbleMenu.addEventListener('click', function(e) {
|
|
var btn = e.target.closest('.poc-bubble-btn');
|
|
if (!btn) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (btn.id === 'poc-bubble-more') { bubbleMoreMenu.classList.toggle('visible'); return; }
|
|
execBubbleAction(btn.getAttribute('data-action'));
|
|
});
|
|
|
|
bubbleMoreMenu.addEventListener('click', function(e) {
|
|
var item = e.target.closest('.poc-bubble-more-item');
|
|
if (!item) return;
|
|
e.stopPropagation();
|
|
execBubbleAction(item.getAttribute('data-action'));
|
|
});
|
|
|
|
// Quick insert menu
|
|
document.getElementById('poc-quick-insert-btn').addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
showQuickInsertMenu();
|
|
});
|
|
qiMenu.addEventListener('click', function(e) {
|
|
var item = e.target.closest('.poc-bubble-more-item');
|
|
if (!item) return;
|
|
var action = item.getAttribute('data-qi');
|
|
if (action === 'ai') { simAI('actions'); qiMenu.style.display = 'none'; return; }
|
|
selectSlashAction(action);
|
|
qiMenu.style.display = 'none';
|
|
});
|
|
|
|
// Click outside closes menus
|
|
document.addEventListener('click', function(e) {
|
|
if (slashVisible && !slashMenu.contains(e.target) && e.target !== ta) hideSlashMenu();
|
|
if (bubbleVisible && !bubbleMenu.contains(e.target)) hideBubbleMenu();
|
|
if (qiMenu.style.display === 'block' && !qiMenu.contains(e.target) && e.target.id !== 'poc-quick-insert-btn') qiMenu.style.display = 'none';
|
|
});
|
|
|
|
// AI Panel
|
|
btnAI.addEventListener('click', toggleAIPanel);
|
|
document.getElementById('poc-ai-panel-close').addEventListener('click', toggleAIPanel);
|
|
document.getElementById('poc-ai-send').addEventListener('click', sendAIMessage);
|
|
document.getElementById('poc-ai-input').addEventListener('keydown', function(e) { if (e.key === 'Enter') sendAIMessage(); });
|
|
document.querySelectorAll('.poc-ai-suggestion').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
document.getElementById('poc-ai-input').value = btn.textContent.trim();
|
|
sendAIMessage();
|
|
});
|
|
});
|
|
|
|
// Title auto-save
|
|
document.getElementById('poc-title').addEventListener('input', function() {
|
|
clearTimeout(saveTimer);
|
|
saveIndicator.innerHTML = '<span style="color:var(--warning);font-size:12px">Saving...</span>';
|
|
saveTimer = setTimeout(function() {
|
|
saveIndicator.innerHTML = '<span class="poc-save-dot"></span> Saved';
|
|
}, 600);
|
|
});
|
|
|
|
// ---- Word count ----
|
|
function updateWordCount() {
|
|
var text = ta.value || '';
|
|
var words = text.trim() ? text.trim().split(/\s+/).length : 0;
|
|
var chars = text.length;
|
|
wordCount.textContent = words + ' words | ' + chars + ' chars';
|
|
}
|
|
|
|
// ---- Toast ----
|
|
var toastTimer = null;
|
|
function showToast(msg) {
|
|
toast.textContent = msg;
|
|
toast.classList.add('visible');
|
|
clearTimeout(toastTimer);
|
|
toastTimer = setTimeout(function() { toast.classList.remove('visible'); }, 2000);
|
|
}
|
|
|
|
// ---- Init ----
|
|
initSlashItems();
|
|
updateWordCount();
|
|
|
|
console.log('ObsiGate Editor POC v2 — markdown textarea ready');
|
|
console.log(' Type / at start of line for slash commands');
|
|
console.log(' Select text for bubble menu (Bold/Italic/etc.)');
|
|
console.log(' Ctrl+J for AI panel');
|
|
console.log(' Ctrl+B for bold, Ctrl+I for italic');
|
|
console.log(' Quick insert (+) for images, PDFs, code blocks');
|
|
console.log(' Drag files for drop zone');
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html> |