Shell 1.9.4: header menus open where their button is, folded bar or not; ProdCom 1.3.3 keeps its filter on a narrow tile
A tile's dropdown menus (ProdCom's Channels, the Timers menu) opened but never showed under a folded-away title bar — in fullscreen, or with auto-hide on. The bar carries a backdrop-filter and a transform while folded, and either one makes a fixed-positioned descendant position itself against the bar instead of the viewport: the menu landed a screen-height below its button, inside the tile's overflow. A tile near the top of the screen showed a misplaced menu; anything lower showed nothing at all. The shell now hosts every header menu on the body, under an element carrying the tile's data-module so a module's scoped CSS still reaches its items; places it whole inside the viewport; holds a folded bar open while one of its menus is open (the pointer has to travel down into the menu); and marks the open menu's button the way the gear is marked. ProdCom: seven controls need ~175 px of bar, and the shell clips what a narrow tile cannot fit — the Channels button went with the rest. Below 380 px the six secondary controls fold into a ⋯ menu offering the same actions, so the filter is the last thing to go. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
92b98b6579
commit
e3967af110
@@ -81,8 +81,8 @@ export default function create({ root, moduleApi }) {
|
||||
moduleApi.saveInstanceSettings({ textSize: Math.min(48, Math.max(10, current + delta)) });
|
||||
applyInstanceSettings();
|
||||
}
|
||||
moduleApi.header.addButton({ icon: MINUS_SVG, title: 'Smaller text', onClick: () => bumpTextSize(-2) });
|
||||
moduleApi.header.addButton({ icon: PLUS_SVG, title: 'Larger text', onClick: () => bumpTextSize(2) });
|
||||
const minusBtn = moduleApi.header.addButton({ icon: MINUS_SVG, title: 'Smaller text', onClick: () => bumpTextSize(-2) });
|
||||
const plusBtn = moduleApi.header.addButton({ icon: PLUS_SVG, title: 'Larger text', onClick: () => bumpTextSize(2) });
|
||||
|
||||
// icon and tooltip track the current direction — applyInstanceSettings sets both
|
||||
const flowBtn = moduleApi.header.addButton({
|
||||
@@ -95,7 +95,7 @@ export default function create({ root, moduleApi }) {
|
||||
},
|
||||
});
|
||||
|
||||
moduleApi.header.addButton({
|
||||
const latestBtn = moduleApi.header.addButton({
|
||||
icon: JUMP_SVG,
|
||||
title: 'Latest',
|
||||
onClick() {
|
||||
@@ -112,6 +112,82 @@ export default function create({ root, moduleApi }) {
|
||||
});
|
||||
clearBtn.classList.add('pt-clear');
|
||||
|
||||
/* ── a narrow tile: the bar keeps Channels, the rest fold into ⋯ ──
|
||||
Seven controls need ~175 px, and the shell clips whatever the bar
|
||||
cannot fit — on a 3-column tile among many, or one squeezed by the
|
||||
fullscreen fit, that used to take the channel filter with it. Below
|
||||
COMPACT_PX the six secondary controls hide and a single ⋯ menu offers
|
||||
the same actions, so the filter button is the last thing to go. */
|
||||
const MORE_SVG = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>';
|
||||
const COMPACT_PX = 380; // tile width below which the full bar no longer fits
|
||||
|
||||
let moreMenuEl = null;
|
||||
const moreMenu = moduleApi.header.addMenu({
|
||||
icon: MORE_SVG,
|
||||
title: 'More',
|
||||
build(menu) {
|
||||
menu.classList.add('pt-menu');
|
||||
moreMenuEl = menu;
|
||||
rebuildMoreMenu();
|
||||
},
|
||||
});
|
||||
const moreWrap = moreMenu.button.parentElement; // the shell's .tile-dd
|
||||
const secondaryBtns = [timeBtn, minusBtn, plusBtn, flowBtn, latestBtn, clearBtn];
|
||||
|
||||
function addMoreItem(label, { on = null, onClick } = {}) {
|
||||
const item = document.createElement('button');
|
||||
item.className = 'pt-channel-item' + (on ? ' on' : '');
|
||||
if (on !== null) {
|
||||
item.setAttribute('role', 'menuitemcheckbox');
|
||||
item.setAttribute('aria-checked', String(on));
|
||||
} else {
|
||||
item.setAttribute('role', 'menuitem');
|
||||
}
|
||||
const check = document.createElement('span');
|
||||
check.className = 'check';
|
||||
check.textContent = '✓';
|
||||
const name = document.createElement('span');
|
||||
name.className = 'name';
|
||||
name.textContent = label;
|
||||
item.append(check, name);
|
||||
item.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // the rebuild detaches this item — keep the menu open
|
||||
onClick();
|
||||
});
|
||||
moreMenuEl.appendChild(item);
|
||||
}
|
||||
|
||||
function rebuildMoreMenu() {
|
||||
if (!moreMenuEl) return;
|
||||
moreMenuEl.innerHTML = '';
|
||||
const s = moduleApi.instanceSettings;
|
||||
addMoreItem('Timestamps', { on: Boolean(s.showTimes), onClick: () => timeBtn.click() });
|
||||
addMoreItem('Newest first', { on: newestFirst(), onClick: () => flowBtn.click() });
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'pt-menu-divider';
|
||||
moreMenuEl.appendChild(divider);
|
||||
addMoreItem('Smaller text', { onClick: () => bumpTextSize(-2) });
|
||||
addMoreItem('Larger text', { onClick: () => bumpTextSize(2) });
|
||||
addMoreItem('Latest', { onClick: () => { moreMenu.close(); latestBtn.click(); } });
|
||||
addMoreItem('Clear', { onClick: () => { moreMenu.close(); clearView(); } });
|
||||
}
|
||||
|
||||
let compact = null; // unknown until the first measurement
|
||||
function applyCompact(width) {
|
||||
const next = width > 0 && width < COMPACT_PX;
|
||||
if (next === compact) return;
|
||||
compact = next;
|
||||
for (const btn of secondaryBtns) btn.hidden = compact;
|
||||
moreWrap.hidden = !compact;
|
||||
if (!compact) moreMenu.close();
|
||||
}
|
||||
// the tile's own body tells the width; onResize only knows grid cells
|
||||
const sizeWatch = new ResizeObserver((entries) => {
|
||||
applyCompact(entries[0]?.contentRect.width || root.clientWidth);
|
||||
});
|
||||
sizeWatch.observe(root);
|
||||
applyCompact(root.clientWidth);
|
||||
|
||||
function applyInstanceSettings() {
|
||||
const s = moduleApi.instanceSettings;
|
||||
const size = Math.min(48, Math.max(10, Number(s.textSize) || 15));
|
||||
@@ -126,6 +202,7 @@ export default function create({ root, moduleApi }) {
|
||||
flowBtn.classList.toggle('active', up);
|
||||
flowBtn.title = up ? 'Newest first' : 'Oldest first';
|
||||
jumpBtn.textContent = (up ? '↑' : '↓') + ' New messages';
|
||||
rebuildMoreMenu(); // its checkmarks mirror the buttons
|
||||
}
|
||||
applyInstanceSettings();
|
||||
|
||||
@@ -415,6 +492,7 @@ export default function create({ root, moduleApi }) {
|
||||
},
|
||||
stop() {
|
||||
stopped = true;
|
||||
sizeWatch.disconnect();
|
||||
stream?.close();
|
||||
root.innerHTML = ''; // header controls are removed by the shell
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "prodcom-transcript",
|
||||
"name": "ProdCom",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.3",
|
||||
"proddash": ">=1.1.0",
|
||||
"description": "Live ProdCom transcript",
|
||||
"permissions": [
|
||||
|
||||
@@ -20,9 +20,13 @@
|
||||
}
|
||||
[data-module="prodcom-transcript"] .pt-count[hidden] { display: none; }
|
||||
|
||||
/* narrow tile: the secondary controls fold into the ⋯ menu (client.js) */
|
||||
[data-module="prodcom-transcript"] .tile-btn[hidden],
|
||||
[data-module="prodcom-transcript"] .tile-dd[hidden] { display: none; }
|
||||
|
||||
/* ── channels menu ──
|
||||
The menu container is created and positioned by the shell (.tile-menu);
|
||||
it lives inside the tile, so [data-module] scoping still applies. */
|
||||
it hangs off the body under a [data-module] host, so scoping still applies. */
|
||||
|
||||
[data-module="prodcom-transcript"] .pt-channel-item {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user