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-29 14:57:43 -04:00
co-authored by Claude Opus 5
parent 4e5b60e82b
commit 06066529bc
4 changed files with 49 additions and 23 deletions
+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}`;
}