118 lines
3.9 KiB
JavaScript
118 lines
3.9 KiB
JavaScript
/*
|
|
* DigestWindow — floating panel chrome for the digest canvas.
|
|
*
|
|
* 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 and
|
|
* inside a Home Assistant iframe card; neither has a 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 the
|
|
* LLM decides to emit.
|
|
*/
|
|
|
|
(function (global) {
|
|
'use strict';
|
|
|
|
var counter = 0;
|
|
|
|
function escapeHtml(text) {
|
|
return String(text)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
}
|
|
|
|
// Intentionally tiny: bold, italic, inline code and paragraph breaks. The
|
|
// schema calls content "markdown-ish", and 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, '<code>$1</code>')
|
|
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/(^|[\s(])\*([^*]+)\*/g, '$1<em>$2</em>');
|
|
}
|
|
|
|
function renderContent(content) {
|
|
var body = document.createElement('div');
|
|
body.className = 'digest-window-body';
|
|
|
|
if (Array.isArray(content)) {
|
|
var list = document.createElement('ul');
|
|
list.className = 'digest-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 the globe its
|
|
// 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 the globe.
|
|
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, '<br>');
|
|
body.appendChild(p);
|
|
});
|
|
|
|
return body;
|
|
}
|
|
|
|
var DigestWindow = {
|
|
open: function (options) {
|
|
options = options || {};
|
|
|
|
var container = options.container || document.body;
|
|
|
|
var el = document.createElement('section');
|
|
el.className = 'digest-window';
|
|
el.id = options.id || ('digest-window-' + (++counter));
|
|
if (options.variant) {
|
|
el.classList.add('digest-window-' + options.variant);
|
|
}
|
|
|
|
var bar = document.createElement('header');
|
|
bar.className = 'digest-window-titlebar';
|
|
|
|
var dots = document.createElement('span');
|
|
dots.className = 'digest-window-dots';
|
|
dots.setAttribute('aria-hidden', 'true');
|
|
bar.appendChild(dots);
|
|
|
|
var title = document.createElement('h2');
|
|
title.className = 'digest-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('digest-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.DigestWindow = DigestWindow;
|
|
})(window);
|