1// /src/appsPopup.js 2// Apps popup â the tray's leftmost button opens this grid of standalone apps. 3// 4// The apps here are NOT Studio authoring tools: Server Manager, Mail, Project 5// Manager, Application Manager, Proc Creator, Web Analyst and Browser are 6// products in their own right, and Mind Map / ER Diagram / Story Board are the 7// free-tier apps that apps.html offers. Keeping them out of the tray leaves the 8// tray to the things that build on the canvas; this popup is how you reach the 9// rest without leaving the Studio. 10// 11// â The tiles come from src/appsCatalog.js, the SAME declaration apps.html 12// renders. Do not add an app here â add it to the catalogue and both surfaces 13// get it. A tile that exists on one launcher and not the other is invisible 14// until a user goes looking for it. 15 16import { APP_GROUPS, appsInGroup, fillAppTile } from "./appsCatalog.js"; 17 18/** Must clear the tutorial overlay (99990) and card (99995), like the Projects popup. */ 19const POPUP_Z_INDEX = 100000; 20const POPUP_ID = "appsPopup"; 21 22/** 23 * Open (or close, if already open) the Apps popup. 24 * 25 * Toggling on a second call matches the Projects button next to it â pressing 26 * the same tray button twice should put you back where you started. 27 * 28 * @param {HTMLElement} [anchorEl] tray button to anchor above; centred if absent. 29 */ 30export async function openAppsPopup(anchorEl) { 31 const existing = document.getElementById(POPUP_ID); 32 if (existing) { existing.remove(); return; } 33 34 const popup = document.createElement("div"); 35 popup.id = POPUP_ID; 36 popup.className = "apps-popup"; 37 popup.setAttribute("role", "dialog"); 38 popup.setAttribute("aria-label", "Apps"); 39 popup.style.zIndex = String(POPUP_Z_INDEX); 40 41 // --- Header --- 42 const header = document.createElement("div"); 43 header.className = "apps-popup-header"; 44 header.textContent = "\u{1F680} Apps"; 45 46 const closeBtn = document.createElement("span"); 47 closeBtn.className = "apps-popup-close"; 48 closeBtn.setAttribute("role", "button"); 49 closeBtn.setAttribute("tabindex", "0"); 50 closeBtn.setAttribute("aria-label", "Close"); 51 closeBtn.textContent = "â"; 52 header.appendChild(closeBtn); 53 54 // --- Body: one section per catalogue group --- 55 const body = document.createElement("div"); 56 body.className = "apps-popup-body"; 57 58 for (const group of APP_GROUPS) { 59 const apps = appsInGroup(group.id); 60 if (!apps.length) continue; 61 62 const heading = document.createElement("h3"); 63 heading.className = "apps-popup-group"; 64 heading.textContent = group.title; 65 body.appendChild(heading); 66 67 const grid = document.createElement("div"); 68 grid.className = "apps-popup-grid"; 69 grid.setAttribute("aria-label", `${group.title} launcher`); 70 71 for (const app of apps) { 72 const btn = document.createElement("button"); 73 btn.className = "apps-popup-tile"; 74 fillAppTile(btn, app, { iconClass: "apps-popup-tile-icon", labelClass: "apps-popup-tile-label" }); 75 btn.addEventListener("click", () => launch(app)); 76 grid.appendChild(btn); 77 } 78 body.appendChild(grid); 79 } 80 81 popup.append(header, body); 82 document.body.appendChild(popup); 83 positionAbove(popup, anchorEl); 84 makeDraggable(popup, header); 85 86 // --- Dismissal --- 87 function close() { 88 popup.remove(); 89 window.removeEventListener("keydown", escHandler); 90 document.removeEventListener("pointerdown", outsideHandler, true); 91 } 92 function escHandler(e) { if (e.key === "Escape") close(); } 93 function outsideHandler(e) { 94 if (popup.contains(e.target)) return; 95 // The tray button that opened this popup toggles it itself; ignoring the 96 // whole tray here stops the two handlers from fighting (close-then-reopen, 97 // which reads as the button doing nothing). 98 if (e.target.closest?.("#appTray")) return; 99 close(); 100 } 101 closeBtn.addEventListener("click", close); 102 closeBtn.addEventListener("keydown", (e) => { 103 if (e.key === "Enter" || e.key === " ") close(); 104 }); 105 window.addEventListener("keydown", escHandler); 106 document.addEventListener("pointerdown", outsideHandler, true); 107 108 /** 109 * Launch through toggleAppShell so the popup behaves exactly like the tray: 110 * same shell sizes, same de-duplication, same close tracking. The import is 111 * dynamic because appTray.js imports this module to build the button â a 112 * static import back would be a cycle. 113 */ 114 async function launch(app) { 115 close(); 116 // An `href` tile navigates rather than opening a shell. Studio is kept out of 117 // this popup (see APP_GROUPS), so this is belt-and-braces for any future one. 118 if (app.href) { window.location.href = app.href; return; } 119 try { 120 const { toggleAppShell } = await import("./appTray.js"); 121 await toggleAppShell(app.id, app.label); 122 } catch (err) { 123 console.error(`[appsPopup] Failed to launch "${app.id}":`, err); 124 } 125 } 126} 127 128/** 129 * Sit the popup just above the tray button that opened it, clamped to the 130 * viewport. Falls back to centring when there is no anchor (or it is offscreen). 131 */
132function positionAbove(el, anchorEl) { 133 if (!anchorEl) return; // CSS centres it 134 const a = anchorEl.getBoundingClientRect(); 135 if (!a.width && !a.height) return; 136 const r = el.getBoundingClientRect(); 137 const margin = 12; 138 const left = Math.max(margin, Math.min(a.left, window.innerWidth - r.width - margin)); 139 const top = Math.max(margin, a.top - r.height - 10); 140 el.style.transform = ""; 141 el.style.left = `${left}px`; 142 el.style.top = `${top}px`; 143} 144 145/** Drag by the header â same affordance as the Projects popup. */ 146function makeDraggable(el, handle) { 147 let startX, startY, origLeft, origTop; 148 149 handle.addEventListener("pointerdown", (e) => { 150 if (e.target.classList.contains("apps-popup-close")) return; 151 startX = e.clientX; 152 startY = e.clientY; 153 const rect = el.getBoundingClientRect(); 154 origLeft = rect.left; 155 origTop = rect.top; 156 el.style.transform = ""; 157 el.style.left = `${origLeft}px`; 158 el.style.top = `${origTop}px`; 159 handle.setPointerCapture(e.pointerId); 160 e.preventDefault(); 161 }); 162 163 handle.addEventListener("pointermove", (e) => { 164 if (!handle.hasPointerCapture(e.pointerId)) return; 165 el.style.left = `${origLeft + (e.clientX - startX)}px`; 166 el.style.top = `${origTop + (e.clientY - startY)}px`; 167 }); 168}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.