Icon topbar, module title-bar controls, hideable title bars, renames
Topbar: the add button is now just + and the layout button a 3-segment grid icon (two squares + one tall rectangle). Modules no longer build their own toolbars — moduleApi.header.addButton/addMenu put controls in the tile's title bar next to the gear/close (menus are fixed-positioned by the shell so a tile's overflow can't clip them, and everything a module adds is removed automatically on unmount). Each title bar grows a notch on its bottom edge that hides/shows the bar, ProdCom-Listener style; the choice persists with the layout. Modules renamed: ProdCom and ProPresenter. The ProdCom tile regains the original listener tools as icon buttons in its title bar: timestamps toggle (clock), A−/A+ text size, jump-to-latest, and a circle-with-cross clear button, plus the Channels menu. MODULE-GUIDE documents the header API; README updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH
This commit is contained in:
@@ -22,14 +22,6 @@ export default function create({ root, moduleApi }) {
|
||||
|
||||
root.innerHTML = `
|
||||
<div class="pt-wrap">
|
||||
<div class="pt-bar">
|
||||
<div class="pt-dd">
|
||||
<button class="pt-btn pt-channels-btn" title="Choose visible channels">Channels ▾</button>
|
||||
<div class="pt-menu" hidden></div>
|
||||
</div>
|
||||
<span class="pt-spacer"></span>
|
||||
<button class="pt-btn pt-clear" title="Clear this tile (does not affect ProdCom)">Clear</button>
|
||||
</div>
|
||||
<div class="pt-stream">
|
||||
<div class="pt-empty">Waiting for transcript…</div>
|
||||
<div class="pt-entries"></div>
|
||||
@@ -42,14 +34,66 @@ export default function create({ root, moduleApi }) {
|
||||
const entriesEl = root.querySelector('.pt-entries');
|
||||
const emptyEl = root.querySelector('.pt-empty');
|
||||
const jumpBtn = root.querySelector('.pt-jump');
|
||||
const channelBtn = root.querySelector('.pt-channels-btn');
|
||||
const channelMenu = root.querySelector('.pt-menu');
|
||||
|
||||
/* ── title-bar controls ─────────────────────────────────────────── */
|
||||
|
||||
const CLOCK_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>';
|
||||
const JUMP_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v11"/><path d="m7 10 5 5 5-5"/><path d="M5 20h14"/></svg>';
|
||||
const CLEAR_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="m9 9 6 6M15 9l-6 6"/></svg>';
|
||||
|
||||
let channelMenuEl = null; // the open menu's element (filled on each open)
|
||||
const channelsMenu = moduleApi.header.addMenu({
|
||||
label: 'Channels ▾',
|
||||
title: 'Choose visible channels',
|
||||
build(menu) {
|
||||
menu.classList.add('pt-menu');
|
||||
channelMenuEl = menu;
|
||||
rebuildChannelMenu();
|
||||
},
|
||||
});
|
||||
channelsMenu.button.classList.add('pt-channels-btn');
|
||||
|
||||
let timeBtn = null;
|
||||
timeBtn = moduleApi.header.addButton({
|
||||
icon: CLOCK_SVG,
|
||||
title: 'Show / hide timestamps',
|
||||
onClick() {
|
||||
moduleApi.saveInstanceSettings({ showTimes: !moduleApi.instanceSettings.showTimes });
|
||||
applyInstanceSettings();
|
||||
},
|
||||
});
|
||||
|
||||
function bumpTextSize(delta) {
|
||||
const current = Number(moduleApi.instanceSettings.textSize) || 15;
|
||||
moduleApi.saveInstanceSettings({ textSize: Math.min(48, Math.max(10, current + delta)) });
|
||||
applyInstanceSettings();
|
||||
}
|
||||
moduleApi.header.addButton({ label: 'A−', title: 'Smaller text', onClick: () => bumpTextSize(-2) });
|
||||
moduleApi.header.addButton({ label: 'A+', title: 'Larger text', onClick: () => bumpTextSize(2) });
|
||||
|
||||
moduleApi.header.addButton({
|
||||
icon: JUMP_SVG,
|
||||
title: 'Jump to latest',
|
||||
onClick() {
|
||||
pinnedToBottom = true;
|
||||
jumpBtn.hidden = true;
|
||||
scrollToBottom();
|
||||
},
|
||||
});
|
||||
|
||||
const clearBtn = moduleApi.header.addButton({
|
||||
icon: CLEAR_SVG,
|
||||
title: 'Clear this tile (does not affect ProdCom)',
|
||||
onClick: clearView,
|
||||
});
|
||||
clearBtn.classList.add('pt-clear');
|
||||
|
||||
function applyInstanceSettings() {
|
||||
const s = moduleApi.instanceSettings;
|
||||
const size = Math.min(48, Math.max(10, Number(s.textSize) || 15));
|
||||
wrap.style.setProperty('--pt-size', size + 'px');
|
||||
wrap.classList.toggle('show-times', Boolean(s.showTimes));
|
||||
timeBtn?.classList.toggle('active', Boolean(s.showTimes));
|
||||
}
|
||||
applyInstanceSettings();
|
||||
|
||||
@@ -106,7 +150,20 @@ export default function create({ root, moduleApi }) {
|
||||
);
|
||||
}
|
||||
|
||||
function updateChannelsLabel() {
|
||||
let visibleCount = 0;
|
||||
for (const id of channels.keys()) {
|
||||
if (!hiddenChannels.has(id)) visibleCount++;
|
||||
}
|
||||
channelsMenu.setLabel(visibleCount === channels.size
|
||||
? 'Channels ▾'
|
||||
: `Channels (${visibleCount}/${channels.size}) ▾`);
|
||||
}
|
||||
|
||||
function rebuildChannelMenu() {
|
||||
updateChannelsLabel();
|
||||
const channelMenu = channelMenuEl;
|
||||
if (!channelMenu) return; // menu not opened yet — the label is enough
|
||||
channelMenu.innerHTML = '';
|
||||
|
||||
if (groups.size) {
|
||||
@@ -134,10 +191,8 @@ export default function create({ root, moduleApi }) {
|
||||
channelMenu.appendChild(divider);
|
||||
}
|
||||
|
||||
let visibleCount = 0;
|
||||
for (const [id, info] of channels) {
|
||||
const on = !hiddenChannels.has(id);
|
||||
if (on) visibleCount++;
|
||||
const item = document.createElement('button');
|
||||
item.className = 'pt-channel-item' + (on ? ' on' : '');
|
||||
item.setAttribute('role', 'menuitemcheckbox');
|
||||
@@ -158,27 +213,15 @@ export default function create({ root, moduleApi }) {
|
||||
});
|
||||
channelMenu.appendChild(item);
|
||||
}
|
||||
channelBtn.textContent = visibleCount === channels.size
|
||||
? 'Channels ▾'
|
||||
: `Channels (${visibleCount}/${channels.size}) ▾`;
|
||||
}
|
||||
|
||||
channelBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
channelMenu.hidden = !channelMenu.hidden;
|
||||
});
|
||||
const onDocClick = (e) => {
|
||||
if (!channelMenu.hidden && !e.target.closest('.pt-dd')) channelMenu.hidden = true;
|
||||
};
|
||||
document.addEventListener('click', onDocClick);
|
||||
|
||||
/* ── clear (local view only) ────────────────────────────────────── */
|
||||
|
||||
root.querySelector('.pt-clear').addEventListener('click', () => {
|
||||
function clearView() {
|
||||
entryEls.forEach((el) => el.remove());
|
||||
entryEls.clear();
|
||||
emptyEl.hidden = false;
|
||||
});
|
||||
}
|
||||
|
||||
/* ── scrolling ──────────────────────────────────────────────────── */
|
||||
|
||||
@@ -393,8 +436,7 @@ export default function create({ root, moduleApi }) {
|
||||
stopped = true;
|
||||
clearTimeout(bootTimer);
|
||||
stream?.close();
|
||||
document.removeEventListener('click', onDocClick);
|
||||
root.innerHTML = '';
|
||||
root.innerHTML = ''; // header controls are removed by the shell
|
||||
},
|
||||
onResize() {
|
||||
if (pinnedToBottom) scrollToBottom();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "prodcom-transcript",
|
||||
"name": "ProdCom Transcript",
|
||||
"name": "ProdCom",
|
||||
"version": "1.0.0",
|
||||
"description": "Live ProdCom transcript stream",
|
||||
"client": "client.js",
|
||||
|
||||
@@ -9,49 +9,9 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ── toolbar ── */
|
||||
|
||||
[data-module="prodcom-transcript"] .pt-bar {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
[data-module="prodcom-transcript"] .pt-spacer { flex: 1; }
|
||||
|
||||
[data-module="prodcom-transcript"] .pt-btn {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 3px 8px;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
[data-module="prodcom-transcript"] .pt-btn:hover { border-color: var(--muted); }
|
||||
|
||||
[data-module="prodcom-transcript"] .pt-dd { position: relative; }
|
||||
|
||||
[data-module="prodcom-transcript"] .pt-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 5px);
|
||||
left: 0;
|
||||
z-index: 30;
|
||||
min-width: 170px;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
[data-module="prodcom-transcript"] .pt-menu[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. */
|
||||
|
||||
[data-module="prodcom-transcript"] .pt-channel-item {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user