From 7f19e1188543e67884efd4dbb813c897349edf2a Mon Sep 17 00:00:00 2001 From: Ian Keane Date: Thu, 11 Jun 2026 14:31:38 -0400 Subject: [PATCH 01/10] Fix scrolling --- style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/style.css b/style.css index 249d662..38a12c6 100644 --- a/style.css +++ b/style.css @@ -407,6 +407,10 @@ fieldset.active .fs-body { display: contents; } /* ── Responsive ───────────────────────────────────────────── */ @media (max-width: 600px) { + /* Fixed viewport so header and footer stay put while main scrolls */ + body { height: 100dvh; overflow: hidden; } + main { overflow-y: auto; } + /* Header: h1 + actions on line 1, search full-width on line 2 */ header { flex-wrap: wrap; } header h1 { order: 1; flex: 1; } From 5812d3cc767fa6a4c4de1c1127d252dd56a7803d Mon Sep 17 00:00:00 2001 From: Ian Keane Date: Thu, 11 Jun 2026 14:51:57 -0400 Subject: [PATCH 02/10] Slim dict for faster loading --- app.js | 156 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 93 insertions(+), 63 deletions(-) diff --git a/app.js b/app.js index 91fa837..297daaf 100644 --- a/app.js +++ b/app.js @@ -416,7 +416,7 @@ function renderTable() { emptyMsg.hidden = rows.length > 0; rows.forEach(({ tune, entry }) => { - const hasNotes = (tune.notes?.length > 0) || (entry?.notes?.length > 0); + const hasNotes = tune.has_notes ?? false; // ── Mobile 2-line card data ────────────────────────────────── const nameRaw = tune.name ?? ''; @@ -431,7 +431,7 @@ function renderTable() { const mobPills = entry ? pillStatus(entry.status, 'mob-pill') : ''; const mobActions = [ hasNotes ? `` : '', - tune.references?.length > 0 ? `` : '', + (tune.reference_count ?? 0) > 0 ? `` : '', ``, ].join(''); @@ -446,7 +446,7 @@ function renderTable() { ${hasNotes ? `` : ''} - ${tune.references?.length > 0 + ${(tune.reference_count ?? 0) > 0 ? `` : ''} + + + +
diff --git a/style.css b/style.css index e96f8e8..6236ee7 100644 --- a/style.css +++ b/style.css @@ -255,6 +255,9 @@ th.sort-desc .sort-indicator::after { content: '▼'; } /* ── Modal ────────────────────────────────────────────────── */ dialog { + position: fixed; + inset: 0; + margin: auto; background: var(--surface); color: var(--text); border: 1px solid var(--border); @@ -356,7 +359,7 @@ fieldset.active .fs-body { display: contents; } border-bottom: 1px solid var(--border); } .note-row:last-child { border-bottom: none; } -.note-text { flex: 1; font-size: .875rem; } +.note-text { flex: 1; font-size: .875rem; white-space: pre-wrap; word-break: break-word; } .note-edit-controls { display: flex; gap: .25rem; } .btn-edit-row { background: transparent; border: none; font-size: 1rem; padding: .2rem .3rem; } @@ -392,6 +395,43 @@ fieldset.active .fs-body { display: contents; } #btn-save-tune { background: var(--accent); } #btn-cancel { background: transparent; border: 1px solid var(--border); } +#notes-modal { + max-width: 700px; + height: 70vh; + max-height: 85vh; +} + +#note-editor-modal { + max-width: 700px; + height: 90vh; + max-height: 95vh; +} +#note-editor-modal[open] { + display: flex; + flex-direction: column; +} + +#note-editor-ta { + flex: 1; + min-height: 0; + display: block; + width: 100%; + resize: vertical; + background: var(--bg); + border: 1px solid var(--border); + color: var(--text); + border-radius: var(--radius); + padding: .5rem .65rem; + font-size: .875rem; + font-family: var(--font); + line-height: 1.5; + margin-bottom: .5rem; +} +#note-editor-ta:focus { + outline: 2px solid var(--accent2); + outline-offset: -1px; +} + /* ── Detail modal ─────────────────────────────────────────── */ #detail-content h2 { margin-bottom: .75rem; } #detail-content table { width: 100%; border-collapse: collapse; font-size: .85rem; } @@ -480,17 +520,17 @@ fieldset.active .fs-body { display: contents; } } .mob-actions { display: flex; gap: .2rem; flex-shrink: 0; } - /* Modals: fullscreen */ + /* Modals: fullscreen — !important beats any ID-specific desktop size rules */ dialog { - position: fixed; - inset: 0; - width: 100%; - max-width: 100%; - height: 100%; - max-height: 100%; - border-radius: 0; - border: none; - margin: 0; + position: fixed !important; + inset: 0 !important; + width: 100% !important; + max-width: 100% !important; + height: 100% !important; + max-height: 100% !important; + border-radius: 0 !important; + border: none !important; + margin: 0 !important; } } From eaf89d1d891bad89f6a0f0b7c93da1537ab8980c Mon Sep 17 00:00:00 2001 From: Ian Keane Date: Thu, 11 Jun 2026 17:03:56 -0400 Subject: [PATCH 05/10] CSS tweaks --- app.js | 9 +++++---- index.html | 3 ++- style.css | 24 ++++++++++++++++-------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app.js b/app.js index f981420..e5602e3 100644 --- a/app.js +++ b/app.js @@ -442,8 +442,8 @@ function renderTable() { ${esc(nameRaw)} ${esc(sourceRaw)} ${esc(keyStr)} - ${esc(entry?.instrument_name ?? '')} - ${esc(entry?.tuning ?? '')} + ${esc(entry?.instrument_name ?? '')} + ${esc(entry?.tuning ?? '')} ${pillStatus(entry?.status)} ${hasNotes ? `` @@ -1092,8 +1092,9 @@ function callSelect(value, modal) { } } -$('btn-call').addEventListener('click', () => openCallModal('callable')); -$('btn-learn').addEventListener('click', () => openCallModal('to_learn')); +$('btn-call').addEventListener('click', () => openCallModal('callable')); +$('btn-review').addEventListener('click', () => openCallModal('review')); +$('btn-learn').addEventListener('click', () => openCallModal('to_learn')); $('call-close').addEventListener('click', () => callModal.close()); $('call-back').addEventListener('click', () => { callStep--; diff --git a/index.html b/index.html index b2aab72..f38b339 100644 --- a/index.html +++ b/index.html @@ -14,8 +14,9 @@
+ - +
diff --git a/style.css b/style.css index 6236ee7..7cade91 100644 --- a/style.css +++ b/style.css @@ -29,6 +29,7 @@ header { padding: .75rem 1.25rem; display: flex; align-items: center; + flex-wrap: wrap; gap: .75rem; } @@ -39,8 +40,11 @@ header h1 { font-size: 1.4rem; white-space: nowrap; } gap: .6rem; align-items: center; flex-shrink: 0; + margin-left: auto; /* right-aligns on same line; left-fills when alone on a row */ } + + #filter-search { flex: 1; background: var(--bg); @@ -228,8 +232,10 @@ th.sort-desc .sort-indicator::after { content: '▼'; } text-overflow: ellipsis; white-space: nowrap; } -.cell-name-content { max-width: min(22vw, 260px); } -.cell-source-content { max-width: min(26vw, 200px); } +.cell-name-content { max-width: min(14vw, 200px); } +.cell-source-content { max-width: min(11vw, 160px); } +.cell-inst-content { max-width: min(11vw, 130px); } +.cell-tuning-content { max-width: min(9vw, 110px); } .pill { display: inline-block; @@ -451,11 +457,12 @@ fieldset.active .fs-body { display: contents; } body { height: 100dvh; overflow: hidden; } main { overflow-y: auto; } - /* Header: h1 + actions on line 1, search full-width on line 2 */ - header { flex-wrap: wrap; } - header h1 { order: 1; flex: 1; } - #header-actions { order: 2; flex-shrink: 0; } - #filter-search { order: 3; flex-basis: 100%; } + /* Search drops to its own full-width row */ + #filter-search { order: 3; flex-basis: 100%; } + /* Actions fill remaining space; each button gets an equal share; ☰ stays natural size */ + #header-actions { flex: 1; margin-left: 0; } + #header-actions > button { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; } + #btn-bulk-edit { flex: none; } /* Table: block layout so the card has a concrete width to truncate against */ #tune-table, @@ -563,7 +570,8 @@ fieldset.active .fs-body { display: contents; } #btn-bulk-edit { background: transparent; border: 1px solid var(--border); color: var(--text); font-size: 1rem; padding: .3rem .65rem; } #btn-call { background: #1b4332; color: #74c69d; border: 1px solid #2d6a4f; font-weight: 600; } -#btn-learn { background: #3d2e10; color: #f2c94c; border: 1px solid #4d3a1e; font-weight: 600; } +#btn-review { background: #3d2e10; color: #f2c94c; border: 1px solid #4d3a1e; font-weight: 600; } +#btn-learn { background: #2a1a4e; color: #c4b5fd; border: 1px solid #3d2562; font-weight: 600; } /* ── Call modal ───────────────────────────────────────────── */ #call-modal { From 52eb0f61898c503b1cdf52236d6b91dffdb33d85 Mon Sep 17 00:00:00 2001 From: Ian Keane Date: Thu, 11 Jun 2026 17:12:05 -0400 Subject: [PATCH 06/10] Require key, instrument, tuning --- app.js | 35 ++++++++++++++++++++++++++++++++++- index.html | 1 + style.css | 4 ++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index e5602e3..df4dc48 100644 --- a/app.js +++ b/app.js @@ -504,6 +504,7 @@ function openAdd() { editingTuneId = null; editingEntryId = null; editingOriginalRefIds = []; + setTuneError(''); $('modal-title').textContent = 'Add tune'; tuneForm.reset(); refsList.innerHTML = ''; @@ -659,11 +660,43 @@ function addRefRow(link = '', site = '', refId = null, siteId = null, initialMus // ── Save ────────────────────────────────────────────────────── $('btn-save-tune').addEventListener('click', saveTune); -$('btn-cancel').addEventListener('click', () => tuneModal.close()); +$('btn-cancel').addEventListener('click', () => { setTuneError(''); tuneModal.close(); }); + +function setTuneError(msg) { + const el = $('tune-error'); + el.textContent = msg; + el.hidden = !msg; +} async function saveTune() { const f = tuneForm.elements; + // Validate required fields + const keyVal = f.key.value.trim(); + const tuningVal = f.inst_tuning.value.trim(); + + if (!keyVal) { + setTuneError('Key is required.'); + f.key.focus(); + return; + } + if (!instrumentSelect.value) { + setTuneError('Instrument is required.'); + instrumentSelect.focus(); + return; + } + if (!tuningVal) { + setTuneError('Tuning is required.'); + f.inst_tuning.focus(); + return; + } + if (!resolveTuningId(tuningVal)) { + setTuneError(`Tuning "${tuningVal}" not found — use + to add it first.`); + f.inst_tuning.focus(); + return; + } + setTuneError(''); + const tuneBody = { name: f.name.value || null, key: f.key.value || null, diff --git a/index.html b/index.html index f38b339..064659d 100644 --- a/index.html +++ b/index.html @@ -154,6 +154,7 @@
+
+ + +

Upload References from CSV

+
+ Expected format +

One reference per row. Tune matched by name — must already exist.

+ + + + + + + + + + + +
ColumnRequiredNotes
tune_nameYesMust match an existing tune (case-insensitive)
linkURL
siteSite name — created if new
musician1Musician name — created if new
musician2
musician3
musician4
+

Rows where the tune name is not found are skipped with a warning in the results.

+
+
+ +
+ + +
+ From 9dc16c6c637c596e0c3083d6bcf65597d916ce2b Mon Sep 17 00:00:00 2001 From: Ian Keane Date: Fri, 7 Aug 2026 17:25:32 -0400 Subject: [PATCH 10/10] Last cleanup commit --- app.js | 26 +++++++++++++++++++++----- index.html | 1 + 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app.js b/app.js index 3c4d93d..b2c54ca 100644 --- a/app.js +++ b/app.js @@ -1589,7 +1589,7 @@ $('btn-refs-csv-import').addEventListener('click', async () => { await loadAll(); let html = `

✓ ${results.created} reference${results.created !== 1 ? 's' : ''} imported.

`; if (results.warnings.length) { - html += `

${results.warnings.length} skipped — tune not found:

    `; + html += `

    ${results.warnings.length} skipped:

      `; results.warnings.forEach(w => { html += `
    • ${esc(w)}
    • `; }); html += '
    '; } @@ -1633,13 +1633,29 @@ async function importRefsCsvRows(rows) { } for (const row of rows) { - const tuneName = row.tune_name?.trim(); + const tuneName = row.tune_name?.trim(); + const sourceName = row.source?.trim(); if (!tuneName) continue; - const tune = allTunes.find(t => (t.name ?? '').toLowerCase() === tuneName.toLowerCase()); - if (!tune) { - results.warnings.push(tuneName); + const matches = allTunes.filter(t => (t.name ?? '').toLowerCase() === tuneName.toLowerCase()); + let tune; + if (matches.length === 0) { + results.warnings.push(`${tuneName} — not found`); continue; + } else if (matches.length === 1) { + tune = matches[0]; + } else { + // Multiple tunes with this name — need source to disambiguate + if (sourceName) { + tune = matches.find(t => (t.source?.name ?? '').toLowerCase() === sourceName.toLowerCase()); + if (!tune) { + results.warnings.push(`${tuneName} (source: ${sourceName}) — no matching source found`); + continue; + } + } else { + results.warnings.push(`${tuneName} — ambiguous (${matches.length} tunes with this name; add a source column to disambiguate)`); + continue; + } } try { diff --git a/index.html b/index.html index 8a0bbc3..3e87558 100644 --- a/index.html +++ b/index.html @@ -266,6 +266,7 @@ ColumnRequiredNotes tune_nameYesMust match an existing tune (case-insensitive) + sourceDisambiguates when multiple tunes share a name linkURL siteSite name — created if new musician1Musician name — created if new