/* * AdminWindow — floating panel chrome for the admin canvas. * * A near-verbatim copy of digest-engine/render/digest-canvas-sdk/window-chrome.js * (docs/project-plan.md Phase 12), duplicated rather than shared per Phase 13's * decision to keep the two canvases fully decoupled. Only the `digest-` class/name * prefix has been renamed to `admin-` throughout; the behaviour is identical. * * Windows look like windows (title bar, rounded corners, shadow, glow accent) but * are not actually draggable. The canvas is read on a wall-mounted kiosk; there is * no pointer doing window management, and real drag-and-drop would only add state * that nothing persists. * * x/y/w/h are optional. Given, the window is absolutely positioned (percentages of * the canvas) — that is the escape hatch for a deliberately composed layout. * Omitted, the window participates in the canvas's normal flow/grid layout, which * is what render.js does by default so the layout survives any window count. */ (function (global) { 'use strict'; var counter = 0; function escapeHtml(text) { return String(text) .replace(/&/g, '&') .replace(//g, '>'); } // Intentionally tiny: bold, italic, inline code and paragraph breaks — pulling in // a markdown parser for three inline forms would violate this project's // no-unnecessary-deps rule. function renderInline(text) { return escapeHtml(text) .replace(/`([^`]+)`/g, '$1') .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/(^|[\s(])\*([^*]+)\*/g, '$1$2'); } function renderContent(content) { var body = document.createElement('div'); body.className = 'admin-window-body'; if (Array.isArray(content)) { var list = document.createElement('ul'); list.className = 'admin-window-list'; content.forEach(function (item) { var li = document.createElement('li'); li.innerHTML = renderInline(item); list.appendChild(li); }); body.appendChild(list); return body; } // Duck-typed rather than `instanceof Node`: render.js hands stat/chart windows // their mount point through here, and an identity check against a global that // may not exist in every embedding context is a needless way to lose it. if (content && typeof content.nodeType === 'number') { body.appendChild(content); return body; } String(content === undefined || content === null ? '' : content) .split(/\n{2,}/) .forEach(function (block) { if (!block.trim()) { return; } var p = document.createElement('p'); p.innerHTML = renderInline(block).replace(/\n/g, '
'); body.appendChild(p); }); return body; } var AdminWindow = { open: function (options) { options = options || {}; var container = options.container || document.body; var el = document.createElement('section'); el.className = 'admin-window'; el.id = options.id || ('admin-window-' + (++counter)); if (options.variant) { el.classList.add('admin-window-' + options.variant); } var bar = document.createElement('header'); bar.className = 'admin-window-titlebar'; var dots = document.createElement('span'); dots.className = 'admin-window-dots'; dots.setAttribute('aria-hidden', 'true'); bar.appendChild(dots); var title = document.createElement('h2'); title.className = 'admin-window-title'; title.textContent = options.title || ''; bar.appendChild(title); el.appendChild(bar); el.appendChild(renderContent(options.content)); if (typeof options.x === 'number' && typeof options.y === 'number') { el.classList.add('admin-window-positioned'); el.style.left = options.x + '%'; el.style.top = options.y + '%'; } if (typeof options.w === 'number') { el.style.width = options.w + '%'; } if (typeof options.h === 'number') { el.style.height = options.h + '%'; } container.appendChild(el); return el; } }; global.AdminWindow = AdminWindow; })(window);