PageSourceSearch

https://smartcone.ai/src/appsPopup.js

js smartcone.ai collected 2026-10-05 15:06:08 UTC 6,324 bytes, 168 lines download raw bytes

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.