520 lines
17 KiB
HTML
520 lines
17 KiB
HTML
|
|
<!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>
|