aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/static
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin/static')
-rw-r--r--internal/admin/static/admin.css7
-rw-r--r--internal/admin/static/admin.js4
-rw-r--r--internal/admin/static/map.js119
3 files changed, 127 insertions, 3 deletions
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css
index 8c3ee8e..f068207 100644
--- a/internal/admin/static/admin.css
+++ b/internal/admin/static/admin.css
@@ -41,10 +41,15 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100
.search-bar input{width:100%;padding:6px 8px;font-size:13px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px}
.login{max-width:360px;margin:80px auto;background:var(--panel);padding:30px;border:1px solid var(--border);border-radius:6px}
.login h1{font-size:18px;margin-bottom:20px;text-align:center}
-.z-controls{position:absolute;top:12px;left:12px;display:flex;gap:6px;align-items:center;background:var(--panel);padding:6px 10px;border-radius:4px;border:1px solid var(--border);z-index:10}
+.z-controls-wrapper{position:absolute;top:12px;left:12px;z-index:10}
+.z-controls{position:relative;display:flex;gap:6px;align-items:center;background:var(--panel);padding:6px 10px;border-radius:4px;border:1px solid var(--border)}
.z-controls button{width:28px;height:28px;font-size:16px;line-height:1;background:var(--input-bg);color:var(--text);border:1px solid var(--border);border-radius:3px;cursor:pointer}
.z-controls button:hover{background:var(--accent)}
.z-controls .z-label{font-size:13px;min-width:40px;text-align:center;font-weight:bold}
+.z-controls .z-text-btn{width:auto}
+.z-controls .z-text-btn.active{background:var(--accent);border-color:#1a5a8e}
+.z-controls .z-sep{width:1px;height:20px;background:var(--border);flex-shrink:0}
+.z-dir-form{display:none;position:absolute;left:0;top:calc(100% + 4px);background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px 10px;z-index:11;gap:6px;align-items:center;white-space:nowrap}
.room-tooltip{position:absolute;background:var(--panel);border:1px solid var(--border);padding:6px 10px;border-radius:3px;font-size:11px;pointer-events:none;z-index:20;white-space:nowrap}
.notification{position:fixed;bottom:16px;right:16px;background:var(--panel);border:1px solid var(--border);padding:10px 16px;border-radius:4px;font-size:12px;z-index:100;animation:fadeIn .2s}
.notification.error{border-color:var(--danger)}
diff --git a/internal/admin/static/admin.js b/internal/admin/static/admin.js
index cce3bc9..f8e9d13 100644
--- a/internal/admin/static/admin.js
+++ b/internal/admin/static/admin.js
@@ -31,11 +31,11 @@ window.updateUndoBar = function() {
};
window.doUndo = function() {
- API.post('/api/undo/undo').then(function(r) { notify(r.message || 'Undo complete', 'success'); updateUndoBar(); if(window.refreshPage)refreshPage(); }).catch(function(e) { notify('Undo failed: '+e.message, 'error'); });
+ API.post('/api/undo/undo').then(function(r) { notify(r.message || 'Undo complete', 'success'); updateUndoBar(); if(r.has_duplicates) { window.location = '/'; return; } if(window.refreshPage)refreshPage(); }).catch(function(e) { notify('Undo failed: '+e.message, 'error'); });
};
window.doRedo = function() {
- API.post('/api/undo/redo').then(function(r) { notify(r.message || 'Redo complete', 'success'); updateUndoBar(); if(window.refreshPage)refreshPage(); }).catch(function(e) { notify('Redo failed: '+e.message, 'error'); });
+ API.post('/api/undo/redo').then(function(r) { notify(r.message || 'Redo complete', 'success'); updateUndoBar(); if(r.has_duplicates) { window.location = '/'; return; } if(window.refreshPage)refreshPage(); }).catch(function(e) { notify('Redo failed: '+e.message, 'error'); });
};
window.addEventListener('keydown', function(e) {
diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js
index 6acec47..740edf3 100644
--- a/internal/admin/static/map.js
+++ b/internal/admin/static/map.js
@@ -135,6 +135,13 @@ function loadMap() {
}
renderMap(data);
renderDisconnectedList(data);
+ var newRoomBtn = $('#newRoomBtn');
+ var newRoomSep = $('#newRoomSep');
+ if (newRoomBtn && newRoomSep) {
+ var empty = !data.rooms || data.rooms.length === 0;
+ newRoomBtn.style.display = empty ? '' : 'none';
+ newRoomSep.style.display = empty ? '' : 'none';
+ }
if (wasDirChange && data.seed) {
selectRoom(data.seed);
}
@@ -1716,4 +1723,116 @@ function updateDirSelect() {
}
}
+function showRenameDir() {
+ if (currentDir === '') { notify('Cannot rename root', 'error'); return; }
+ hideNewDir();
+ $('#renameDirBtn').classList.add('active');
+ var form = $('#renameDirForm');
+ form.style.display = 'flex';
+ var base = currentDir.includes('/') ? currentDir.split('/').pop() : currentDir;
+ var input = $('#renameDirInput');
+ input.placeholder = base;
+ input.value = '';
+ input.focus();
+}
+
+function cancelRenameDir() {
+ $('#renameDirForm').style.display = 'none';
+ $('#renameDirBtn').classList.remove('active');
+}
+
+function confirmRenameDir() {
+ var newName = $('#renameDirInput').value.trim();
+ if (!newName) { notify('Enter a directory name', 'error'); return; }
+ if (newName.indexOf('/') !== -1 || newName.indexOf('\\') !== -1 || newName === '.' || newName === '..') { notify('Invalid directory name', 'error'); return; }
+ API.post('/api/room-dirs/rename', {dir: currentDir, new_name: newName}).then(function(r) {
+ if (r.error) { notify(r.error, 'error'); return; }
+ notify('Renamed to ' + newName, 'success');
+ cancelRenameDir();
+ currentDir = r.new_dir;
+ reloadRoomDirs(function() {
+ changeDir(r.new_dir);
+ });
+ }).catch(function(e) { notify('Rename failed: ' + e.message, 'error'); });
+}
+
+function showNewDir() {
+ hideRenameDir();
+ $('#newDirBtn').classList.add('active');
+ var form = $('#newDirForm');
+ form.style.display = 'flex';
+ var input = $('#newDirInput');
+ input.value = '';
+ input.focus();
+}
+
+function cancelNewDir() {
+ $('#newDirForm').style.display = 'none';
+ $('#newDirBtn').classList.remove('active');
+}
+
+function confirmNewDir() {
+ var name = $('#newDirInput').value.trim();
+ if (!name) { notify('Enter a directory name', 'error'); return; }
+ if (name.indexOf('/') !== -1 || name.indexOf('\\') !== -1 || name === '.' || name === '..') { notify('Invalid directory name', 'error'); return; }
+ API.post('/api/room-dirs/create', {name: name}).then(function(r) {
+ if (r.error) { notify(r.error, 'error'); return; }
+ notify('Created ' + name, 'success');
+ cancelNewDir();
+ currentDir = name;
+ reloadRoomDirs(function() {
+ changeDir(name);
+ });
+ }).catch(function(e) { notify('Create failed: ' + e.message, 'error'); });
+}
+
+function onDirInputKey(event, type) {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ if (type === 'rename') confirmRenameDir();
+ else if (type === 'new') confirmNewDir();
+ } else if (event.key === 'Escape') {
+ event.preventDefault();
+ if (type === 'rename') cancelRenameDir();
+ else if (type === 'new') cancelNewDir();
+ }
+}
+
+function createEmptyRoom() {
+ API.post('/api/rooms/new-empty', {dir: currentDir}).then(function(r) {
+ if (r.error) { notify(r.error, 'error'); return; }
+ notify('Created room ' + r.room.id, 'success');
+ loadMap().then(function() {
+ if (r.room && r.room.id) selectRoom(r.room.id);
+ });
+ }).catch(function(e) { notify('Create room failed: ' + e.message, 'error'); });
+}
+
+function hideRenameDir() {
+ $('#renameDirForm').style.display = 'none';
+ $('#renameDirBtn').classList.remove('active');
+}
+
+function hideNewDir() {
+ $('#newDirForm').style.display = 'none';
+ $('#newDirBtn').classList.remove('active');
+}
+
+function reloadRoomDirs(cb) {
+ API.get('/api/room-dirs').then(function(dirs) {
+ var sel = $('#dirSelect');
+ if (sel) {
+ sel.innerHTML = '';
+ dirs.forEach(function(d) {
+ var opt = document.createElement('option');
+ opt.value = d;
+ opt.textContent = d || '(root)';
+ sel.appendChild(opt);
+ });
+ }
+ updateDirSelect();
+ if (cb) cb(dirs);
+ });
+}
+
document.addEventListener('DOMContentLoaded', function() { loadRoomDirs(function() { loadMap(); }); initPanelResize(); });