Spaces:
Sleeping
Sleeping
| // Shared modal overlay utility (extracted from Sidebar.js in H4 4.0). | |
| // Builds a single `#app-modal-overlay` over the app, stopping propagation so | |
| // inputs/clicks inside the modal never reach the 3D controls. | |
| /** @typedef {import('../types.js').ModalOptions} ModalOptions */ | |
| /** | |
| * Show a custom modal overlay. Replaces any existing overlay. | |
| * @param {ModalOptions} options | |
| * @returns {void} | |
| */ | |
| export function showCustomModal({ title, contentHTML, onConfirm, onCancel, confirmText = "Confirm", cancelText = "Cancel", isAlert = false }) { | |
| // Clean old modal overlays | |
| const oldOverlay = document.getElementById('app-modal-overlay'); | |
| if (oldOverlay) oldOverlay.remove(); | |
| const overlay = document.createElement('div'); | |
| overlay.id = 'app-modal-overlay'; | |
| overlay.className = 'modal-overlay'; | |
| document.body.appendChild(overlay); | |
| overlay.innerHTML = ` | |
| <div class="modal-content app-panel"> | |
| <div class="modal-header"> | |
| <span class="modal-title">${title}</span> | |
| ${isAlert ? '<button id="modal-close-x" class="btn btn-secondary btn-square" style="width:24px; height:24px; min-height:24px; padding:0; font-size:10px; border-radius:50%;">✕</button>' : ''} | |
| </div> | |
| <div class="modal-body"> | |
| ${contentHTML} | |
| </div> | |
| <div class="modal-actions"> | |
| ${!isAlert ? `<button id="modal-cancel-btn" class="btn btn-secondary">${cancelText}</button>` : ''} | |
| <button id="modal-confirm-btn" class="btn btn-primary">${confirmText}</button> | |
| </div> | |
| </div> | |
| `; | |
| // Prevent propagation so inputs/clicks inside modal don't affect 3D controls | |
| ['mousedown', 'click', 'keydown', 'keyup'].forEach(evt => { | |
| overlay.addEventListener(evt, (e) => e.stopPropagation()); | |
| }); | |
| const closeOverlay = () => { | |
| overlay.classList.remove('active'); | |
| setTimeout(() => overlay.remove(), 300); | |
| }; | |
| const confirmBtn = overlay.querySelector('#modal-confirm-btn'); | |
| if (confirmBtn) { | |
| confirmBtn.onclick = () => { | |
| if (onConfirm) onConfirm(overlay); | |
| closeOverlay(); | |
| }; | |
| } | |
| const cancelBtn = overlay.querySelector('#modal-cancel-btn'); | |
| if (cancelBtn) { | |
| cancelBtn.onclick = () => { | |
| if (onCancel) onCancel(); | |
| closeOverlay(); | |
| }; | |
| } | |
| const closeX = overlay.querySelector('#modal-close-x'); | |
| if (closeX) { | |
| closeX.onclick = () => { | |
| if (onCancel) onCancel(); | |
| closeOverlay(); | |
| }; | |
| } | |
| // Force reflow and activate transition | |
| overlay.offsetHeight; | |
| overlay.classList.add('active'); | |
| } | |
| /** | |
| * Open the bookmark naming modal and lock/unlock PointerLock camera flight controls. | |
| * @param {{ docId: string, chunkIndex: number, dimensionIndex: number, value: number }} coordinates | |
| * @param {function(string): void} callback | |
| * @param {object} [controls] | |
| * @returns {void} | |
| */ | |
| export function showBookmarkNamingModal(coordinates, callback, controls = null) { | |
| if (controls && controls.isLocked) { | |
| controls.unlock(); | |
| } | |
| const valStr = typeof coordinates.value === 'number' ? coordinates.value.toFixed(4) : coordinates.value; | |
| const contentHTML = ` | |
| <div style="display:flex; flex-direction:column; gap: var(--spacing-sm); width: 100%; max-width: 420px; font-family: var(--font-sans);"> | |
| <p style="font-size: var(--font-size-small); color: var(--color-neutral-300); margin: 0; line-height: 1.5;"> | |
| Bookmark: Document <strong>${coordinates.docId}</strong> · Chunk <strong>${coordinates.chunkIndex}</strong> · Dimension <strong>${coordinates.dimensionIndex}</strong> (Value: <strong>${valStr}</strong>) | |
| </p> | |
| <div style="display:flex; flex-direction:column; gap: 4px; margin-top: var(--spacing-xs);"> | |
| <label for="bookmark-name-input" style="font-size: var(--font-size-caption); color: var(--color-neutral-400);">Bookmark name:</label> | |
| <input id="bookmark-name-input" class="input-field" type="text" placeholder="e.g. Attention peak" style="width: 100%; border: 1px solid oklch(from var(--color-neutral-50) l c h / 0.1); background: oklch(from var(--color-neutral-950) l c h / 0.5); color: var(--color-neutral-50); padding: var(--spacing-xs); border-radius: 4px;"> | |
| </div> | |
| </div> | |
| `; | |
| showCustomModal({ | |
| title: "New Bookmark", | |
| contentHTML: contentHTML, | |
| confirmText: "Save", | |
| cancelText: "Cancel", | |
| onConfirm: () => { | |
| // Handled by custom validation below | |
| }, | |
| onCancel: () => { | |
| if (controls) { | |
| controls.lock(); | |
| } | |
| } | |
| }); | |
| // Custom validation: override confirmBtn.onclick to prevent closing if input is empty | |
| const overlay = document.getElementById('app-modal-overlay'); | |
| if (overlay) { | |
| const confirmBtn = overlay.querySelector('#modal-confirm-btn'); | |
| const input = overlay.querySelector('#bookmark-name-input'); | |
| if (confirmBtn && input) { | |
| confirmBtn.onclick = () => { | |
| const name = input.value.trim(); | |
| if (!name) { | |
| input.style.borderColor = 'var(--color-semantic-error)'; | |
| input.focus(); | |
| return; // Prevent closing | |
| } | |
| callback(name); | |
| // Destroy the modal | |
| overlay.classList.remove('active'); | |
| setTimeout(() => overlay.remove(), 300); | |
| // Restore flight controls | |
| if (controls) { | |
| controls.lock(); | |
| } | |
| }; | |
| } | |
| // Focus the input field automatically | |
| setTimeout(() => { | |
| if (input) { | |
| input.focus(); | |
| input.select(); | |
| } | |
| }, 50); | |
| } | |
| } | |