feat: Add Exit Edit Mode button visibility toggle and tab management UI

Fix Exit Edit Mode button:
- Move button before Lock button (left side as requested)
- Add visibility toggle in editModeManager.js
  - Show done button when entering edit mode
  - Hide done button when exiting edit mode
- Fixes critical bug where button stayed hidden permanently

Add tab management UI:
- Create promptDialog.js for text input modals (rename, create)
- Create tabContextMenu.js with right-click context menu on tabs
- Integrate with TabManager API for all operations:
  - Add New Tab (with validation)
  - Rename Tab (with validation)
  - Change Icon (icon picker with 20 common icons)
  - Duplicate Tab (copies widgets)
  - Delete Tab (with confirmation, blocks if last tab)
- Auto-save after tab operations
- Event delegation for dynamic tab elements

Files changed:
- dashboardTemplate.html: Move done button to correct position
- editModeManager.js: Add visibility toggles in enter/exit methods
- promptDialog.js: New dialog system for text input
- tabContextMenu.js: New context menu system
- dashboardIntegration.js: Initialize tab context menu
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-11-04 13:34:36 +11:00
parent 3117cd2598
commit 2ab48190ea
5 changed files with 837 additions and 6 deletions
+282
View File
@@ -0,0 +1,282 @@
/**
* Prompt Dialog System
*
* Provides styled prompt dialogs for text input, similar to confirmDialog.
* Used for tab renaming, creation, etc.
*/
/**
* Show a prompt dialog with text input
* @param {Object} options - Dialog options
* @param {string} options.title - Dialog title
* @param {string} options.message - Dialog message/label
* @param {string} [options.defaultValue=''] - Default input value
* @param {string} [options.placeholder=''] - Input placeholder
* @param {string} [options.confirmText='OK'] - Confirm button text
* @param {string} [options.cancelText='Cancel'] - Cancel button text
* @param {Function} [options.validator] - Optional validation function (value) => {valid: boolean, error: string}
* @returns {Promise<string|null>} Resolves to input value if confirmed, null if cancelled
*/
export function showPromptDialog(options) {
return new Promise((resolve) => {
const {
title = 'Enter Value',
message = '',
defaultValue = '',
placeholder = '',
confirmText = 'OK',
cancelText = 'Cancel',
validator = null
} = options;
// Create modal container
const modal = document.createElement('div');
modal.className = 'rpg-modal rpg-prompt-modal';
modal.style.display = 'flex';
modal.style.position = 'fixed';
modal.style.inset = '0';
modal.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
modal.style.zIndex = '10001';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
// Create modal content
const modalContent = document.createElement('div');
modalContent.className = 'rpg-modal-content rpg-prompt-content';
modalContent.style.cssText = `
background: #16213e;
border-radius: 8px;
padding: 0;
min-width: 400px;
max-width: 90vw;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
`;
// Header
const header = document.createElement('div');
header.className = 'rpg-modal-header';
header.style.cssText = `
padding: 20px;
border-bottom: 1px solid #0f3460;
display: flex;
align-items: center;
justify-content: space-between;
`;
const headerContent = document.createElement('div');
headerContent.style.display = 'flex';
headerContent.style.alignItems = 'center';
headerContent.style.gap = '10px';
const icon = document.createElement('i');
icon.className = 'fa-solid fa-pencil';
icon.style.color = '#4ecca3';
icon.style.fontSize = '20px';
const titleEl = document.createElement('h3');
titleEl.textContent = title;
titleEl.style.cssText = `
margin: 0;
font-size: 18px;
color: #eeeeee;
`;
const closeBtn = document.createElement('button');
closeBtn.className = 'rpg-modal-close';
closeBtn.innerHTML = '<i class="fa-solid fa-times"></i>';
closeBtn.style.cssText = `
background: transparent;
border: none;
color: #eeeeee;
font-size: 20px;
cursor: pointer;
padding: 4px;
width: 28px;
height: 28px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
`;
closeBtn.onmouseenter = () => closeBtn.style.background = '#e94560';
closeBtn.onmouseleave = () => closeBtn.style.background = 'transparent';
headerContent.appendChild(icon);
headerContent.appendChild(titleEl);
header.appendChild(headerContent);
header.appendChild(closeBtn);
// Body
const body = document.createElement('div');
body.className = 'rpg-modal-body';
body.style.cssText = `
padding: 20px;
`;
if (message) {
const messageEl = document.createElement('p');
messageEl.textContent = message;
messageEl.style.cssText = `
margin: 0 0 15px 0;
color: #eeeeee;
font-size: 14px;
`;
body.appendChild(messageEl);
}
const input = document.createElement('input');
input.type = 'text';
input.value = defaultValue;
input.placeholder = placeholder;
input.style.cssText = `
width: 100%;
padding: 10px;
background: #0f3460;
border: 1px solid #1a4d7a;
border-radius: 4px;
color: #eeeeee;
font-size: 14px;
font-family: inherit;
box-sizing: border-box;
`;
const errorEl = document.createElement('div');
errorEl.className = 'rpg-prompt-error';
errorEl.style.cssText = `
margin-top: 8px;
color: #e94560;
font-size: 12px;
min-height: 18px;
`;
body.appendChild(input);
body.appendChild(errorEl);
// Footer
const footer = document.createElement('div');
footer.className = 'rpg-modal-footer';
footer.style.cssText = `
padding: 20px;
border-top: 1px solid #0f3460;
display: flex;
gap: 10px;
justify-content: flex-end;
`;
const cancelBtn = document.createElement('button');
cancelBtn.className = 'rpg-btn-secondary';
cancelBtn.textContent = cancelText;
cancelBtn.style.cssText = `
padding: 10px 20px;
background: #0f3460;
border: none;
border-radius: 6px;
color: #eeeeee;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
`;
cancelBtn.onmouseenter = () => cancelBtn.style.background = '#1a4d7a';
cancelBtn.onmouseleave = () => cancelBtn.style.background = '#0f3460';
const confirmBtn = document.createElement('button');
confirmBtn.className = 'rpg-btn-primary';
confirmBtn.textContent = confirmText;
confirmBtn.style.cssText = `
padding: 10px 20px;
background: #4ecca3;
border: none;
border-radius: 6px;
color: #16213e;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
`;
confirmBtn.onmouseenter = () => confirmBtn.style.background = '#45b993';
confirmBtn.onmouseleave = () => confirmBtn.style.background = '#4ecca3';
footer.appendChild(cancelBtn);
footer.appendChild(confirmBtn);
// Assemble modal
modalContent.appendChild(header);
modalContent.appendChild(body);
modalContent.appendChild(footer);
modal.appendChild(modalContent);
// Append to body
document.body.appendChild(modal);
// Validation helper
const validate = () => {
if (!validator) return { valid: true, error: '' };
const result = validator(input.value);
errorEl.textContent = result.error || '';
return result;
};
// Handle confirm
const handleConfirm = () => {
const validation = validate();
if (!validation.valid) {
input.focus();
return;
}
modal.remove();
cleanup();
resolve(input.value);
};
// Handle cancel
const handleCancel = () => {
modal.remove();
cleanup();
resolve(null);
};
// Handle keyboard
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
e.preventDefault();
handleCancel();
} else if (e.key === 'Enter') {
e.preventDefault();
handleConfirm();
}
};
// Handle backdrop click
const handleBackdropClick = (e) => {
if (e.target === modal) {
handleCancel();
}
};
// Clean up event listeners
const cleanup = () => {
confirmBtn.removeEventListener('click', handleConfirm);
cancelBtn.removeEventListener('click', handleCancel);
closeBtn.removeEventListener('click', handleCancel);
input.removeEventListener('keydown', handleKeyDown);
modal.removeEventListener('click', handleBackdropClick);
};
// Attach event listeners
confirmBtn.addEventListener('click', handleConfirm);
cancelBtn.addEventListener('click', handleCancel);
closeBtn.addEventListener('click', handleCancel);
input.addEventListener('keydown', handleKeyDown);
modal.addEventListener('click', handleBackdropClick);
// Focus input and select default text
setTimeout(() => {
input.focus();
if (defaultValue) {
input.select();
}
}, 100);
});
}