botserver/ui/suite/editor.html

520 lines
17 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Editor - General Bots</title>
<link rel="stylesheet" href="css/app.css" />
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--accent-color: #3b82f6;
--accent-hover: #2563eb;
--border-color: #475569;
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
height: 100vh;
overflow: hidden;
}
.editor-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.editor-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.editor-title {
display: flex;
align-items: center;
gap: 12px;
}
.editor-title-icon {
font-size: 24px;
}
.editor-title-text {
font-size: 16px;
font-weight: 600;
}
.editor-path {
font-size: 12px;
color: var(--text-secondary);
}
.editor-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.toolbar-group {
display: flex;
align-items: center;
gap: 4px;
padding-right: 12px;
border-right: 1px solid var(--border-color);
}
.toolbar-group:last-child {
border-right: none;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn:hover {
background: var(--border-color);
}
.btn-primary {
background: var(--accent-color);
color: white;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-small {
padding: 6px 10px;
font-size: 12px;
}
.editor-content {
flex: 1;
display: flex;
overflow: hidden;
}
.editor-wrapper {
display: flex;
flex: 1;
overflow: hidden;
}
.line-numbers {
width: 50px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
padding: 16px 8px;
overflow: hidden;
text-align: right;
user-select: none;
font-family: "Consolas", monospace;
font-size: 13px;
line-height: 1.6;
color: var(--text-secondary);
}
.text-editor {
flex: 1;
background: var(--bg-primary);
color: var(--text-primary);
border: none;
padding: 16px;
font-family: "Consolas", monospace;
font-size: 13px;
line-height: 1.6;
resize: none;
outline: none;
white-space: pre;
overflow: auto;
tab-size: 4;
}
.csv-editor {
flex: 1;
overflow: auto;
padding: 16px;
}
.csv-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.csv-table th,
.csv-table td {
border: 1px solid var(--border-color);
padding: 0;
min-width: 120px;
}
.csv-table th {
background: var(--bg-tertiary);
font-weight: 600;
}
.csv-table .row-num {
width: 40px;
min-width: 40px;
background: var(--bg-secondary);
color: var(--text-secondary);
text-align: center;
padding: 8px 4px;
font-size: 12px;
}
.csv-input {
width: 100%;
background: transparent;
border: none;
color: var(--text-primary);
padding: 8px 12px;
font-size: 13px;
outline: none;
}
.csv-input:focus {
background: var(--bg-secondary);
}
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 16px;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
font-size: 12px;
color: var(--text-secondary);
}
.status-left,
.status-right {
display: flex;
align-items: center;
gap: 16px;
}
.dirty-indicator {
width: 8px;
height: 8px;
background: var(--warning);
border-radius: 50%;
margin-left: 8px;
}
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline-block;
}
.spinner {
width: 14px;
height: 14px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.notification {
position: fixed;
bottom: 60px;
right: 20px;
padding: 12px 20px;
background: var(--bg-tertiary);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border-left: 4px solid var(--accent-color);
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
}
.notification.show {
opacity: 1;
transform: translateX(0);
}
.notification.success {
border-left-color: var(--success);
}
.notification.error {
border-left-color: var(--error);
}
</style>
</head>
<body>
<div class="editor-container">
<!-- Header -->
<div class="editor-header">
<div class="editor-title">
<span class="editor-title-icon">📝</span>
<div>
<span
class="editor-title-text"
id="editor-filename"
hx-get="/api/v1/editor/filename"
hx-trigger="load"
hx-swap="innerHTML"></div>
Untitled
</span>
<div
class="editor-path"
id="editor-filepath"
hx-get="/api/v1/editor/filepath"
hx-trigger="load"
hx-swap="innerHTML">
</div>
</div>
<span
class="dirty-indicator"
id="dirty-indicator"
style="display: none;"
title="Unsaved changes">
</span>
</div>
<div>
<a href="#drive"
class="btn btn-small"
hx-get="/api/drive/list"
hx-target="#main-content"
hx-push-url="true">
✕ Close
</a>
</div>
</div>
<!-- Toolbar -->
<div class="editor-toolbar">
<div class="toolbar-group">
<button
class="btn btn-primary btn-small"
hx-post="/api/v1/editor/save"
hx-include="#text-editor"
hx-indicator="#save-spinner"
hx-swap="none"
hx-on::after-request="showSaveNotification(event)">
<span class="htmx-indicator spinner" id="save-spinner"></span>
💾 Save
</button>
<button
class="btn btn-small"
hx-get="/api/v1/editor/save-as"
hx-target="#save-dialog"
hx-swap="innerHTML">
Save As
</button>
</div>
<div class="toolbar-group">
<button
class="btn btn-small"
hx-post="/api/v1/editor/undo"
hx-target="#editor-content"
hx-swap="innerHTML">
↩️ Undo
</button>
<button
class="btn btn-small"
hx-post="/api/v1/editor/redo"
hx-target="#editor-content"
hx-swap="innerHTML">
↪️ Redo
</button>
</div>
<div class="toolbar-group" id="text-tools">
<button
class="btn btn-small"
hx-post="/api/v1/editor/format"
hx-include="#text-editor"
hx-target="#text-editor"
hx-swap="innerHTML">
{ } Format
</button>
</div>
<div class="toolbar-group" id="csv-tools" style="display: none;">
<button
class="btn btn-small"
hx-post="/api/v1/editor/csv/add-row"
hx-target="#csv-table-body"
hx-swap="beforeend">
Row
</button>
<button
class="btn btn-small"
hx-post="/api/v1/editor/csv/add-column"
hx-target="#csv-editor"
hx-swap="innerHTML">
Column
</button>
</div>
</div>
<!-- Editor Content - loaded via HTMX based on file type -->
<div class="editor-content" id="editor-content">
<!-- Text Editor (default) -->
<div class="editor-wrapper" id="text-editor-wrapper">
<div
class="line-numbers"
id="line-numbers"
hx-get="/api/v1/editor/line-numbers"
hx-trigger="keyup from:#text-editor delay:100ms"
hx-swap="innerHTML">
1
</div>
<textarea
class="text-editor"
id="text-editor"
name="content"
spellcheck="false"
hx-post="/api/v1/editor/autosave"
hx-trigger="keyup changed delay:5s"
hx-swap="none"
hx-indicator="#autosave-indicator"
placeholder="Start typing or open a file..."></textarea>
</div>
<!-- CSV Editor (shown for .csv files) -->
<div class="csv-editor" id="csv-editor" style="display: none;">
<table class="csv-table">
<thead id="csv-table-head">
<tr>
<th class="row-num">#</th>
<th>
<input
type="text"
class="csv-input"
name="header_0"
value="Column 1"
hx-post="/api/v1/editor/csv/update-header"
hx-trigger="change"
hx-swap="none">
</th>
</tr>
</thead>
<tbody
id="csv-table-body"
hx-get="/api/v1/editor/csv/rows"
hx-trigger="load"
hx-swap="innerHTML">
</tbody>
</table>
</div>
</div>
<!-- Status Bar -->
<div class="status-bar">
<div class="status-left">
<span
id="file-type"
hx-get="/api/v1/editor/filetype"
hx-trigger="load"
hx-swap="innerHTML">
📄 Plain Text
</span>
<span>UTF-8</span>
<span
id="autosave-indicator"
class="htmx-indicator"
style="font-size: 11px;">
Saving...
</span>
</div>
<div class="status-right">
<span
id="cursor-position"
hx-get="/api/v1/editor/position"
hx-trigger="click from:#text-editor, keyup from:#text-editor"
hx-swap="innerHTML">
Ln 1, Col 1
</span>
</div>
</div>
</div>
<!-- Save Dialog (loaded via HTMX) -->
<div id="save-dialog"></div>
<!-- Notification -->
<div class="notification" id="notification"></div>
<script>
// Minimal JS for notification display (could be replaced with htmx extension)
function showSaveNotification(event) {
const notification = document.getElementById('notification');
if (event.detail.successful) {
notification.textContent = '✓ File saved';
notification.className = 'notification success show';
document.getElementById('dirty-indicator').style.display = 'none';
} else {
notification.textContent = '✗ Save failed';
notification.className = 'notification error show';
}
setTimeout(() => notification.classList.remove('show'), 3000);
}
// Mark as dirty on edit
document.getElementById('text-editor')?.addEventListener('input', function() {
document.getElementById('dirty-indicator').style.display = 'inline-block';
});
// Keyboard shortcuts using htmx triggers
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
htmx.trigger(document.querySelector('[hx-post="/api/v1/editor/save"]'), 'click');
}
});
</script>
</body>
</html>