Files
ObsiGate/frontend/editor-poc.html
T
bruno 739b359de2
CI / lint (push) Successful in 15s
CI / security (push) Successful in 9s
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled
feat: Editor POC — multi-zone toolbar mockup (TOOLBAR_DESIGN_ANALYSIS.md)
Add standalone /editor-poc page demonstrating all 5 toolbar zones:
- Zone 1: Top Fixed Bar (hamburger, title, AI glow button, save indicator)
- Zone 2a: Slash Command Menu (/ on empty line, 15 commands, filtering)
- Zone 2b: Bubble Menu (on text selection, formatting + AI dropdown)
- Zone 3: Floating Quick Insert (per-line, Image/PDF/Code/Table/AI)
- Zone 4: AI Side Panel (Ctrl+J toggle, chat, suggestions)
- Zone 5: Drag & Drop overlay with dashed border

Files:
- new: frontend/editor-poc.html (46KB standalone vanilla JS)
- backend/main.py: +/editor-poc route before catch-all
- frontend/index.html: Editor POC button in header menu
- frontend/js/config.js: initEditorPocBtn() function + export
- frontend/js/legacy.js: re-export initEditorPocBtn
- frontend/js/app.js + frontend/app.js: init call in both versions
2026-06-05 20:29:56 -04:00

