Keys read as notes on the Plan tile's badge too

The item badge still showed Planning Center's raw "Bb" and "F#m" while the
Song Key tile beside it showed B♭ and F♯m. One module should not spell the
same thing two ways.

The notation moves into key-notation.js and both tiles import it, rather
than each keeping its own copy of the parse: the day one of them changes,
they all should.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-27 11:27:31 -04:00
co-authored by Claude Opus 5
parent c9d7c02bd6
commit 93e9f714aa
4 changed files with 49 additions and 23 deletions
+8 -4
View File
@@ -357,10 +357,14 @@ below. Nothing is installed without someone clicking.
explain. explain.
**Song Key** is one value, as large as the tile allows: the key of the item **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 ProPresenter has live, with the song's name and the next key under it — both
next key under it — both switches, as is the colour. When the feed that switches, as is the colour. When the feed that picks the item is gone the key
picks the item is gone the key stays up, faded, and the tile's dot says so stays up, faded, and the tile's dot says so rather than pretending to be
rather than pretending to be current. 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 **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 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 createTeams from "./teams-client.js";
import createKey from "./key-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>'; 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) { if (on('showKey') && item.keyName) {
const key = document.createElement('span'); const key = document.createElement('span');
key.className = 'pp-key'; 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); titleLine.appendChild(key);
} }
if (on('showNowIndicator')) { 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; 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. */ which item is current comes from the ProPresenter module, as ever. */
import { parseKey, prettyKey } from "./key-notation.js";
export default function createKey({ root, moduleApi }) { export default function createKey({ root, moduleApi }) {
let state = null; let state = null;
let stream = 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 (!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'); 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'}`); moduleApi.setStatus('ok', `${titleOf(item)}${item.keyName ? ` — ${prettyKey(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 || '');
} }
/** /**
@@ -84,23 +80,22 @@ export default function createKey({ root, moduleApi }) {
*/ */
function drawKey(text) { function drawKey(text) {
keyEl.innerHTML = ''; keyEl.innerHTML = '';
const match = /^([A-G])([#b♯♭]?)(.*)$/.exec(String(text).trim()); const parts = parseKey(text);
if (!match) { if (!parts) {
keyEl.textContent = text; keyEl.textContent = text;
return; return;
} }
const [, note, accidental, rest] = match; keyEl.appendChild(document.createTextNode(parts.note));
keyEl.appendChild(document.createTextNode(note)); if (parts.accidental) {
if (accidental) { const acc = document.createElement('span');
const sup = document.createElement('span'); acc.className = 'pk-accidental';
sup.className = 'pk-accidental'; acc.textContent = parts.accidental;
sup.textContent = accidental === '#' ? '♯' : accidental === 'b' ? '♭' : accidental; keyEl.appendChild(acc);
keyEl.appendChild(sup);
} }
if (rest) { if (parts.quality) {
const tail = document.createElement('span'); const tail = document.createElement('span');
tail.className = 'pk-quality'; tail.className = 'pk-quality';
tail.textContent = rest; tail.textContent = parts.quality;
keyEl.appendChild(tail); 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}`;
}