2026-06-09 15:04:27 -04:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" / >
< title > Repertory< / title >
< link rel = "stylesheet" href = "style.css" / >
< / head >
< body >
< header >
2026-06-11 10:06:20 -04:00
< h1 > 🪕 Repertory< / h1 >
2026-06-11 13:39:26 -04:00
< input type = "search" id = "filter-search" placeholder = "Search…" / >
2026-06-09 15:04:27 -04:00
< div id = "header-actions" >
< button id = "btn-clear-filters" hidden > ✕ Clear filters< / button >
2026-06-11 12:30:11 -04:00
< button id = "btn-call" > Call< / button >
2026-06-11 17:03:56 -04:00
< button id = "btn-review" > Review< / button >
2026-06-11 12:35:38 -04:00
< button id = "btn-learn" > Learn< / button >
2026-06-11 17:03:56 -04:00
< button id = "btn-add-tune" hidden > + Add< / button >
2026-06-09 15:04:27 -04:00
< button id = "btn-unlock" > 🔒 Unlock< / button >
2026-06-11 14:12:46 -04:00
< button id = "btn-bulk-edit" hidden > ☰< / button >
2026-06-09 15:04:27 -04:00
< / div >
< / header >
<!-- ── Tune table ───────────────────────────────────────────── -->
< main >
< table id = "tune-table" >
< thead >
< tr >
< th data-col = "name" > Name < span class = "sort-indicator" > < / span > < / th >
< th data-filter = "source" > Source< / th >
2026-06-11 10:06:20 -04:00
< th data-filter = "key" > Key< / th >
2026-06-09 15:04:27 -04:00
< th data-filter = "instrument" > Instrument< / th >
< th data-filter = "tuning" > Tuning< / th >
2026-06-11 12:54:08 -04:00
< th data-filter = "status" > Status< / th >
2026-06-09 15:04:27 -04:00
< th > Notes< / th >
2026-06-11 10:06:20 -04:00
< th > Refs< / th >
2026-06-09 15:04:27 -04:00
< th class = "edit-col" hidden > < / th >
< / tr >
< / thead >
< tbody id = "tune-tbody" > < / tbody >
< / table >
< p id = "empty-msg" hidden > No tunes match the current filters.< / p >
< / main >
<!-- ── Filter popover (shared, moved in DOM) ────────────────── -->
< div id = "filter-popover" hidden >
< div id = "filter-popover-inner" > < / div >
< / div >
2026-06-11 10:06:20 -04:00
<!-- ── Bulk edit popover ─────────────────────────────────────── -->
< div id = "bulk-edit-popover" hidden >
< button id = "btn-open-bulk-refs" > 📎 Bulk Edit References< / button >
2026-06-12 10:15:43 -04:00
< button id = "btn-upload-csv" > 📂 Upload Tunes from CSV< / button >
< button id = "btn-upload-refs-csv" > 🔗 Upload References from CSV< / button >
2026-06-11 10:06:20 -04:00
< / div >
<!-- ── Bulk edit references modal ───────────────────────────── -->
< dialog id = "bulk-refs-modal" >
< h2 > Bulk Edit References< / h2 >
< div id = "bulk-refs-controls" >
< input type = "search" id = "bulk-refs-search" placeholder = "Filter by name…" / >
< select id = "bulk-refs-instrument" >
< option value = "" > All instruments< / option >
< / select >
< select id = "bulk-refs-source" >
< option value = "" > All sources< / option >
< / select >
< select id = "bulk-refs-has-refs" >
< option value = "" > All tunes< / option >
< option value = "no" > No refs yet< / option >
< option value = "yes" > Has refs< / option >
< / select >
< select id = "bulk-refs-sort" >
< option value = "newest" > Newest first< / option >
< option value = "oldest" > Oldest first< / option >
< option value = "name_asc" > Name A→Z< / option >
< option value = "name_desc" > Name Z→A< / option >
< / select >
< / div >
< div id = "bulk-refs-list" > < / div >
< div class = "modal-actions" >
< button id = "btn-bulk-refs-close" > Close< / button >
< / div >
< / dialog >
2026-06-09 15:04:27 -04:00
<!-- ── Add / Edit tune modal ────────────────────────────────── -->
< dialog id = "tune-modal" >
< form id = "tune-form" method = "dialog" >
< h2 id = "modal-title" > Add tune< / h2 >
< fieldset >
< legend > Tune< / legend >
< label > Name < input name = "name" type = "text" / > < / label >
< label > Key < input name = "key" type = "text" placeholder = "e.g. D" / > < / label >
< label > Modal < input name = "modal" type = "checkbox" / > < / label >
< label > Source
< span class = "autocomplete-row" >
< input name = "source_name" id = "source-input" type = "text"
list="source-list" placeholder="— none —" autocomplete="off" />
< datalist id = "source-list" > < / datalist >
< button type = "button" class = "quick-add-btn" id = "btn-quick-add-source" title = "Add new source" > +< / button >
< / span >
< / label >
< / fieldset >
< fieldset id = "fs-instrument" >
< legend > Instrument entry< / legend >
< label > Instrument
2026-06-11 14:12:46 -04:00
< span class = "autocomplete-row" >
< select name = "instrument_id" id = "instrument-select" >
< option value = "" > — none —< / option >
< / select >
< button type = "button" class = "quick-add-btn" id = "btn-quick-add-instrument" title = "Add new instrument" > +< / button >
< / span >
2026-06-09 15:04:27 -04:00
< / label >
< label > Tuning
< span class = "autocomplete-row" >
< input name = "inst_tuning" type = "text"
list="tuning-list" placeholder="— none —" autocomplete="off" />
< datalist id = "tuning-list" > < / datalist >
< button type = "button" class = "quick-add-btn" id = "btn-quick-add-tuning" title = "Add new tuning" > +< / button >
< / span >
< / label >
2026-06-11 17:28:14 -04:00
< label > Term
< span class = "autocomplete-row" >
< input name = "inst_term" type = "text"
list="term-list" placeholder="— none —" autocomplete="off" />
< datalist id = "term-list" > < / datalist >
< button type = "button" class = "quick-add-btn" id = "btn-quick-add-term" title = "Add new term" > +< / button >
< / span >
< / label >
2026-06-09 15:04:27 -04:00
< label > Learned from
< span class = "autocomplete-row" >
< input name = "inst_learned_from" type = "text"
list="musician-list" placeholder="— none —" autocomplete="off" />
< datalist id = "musician-list" > < / datalist >
< button type = "button" class = "quick-add-btn" id = "btn-quick-add-musician" title = "Add new musician" > +< / button >
< / span >
< / label >
< label > Date learned < input name = "inst_date_learned" type = "date" / > < / label >
2026-06-11 12:54:08 -04:00
< label > Status
< span class = "status-radios" >
< label class = "radio-row" > < input type = "radio" name = "inst_status" value = "" / > None< / label >
< label class = "radio-row" > < input type = "radio" name = "inst_status" value = "callable" / > Callable< / label >
< label class = "radio-row" > < input type = "radio" name = "inst_status" value = "review" / > Needs Review< / label >
< label class = "radio-row" > < input type = "radio" name = "inst_status" value = "to_learn" / > To Learn< / label >
< / span >
< / label >
2026-06-09 15:04:27 -04:00
< label > Difficulty
< select name = "inst_difficulty" >
< option value = "" > —< / option >
< option value = "easy" > Easy< / option >
< option value = "hard" > Hard< / option >
< / select >
< / label >
< / fieldset >
< div id = "refs-section" >
< h3 > References< / h3 >
< div id = "refs-list" > < / div >
2026-06-11 10:06:20 -04:00
< datalist id = "ref-site-list" > < / datalist >
2026-06-09 15:04:27 -04:00
< button type = "button" id = "btn-add-ref" > + reference< / button >
< / div >
2026-06-11 17:12:05 -04:00
< p id = "tune-error" class = "modal-error" hidden > < / p >
2026-06-09 15:04:27 -04:00
< div class = "modal-actions" >
< button type = "button" id = "btn-delete-tune" class = "btn-danger" hidden > Delete tune< / button >
< button type = "button" id = "btn-edit-notes" hidden > 📝 Notes< / button >
< button type = "button" id = "btn-save-tune" > Save< / button >
< button type = "button" id = "btn-cancel" > Cancel< / button >
< / div >
< / form >
< / dialog >
2026-06-11 10:06:20 -04:00
<!-- ── References display modal ─────────────────────────────── -->
< dialog id = "refs-modal" >
< h2 id = "refs-modal-title" > References< / h2 >
< div id = "refs-modal-list" > < / div >
< div class = "modal-actions" >
< button id = "btn-refs-modal-close" > Close< / button >
< / div >
< / dialog >
2026-06-11 16:29:30 -04:00
<!-- ── Notes modal ───────────────────────────────── -->
2026-06-09 15:04:27 -04:00
< dialog id = "notes-modal" >
< h2 id = "notes-modal-title" > Notes< / h2 >
< div id = "tune-notes-section" >
< h3 > Tune notes< / h3 >
< div id = "tune-notes-list" > < / div >
< button type = "button" id = "btn-add-tune-note" hidden > + note< / button >
< / div >
< div id = "inst-notes-section" >
< h3 id = "inst-notes-heading" > Instrument notes< / h3 >
< div id = "inst-notes-list" > < / div >
< button type = "button" id = "btn-add-inst-note" hidden > + note< / button >
< / div >
< div class = "modal-actions" >
< button id = "btn-notes-close" > Close< / button >
< / div >
< / dialog >
2026-06-11 16:29:30 -04:00
<!-- ── Note editor modal (fullscreen, separate from notes list) ─ -->
< dialog id = "note-editor-modal" >
< h2 id = "note-editor-title" > Add note< / h2 >
< textarea id = "note-editor-ta" placeholder = "Write your note… (Ctrl+↵ to save)" > < / textarea >
< div class = "modal-actions" >
< button id = "btn-note-save" > Save note< / button >
< button id = "btn-note-cancel" > Cancel< / button >
< / div >
< / dialog >
2026-06-11 12:30:11 -04:00
<!-- ── Call modal ─────────────────────────────────────────── -->
< dialog id = "call-modal" >
< div id = "call-header" >
< button id = "call-back" hidden > ← Back< / button >
< h2 id = "call-title" > < / h2 >
< button id = "call-close" > ✕< / button >
< / div >
< div id = "call-options" > < / div >
< / dialog >
2026-06-11 14:12:46 -04:00
<!-- ── CSV upload modal ─────────────────────────────────────── -->
< dialog id = "upload-csv-modal" >
< h2 > Upload from CSV< / h2 >
< details id = "csv-format-details" open >
< summary > Expected format< / summary >
< p style = "font-size:.85rem;margin:.5rem 0 .4rem" > One tune per row. Headers (case-insensitive):< / p >
< table class = "csv-format-table" >
< thead > < tr > < th > Column< / th > < th > Required< / th > < th > Notes< / th > < / tr > < / thead >
< tbody >
< tr > < td > name< / td > < td > Yes< / td > < td > Tune name< / td > < / tr >
< tr > < td > key< / td > < td > < / td > < td > e.g. D, G, Am< / td > < / tr >
< tr > < td > modal< / td > < td > < / td > < td > true / false< / td > < / tr >
< tr > < td > source< / td > < td > < / td > < td > Created if new< / td > < / tr >
< tr > < td > instrument< / td > < td > < / td > < td > Created if new< / td > < / tr >
< tr > < td > tuning< / td > < td > < / td > < td > Created if new< / td > < / tr >
< tr > < td > learned_from< / td > < td > < / td > < td > Musician name — created if new< / td > < / tr >
< tr > < td > date_learned< / td > < td > < / td > < td > YYYY-MM-DD< / td > < / tr >
< tr > < td > status< / td > < td > < / td > < td > callable / review / to_learn< / td > < / tr >
< tr > < td > difficulty< / td > < td > < / td > < td > easy / hard< / td > < / tr >
2026-06-11 17:28:14 -04:00
< tr > < td > term< / td > < td > < / td > < td > Created if new< / td > < / tr >
2026-06-12 09:57:24 -04:00
< tr > < td > tune_note< / td > < td > < / td > < td > One note on the tune< / td > < / tr >
< tr > < td > instrument_note< / td > < td > < / td > < td > One note on the instrument entry< / td > < / tr >
2026-06-11 14:12:46 -04:00
< / tbody >
< / table >
< p class = "csv-format-note" > If a tune with the same name already exists a new instrument entry is added to it. Sources, tunings, and musicians are created automatically if they don’ t exist.< / p >
< / details >
< div id = "csv-upload-area" >
< input type = "file" id = "csv-file-input" accept = ".csv,.tsv,text/csv" / >
< / div >
< div id = "csv-results" hidden > < / div >
< div class = "modal-actions" >
< button id = "btn-csv-import" disabled > Import< / button >
< button id = "btn-csv-cancel" > Cancel< / button >
< / div >
< / dialog >
2026-06-12 10:15:43 -04:00
<!-- ── References CSV upload modal ───────────────────────────── -->
< dialog id = "upload-refs-csv-modal" >
< h2 > Upload References from CSV< / h2 >
< details id = "refs-csv-format-details" open >
< summary > Expected format< / summary >
< p style = "font-size:.85rem;margin:.5rem 0 .4rem" > One reference per row. Tune matched by name — must already exist.< / p >
< table class = "csv-format-table" >
< thead > < tr > < th > Column< / th > < th > Required< / th > < th > Notes< / th > < / tr > < / thead >
< tbody >
< tr > < td > tune_name< / td > < td > Yes< / td > < td > Must match an existing tune (case-insensitive)< / td > < / tr >
< tr > < td > link< / td > < td > < / td > < td > URL< / td > < / tr >
< tr > < td > site< / td > < td > < / td > < td > Site name — created if new< / td > < / tr >
< tr > < td > musician1< / td > < td > < / td > < td > Musician name — created if new< / td > < / tr >
< tr > < td > musician2< / td > < td > < / td > < td > < / td > < / tr >
< tr > < td > musician3< / td > < td > < / td > < td > < / td > < / tr >
< tr > < td > musician4< / td > < td > < / td > < td > < / td > < / tr >
< / tbody >
< / table >
< p class = "csv-format-note" > Rows where the tune name is not found are skipped with a warning in the results.< / p >
< / details >
< div id = "refs-csv-upload-area" >
< input type = "file" id = "refs-csv-file-input" accept = ".csv,.tsv,text/csv" / >
< / div >
< div id = "refs-csv-results" hidden > < / div >
< div class = "modal-actions" >
< button id = "btn-refs-csv-import" disabled > Import< / button >
< button id = "btn-refs-csv-cancel" > Cancel< / button >
< / div >
< / dialog >
2026-06-11 11:36:08 -04:00
<!-- Tooltip for truncated mobile text -->
< div id = "mob-tooltip" hidden > < / div >
2026-06-11 13:39:26 -04:00
< footer id = "app-footer" > < span id = "status" > < / span > < / footer >
2026-06-09 15:04:27 -04:00
<!-- config.js is gitignored — copy config.example.js to config.js for local dev -->
< script src = "config.js" onerror = "void 0" > < / script >
< script src = "app.js" > < / script >
< / body >
< / html >