1225 lines
46 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 (Multi-Zone Toolbar)</title>
<style>
/* ==========================================================================
ObsiGate Editor POC — Dark Theme CSS Variables
========================================================================== */
: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;
--error: #f87171;
--radius-sm: 6px;
--radius: 8px;
--radius-lg: 12px;
--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 Fixed 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;
display: flex; align-items: center; justify-content: center;
font-size: 18px;
}
.poc-hamburger:hover { background: var(--bg-hover); color: var(--text-primary); }
.poc-brand {
font-weight: 700; font-size: 13px;
color: var(--accent); white-space: nowrap;
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);
padding: 0 4px;
}
.poc-save-dot {
width: 7px; height: 7px; border-radius: 50%;
background: var(--success);
}
/* ==========================================================================
ZONE 4 — AI Side Panel
========================================================================== */
.poc-workspace {
display: flex; flex: 1; overflow: hidden;
}
.poc-editor-area {
flex: 1; overflow-y: auto; overflow-x: hidden;
transition: margin-right 200ms ease;
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;
transform: translateX(0); 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);
display: flex; align-items: center; gap: 6px;
}
.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;
}
/* ==========================================================================
Editor Content Area
========================================================================== */
.poc-editor {
max-width: 800px; margin: 40px auto; padding: 0 40px 200px 60px;
font-size: 15px; line-height: 1.75; color: var(--text-primary);
outline: none; min-height: 600px;
position: relative;
}
.poc-editor h1 { font-size: 28px; margin: 24px 0 12px; color: var(--text-primary); }
.poc-editor h2 { font-size: 22px; margin: 20px 0 8px; color: var(--text-primary); }
.poc-editor p { margin: 8px 0; }
.poc-editor .line {
position: relative; padding: 4px 0; border-radius: 4px;
min-height: 28px;
}
.poc-editor .line:hover { background: rgba(255,255,255,0.015); }
.poc-editor .line.empty { color: var(--text-muted); font-style: italic; }
/* ==========================================================================
ZONE 3 — Floating Quick Insert
========================================================================== */
.poc-quick-insert {
position: absolute; left: -34px;
width: 24px; height: 24px;
border-radius: 50%; border: 1px solid transparent;
background: transparent; color: transparent;
cursor: pointer; display: flex; align-items: center; justify-content: center;
font-size: 16px; font-weight: 700;
transition: all 100ms ease;
top: 50%; transform: translateY(-50%);
z-index: 10;
}
.poc-editor .line:hover .poc-quick-insert,
.poc-editor .line.empty .poc-quick-insert {
color: var(--text-muted); border-color: var(--border);
background: var(--bg-secondary);
}
.poc-editor .line:hover .poc-quick-insert:hover {
color: var(--text-primary); border-color: var(--accent);
background: var(--accent-bg);
}
/* ==========================================================================
ZONE 2a — Slash Command 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-search input:focus { border-color: var(--accent); }
.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: absolute; 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-btn.active { background: var(--accent-bg); color: var(--accent); }
.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 (Footer)
========================================================================== */
.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 Fixed Bar
====================================================================== -->
<div class="poc-topbar" id="poc-topbar">
<div class="poc-topbar-left">
<button class="poc-hamburger" title="Menu">&#9776;</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)">
&#128302; AI
</button>
<button class="poc-btn" id="poc-btn-insert" title="Insertion rapide">
&#128206; Insert
</button>
<button class="poc-btn" title="Actions">&#9881;</button>
<div class="poc-save-indicator" id="poc-save-indicator">
<span class="poc-save-dot"></span> Saved
</div>
</div>
</div>
<!-- ======================================================================
ZONE 4 + Editor Workspace
====================================================================== -->
<div class="poc-workspace">
<div class="poc-editor-area" id="poc-editor-area">
<!-- ==================================================================
ZONE 5 — Drag & Drop Overlay
================================================================== -->
<div class="poc-drop-overlay" id="poc-drop-overlay">
<div class="poc-drop-message">
<div class="poc-drop-icon">&#128444;</div>
<div class="poc-drop-title">Drop files here</div>
<div class="poc-drop-subtitle">Images, PDFs, documents</div>
</div>
</div>
<!-- ==================================================================
Editor Content
================================================================== -->
<div class="poc-editor" id="poc-editor" contenteditable="true" spellcheck="true">
<h1>Introduction</h1>
<div class="line"><p>This document demonstrates the <strong>multi-zone toolbar architecture</strong> proposed for ObsiGate's next-generation editor. The design draws inspiration from Notion, Medium, Dropbox Paper, and Cursor.</p></div>
<div class="line"><p></p></div>
<h2>Key Principles</h2>
<div class="line"><p>Modern editors no longer use a single fixed toolbar. Controls are distributed across <strong>5 contextual zones</strong> that appear only when relevant.</p></div>
<div class="line"><p>The core principle: <em>"Show the right control, in the right place, at the right time."</em></p></div>
<div class="line"><p></p></div>
<div class="line"><p></p></div>
</div>
<!-- ==================================================================
ZONE 2a — Slash Command 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">&#128221;</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="bullet">
<span class="poc-slash-item-icon">&#8226;</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">&#10077;</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">&lt;/&gt;</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-divider"></div>
<div class="poc-slash-section-title">Insert</div>
<div class="poc-slash-item" data-action="image">
<span class="poc-slash-item-icon">&#127912;</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">&#128196;</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">&#128202;</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-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)">&#10024;</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)">&#10024;</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)">&#10024;</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)">&#10024;</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>
<!-- ==================================================================
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">&lt;/&gt;</button>
<button class="poc-bubble-btn" data-action="link" title="Link (Ctrl+K)">&#128279;</button>
<span class="poc-bubble-sep"></span>
<button class="poc-bubble-btn poc-bubble-btn-ai" id="poc-bubble-more" title="More">&#8943;</button>
<button class="poc-bubble-btn poc-bubble-btn-ai" id="poc-bubble-ai" title="AI Actions">&#10024;</button>
<div class="poc-bubble-more-menu" id="poc-bubble-more-menu">
<div class="poc-bubble-more-item" data-action="ai-improve">&#10024; Improve writing</div>
<div class="poc-bubble-more-item" data-action="ai-grammar">&#9989; Fix grammar</div>
<div class="poc-bubble-more-item" data-action="ai-translate">&#127760; Translate to...</div>
<div class="poc-bubble-more-item" data-action="ai-continue">&#10133; Continue writing</div>
<div class="poc-bubble-more-item" data-action="ai-summarize">&#128221; Summarize</div>
<div class="poc-bubble-more-item" data-action="ai-tone">&#127926; Change tone</div>
</div>
</div>
<!-- ==================================================================
ZONE 3 — Quick Insert buttons (injected by JS)
================================================================== -->
</div><!-- /poc-editor-area -->
<!-- ======================================================================
ZONE 4 — AI Side Panel
====================================================================== -->
<div class="poc-ai-panel hidden" id="poc-ai-panel">
<div class="poc-ai-panel-header">
<span class="poc-ai-panel-title">&#10024; AI Assistant</span>
<button class="poc-ai-panel-close" id="poc-ai-panel-close" title="Close panel">&times;</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 the content of your document. Ask questions, request summaries, or get writing suggestions.
</div>
</div>
<div class="poc-ai-suggestions">
<button class="poc-ai-suggestion">&#10024; Improve overall style</button>
<button class="poc-ai-suggestion">&#10133; Add a conclusion section</button>
<button class="poc-ai-suggestion">&#127760; Translate to English</button>
<button class="poc-ai-suggestion">&#128196; 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">&#10024; AI processing...</span>
</div>
<div class="poc-statusbar-right">
<span id="poc-word-count">42 words</span>
</div>
</div>
<!-- ======================================================================
Toast notifications
====================================================================== -->
<div class="poc-toast" id="poc-toast"></div>
<!-- ======================================================================
JavaScript — All 5 Zones Implementation
====================================================================== -->
<script>
(function() {
'use strict';
// DOM references
var editor = document.getElementById('poc-editor');
var editorArea = document.getElementById('poc-editor-area');
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 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;
// =========================================================================
// Zone 2a — Slash Command Menu
// =========================================================================
function initSlashItems() {
var items = slashMenu.querySelectorAll('.poc-slash-item');
slashItems = Array.from(items);
}
function showSlashMenu(x, y) {
slashMenu.style.left = x + 'px';
slashMenu.style.top = y + 'px';
slashMenu.classList.add('visible');
slashVisible = true;
slashActiveIdx = -1;
slashSearch.value = '';
slashSearch.focus();
filterSlashItems('');
}
function hideSlashMenu() {
slashMenu.classList.remove('visible');
slashVisible = false;
resetSlashItems();
}
function filterSlashItems(query) {
var q = query.toLowerCase().trim();
slashItems.forEach(function(item, i) {
var title = (item.querySelector('.poc-slash-item-title') || {}).textContent || '';
var desc = (item.querySelector('.poc-slash-item-desc') || {}).textContent || '';
var match = !q || title.toLowerCase().indexOf(q) >= 0 || desc.toLowerCase().indexOf(q) >= 0;
item.style.display = match ? '' : 'none';
item.classList.remove('active');
});
// Hide section titles if all items in that section are hidden
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' : '';
});
slashActiveIdx = -1;
}
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 msg = '';
switch(action) {
case 'text': msg = 'Plain text block'; break;
case 'h1': msg = 'Heading 1 inserted'; break;
case 'h2': msg = 'Heading 2 inserted'; break;
case 'bullet': msg = 'Bullet list inserted'; break;
case 'numbered': msg = 'Numbered list inserted'; break;
case 'quote': msg = 'Block quote inserted'; break;
case 'code': msg = 'Code block inserted'; break;
case 'image': msg = 'Image insertion dialog opened'; break;
case 'pdf': msg = 'PDF attachment dialog opened'; break;
case 'table': msg = 'Table inserted'; break;
case 'ai-continue': simAI('continue writing'); break;
case 'ai-improve': simAI('improve writing'); break;
case 'ai-summarize': simAI('summarize'); break;
case 'ai-translate': simAI('translate'); break;
default: msg = 'Action: ' + action;
}
if (msg) showToast(msg);
editor.focus();
}
// =========================================================================
// Zone 2b — Bubble Menu (on text selection)
// =========================================================================
function showBubbleMenu() {
var sel = window.getSelection();
if (!sel || sel.isCollapsed || !sel.rangeCount) {
hideBubbleMenu();
return;
}
// Check selection is inside editor
var range = sel.getRangeAt(0);
if (!editor.contains(range.commonAncestorContainer)) {
hideBubbleMenu();
return;
}
var rect = range.getBoundingClientRect();
if (!rect || rect.width === 0) { hideBubbleMenu(); return; }
var editorRect = editorArea.getBoundingClientRect();
var top = rect.top - editorRect.top - 48; // above selection
var left = rect.left - editorRect.left + (rect.width / 2) - (bubbleMenu.offsetWidth / 2);
// Keep in viewport
if (top < 8) top = rect.bottom - editorRect.top + 8; // below if too high
if (left < 8) left = 8;
if (left + bubbleMenu.offsetWidth > editorRect.width - 8) {
left = editorRect.width - 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': document.execCommand('bold'); break;
case 'italic': document.execCommand('italic'); break;
case 'strike': document.execCommand('strikeThrough'); break;
case 'code': wrapInline('code'); break;
case 'link': promptLink(); 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();
editor.focus();
}
function wrapInline(tag) {
var sel = window.getSelection();
if (!sel || sel.isCollapsed) return;
var range = sel.getRangeAt(0);
var text = range.toString();
var node = document.createTextNode('<' + tag + '>' + text + '</' + tag + '>');
range.deleteContents();
range.insertNode(node);
}
function promptLink() {
var sel = window.getSelection();
if (!sel || sel.isCollapsed) return;
var url = prompt('Enter URL:');
if (url) {
document.execCommand('createLink', false, url);
}
}
// =========================================================================
// Zone 3 — Quick Insert Buttons
// =========================================================================
function injectQuickInserts() {
var lines = editor.querySelectorAll('.line');
lines.forEach(function(line) {
if (line.querySelector('.poc-quick-insert')) return;
var btn = document.createElement('button');
btn.className = 'poc-quick-insert';
btn.textContent = '\u2295'; // ⊕
btn.title = 'Quick insert';
btn.addEventListener('click', function(e) {
e.stopPropagation();
var rect = btn.getBoundingClientRect();
showQuickInsertMenu(rect.left + 28, rect.top);
});
line.insertBefore(btn, line.firstChild);
});
}
function showQuickInsertMenu(x, y) {
var existing = document.getElementById('poc-quick-insert-menu');
if (existing) existing.remove();
var menu = document.createElement('div');
menu.id = 'poc-quick-insert-menu';
menu.style.cssText = [
'position:absolute;z-index:190;',
'left:' + x + 'px;top:' + y + 'px;',
'width:180px;background:var(--bg-elevated);',
'border:1px solid var(--border-light);border-radius:var(--radius);',
'box-shadow:var(--shadow-popover);padding:4px;',
'animation:pocFadeSlideIn 100ms ease'
].join('');
var actions = [
{icon:'\uD83D\uDDBC', label:'Image', action:'image'},
{icon:'\uD83D\uDCC4', label:'PDF', action:'pdf'},
{icon:'</>', label:'Code Block', action:'code'},
{icon:'\uD83D\uDCCA', label:'Table', action:'table'},
{icon:'\u2728', label:'AI Actions', action:'ai'}
];
actions.forEach(function(a) {
var item = document.createElement('div');
item.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 10px;font-size:13px;color:var(--text-secondary);cursor:pointer;border-radius:var(--radius-sm);';
item.innerHTML = '<span style="font-size:15px">' + a.icon + '</span><span>' + a.label + '</span>';
item.addEventListener('mouseenter', function() { item.style.background = 'var(--bg-hover)'; });
item.addEventListener('mouseleave', function() { item.style.background = ''; });
item.addEventListener('click', function() {
menu.remove();
if (a.action === 'ai') {
showToast('AI actions opened');
} else {
selectSlashAction(a.action);
}
});
menu.appendChild(item);
});
editorArea.appendChild(menu);
// Click outside closes
setTimeout(function() {
document.addEventListener('click', function closeMenu() {
document.removeEventListener('click', closeMenu);
if (menu.parentNode) menu.remove();
});
}, 10);
}
// =========================================================================
// Zone 4 — AI Side 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 response to: "' + text + '" (simulated)';
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
// =========================================================================
// Slash menu trigger: type '/' at start of line or on empty line
editor.addEventListener('keydown', function(e) {
if (e.key === '/') {
// Check if we're in a position where slash makes sense (start of line or after space)
var sel = window.getSelection();
if (sel && sel.rangeCount) {
var range = sel.getRangeAt(0);
var node = range.startContainer;
var offset = range.startOffset;
// Only trigger on empty or near-empty context
var text = (node.textContent || '').substring(0, offset).trim();
if (text === '' || text === '') {
// Allow '/' to be typed, but also show menu
setTimeout(function() {
// Check if a '/' was actually inserted
var caret = getCaretCoordinates();
if (caret) {
showSlashMenu(caret.x, caret.y + 20);
}
}, 50);
}
}
}
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) {
var action = visible[slashActiveIdx].getAttribute('data-action');
selectSlashAction(action);
}
}
}
// Ctrl+J for AI panel toggle
if (e.ctrlKey && e.key === 'j') {
e.preventDefault();
toggleAIPanel();
}
});
slashSearch.addEventListener('input', function() {
filterSlashItems(slashSearch.value);
});
slashMenu.addEventListener('click', function(e) {
var item = e.target.closest('.poc-slash-item');
if (item) {
var action = item.getAttribute('data-action');
selectSlashAction(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 on selection change
document.addEventListener('selectionchange', function() {
clearTimeout(window._bubbleTimeout);
window._bubbleTimeout = setTimeout(function() {
var sel = window.getSelection();
if (sel && !sel.isCollapsed && editor.contains(sel.anchorNode)) {
showBubbleMenu();
} else if (!bubbleMoreMenu.classList.contains('visible')) {
hideBubbleMenu();
}
}, 80);
});
// 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;
}
if (btn.id === 'poc-bubble-ai') {
bubbleMoreMenu.classList.toggle('visible');
return;
}
var action = btn.getAttribute('data-action');
if (action) execBubbleAction(action);
});
bubbleMoreMenu.addEventListener('click', function(e) {
var item = e.target.closest('.poc-bubble-more-item');
if (!item) return;
e.stopPropagation();
var action = item.getAttribute('data-action');
execBubbleAction(action);
});
// Click outside closes menus
document.addEventListener('click', function(e) {
if (slashVisible && !slashMenu.contains(e.target) && e.target !== editor) {
hideSlashMenu();
}
if (bubbleVisible && !bubbleMenu.contains(e.target)) {
hideBubbleMenu();
}
var qiMenu = document.getElementById('poc-quick-insert-menu');
if (qiMenu && !qiMenu.contains(e.target)) {
qiMenu.remove();
}
});
// AI Panel buttons
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();
});
// AI suggestions
document.querySelectorAll('.poc-ai-suggestion').forEach(function(btn) {
btn.addEventListener('click', function() {
var text = btn.textContent.trim();
document.getElementById('poc-ai-input').value = text;
sendAIMessage();
});
});
// =========================================================================
// Save indicator & auto-save simulation
// =========================================================================
editor.addEventListener('input', function() {
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';
}, 800);
});
function updateWordCount() {
var text = editor.textContent || '';
var words = text.trim() ? text.trim().split(/\s+/).length : 0;
wordCount.textContent = words + ' words';
}
// 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';
}, 800);
});
// =========================================================================
// Toast
// =========================================================================
var toastTimer = null;
function showToast(msg) {
toast.textContent = msg;
toast.classList.add('visible');
clearTimeout(toastTimer);
toastTimer = setTimeout(function() {
toast.classList.remove('visible');
}, 2000);
}
// =========================================================================
// Caret coordinate helper
// =========================================================================
function getCaretCoordinates() {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) return null;
var range = sel.getRangeAt(0);
var rect = range.getClientRects()[0];
if (!rect) return null;
var editorRect = editorArea.getBoundingClientRect();
return {
x: rect.left - editorRect.left,
y: rect.top - editorRect.top
};
}
// =========================================================================
// Init
// =========================================================================
initSlashItems();
injectQuickInserts();
updateWordCount();
console.log('ObsiGate Editor POC initialized — all 5 zones active');
console.log(' Zone 1: Top Fixed Bar (always visible)');
console.log(' Zone 2a: Slash Command Menu (type / on empty line)');
console.log(' Zone 2b: Bubble Menu (select text)');
console.log(' Zone 3: Quick Insert (hover left of lines)');
console.log(' Zone 4: AI Side Panel (Ctrl+J or AI button)');
console.log(' Zone 5: Drag & Drop (drag any file over)');
})();
</script>
</body>
</html>