PCO Plan 1.9.0: a Song Key tile, keys as notes, and AbleSet-shaped section chips #32

Merged
michael merged 4 commits from claude/pco-song-key into main 2026-09-29 14:58:57 -04:00
4 changed files with 49 additions and 23 deletions
Showing only changes of commit 06066529bc - Show all commits
+8 -4
View File
@@ -359,10 +359,14 @@ below. Nothing is installed without someone clicking.
explain.
**Song Key** is one value, as large as the tile allows: the key of the item
ProPresenter has live, set as a note (B♭, F♯m), with the song's name and the
next key under it — both switches, as is the colour. When the feed that
picks the item is gone the key stays up, faded, and the tile's dot says so
rather than pretending to be current.
ProPresenter has live, with the song's name and the next key under it — both
switches, as is the colour. When the feed that picks the item is gone the key
stays up, faded, and the tile's dot says so rather than pretending to be
current.
Keys are written as notes wherever the module shows one: Planning Center's
`Bb` and `F#m` read as B♭ and F♯m, on the Plan tile's item badge and in the
Song Key tile alike.
**Teams** shows the same plan's team and position assignments — each team
as a band of its people, with their photo (fetched through ProdDash, never
+3 -1
View File
@@ -14,6 +14,7 @@
import createTeams from "./teams-client.js";
import createKey from "./key-client.js";
import { prettyKey } from "./key-notation.js";
const REFRESH_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg>';
@@ -369,7 +370,8 @@ function createPlan({ root, moduleApi }) {
if (on('showKey') && item.keyName) {
const key = document.createElement('span');
key.className = 'pp-key';
key.textContent = item.keyName;
// B♭, F♯m — the same notation the Song Key tile uses.
key.textContent = prettyKey(item.keyName);
titleLine.appendChild(key);
}
if (on('showNowIndicator')) {
+13 -18
View File
@@ -4,6 +4,8 @@
streamed state the Plan tile does, so it needs no connection of its own;
which item is current comes from the ProPresenter module, as ever. */
import { parseKey, prettyKey } from "./key-notation.js";
export default function createKey({ root, moduleApi }) {
let state = null;
let stream = null;
@@ -69,13 +71,7 @@ export default function createKey({ root, moduleApi }) {
}
if (!state.live.ppReachable) return moduleApi.setStatus('error', 'ProPresenter unreachable — showing the last item');
if (!item) return moduleApi.setStatus('ok', 'Waiting for an item to go live');
moduleApi.setStatus('ok', `${titleOf(item)}${item.keyName ? ` — ${item.keyName}` : ' — no key in the plan'}`);
}
/** "Bb" → "B♭", "F#m" → "F♯m" — for the plain-text line under the key. */
function prettyKey(text) {
const match = /^([A-G])([#b])(.*)$/.exec(String(text || '').trim());
return match ? `${match[1]}${match[2] === '#' ? '♯' : '♭'}${match[3]}` : String(text || '');
moduleApi.setStatus('ok', `${titleOf(item)}${item.keyName ? ` — ${prettyKey(item.keyName)}` : ' — no key in the plan'}`);
}
/**
@@ -84,23 +80,22 @@ export default function createKey({ root, moduleApi }) {
*/
function drawKey(text) {
keyEl.innerHTML = '';
const match = /^([A-G])([#b♯♭]?)(.*)$/.exec(String(text).trim());
if (!match) {
const parts = parseKey(text);
if (!parts) {
keyEl.textContent = text;
return;
}
const [, note, accidental, rest] = match;
keyEl.appendChild(document.createTextNode(note));
if (accidental) {
const sup = document.createElement('span');
sup.className = 'pk-accidental';
sup.textContent = accidental === '#' ? '♯' : accidental === 'b' ? '♭' : accidental;
keyEl.appendChild(sup);
keyEl.appendChild(document.createTextNode(parts.note));
if (parts.accidental) {
const acc = document.createElement('span');
acc.className = 'pk-accidental';
acc.textContent = parts.accidental;
keyEl.appendChild(acc);
}
if (rest) {
if (parts.quality) {
const tail = document.createElement('span');
tail.className = 'pk-quality';
tail.textContent = rest;
tail.textContent = parts.quality;
keyEl.appendChild(tail);
}
}
+25
View File
@@ -0,0 +1,25 @@
/* PCO Plan — how a song key is written.
Planning Center stores keys as typed: "Bb", "F#m", "Ab". Every tile in this
module shows them as notes instead — B♭, F♯m, A♭ — so the plan, the badge
on an item and the Song Key tile all agree. Shared rather than repeated
because the day one of them changes, they all should. */
/** "F#m" → { note: 'F', accidental: '♯', quality: 'm' }; anything odd → null. */
export function parseKey(raw) {
const match = /^([A-G])\s*([#b♯♭]?)\s*(.*)$/.exec(String(raw || '').trim());
if (!match) return null;
const [, note, accidental, quality] = match;
return {
note,
accidental: accidental === '#' ? '♯' : accidental === 'b' ? '♭' : accidental,
quality: quality.trim(),
};
}
/** The same thing as one string, for a badge or a caption. */
export function prettyKey(raw) {
const parts = parseKey(raw);
if (!parts) return String(raw || '').trim();
return `${parts.note}${parts.accidental}${parts.quality}`;
}