diff --git a/app.js b/app.js index dd8e02a..7d97353 100644 --- a/app.js +++ b/app.js @@ -409,10 +409,32 @@ function renderTable() { rows.forEach(({ tune, entry }) => { const hasNotes = (tune.notes?.length > 0) || (entry?.notes?.length > 0); + + // ── Mobile 2-line card data ────────────────────────────────── + const nameRaw = tune.name ?? ''; + const sourceRaw = tune.source?.name ?? ''; + const instRaw = entry?.instrument_name ?? ''; + const tuningRaw = entry?.tuning ?? ''; + const instStr = instRaw ? (tuningRaw ? `${instRaw} (${tuningRaw})` : instRaw) : ''; + const keyStr = tune.key + ? (tune.modal ? `${tune.key} modal` : tune.key) + : (tune.modal ? 'modal' : ''); + const metaRaw = [instStr, keyStr].filter(Boolean).join(' · '); + const mobPills = entry ? [ + `Call`, + `Rev`, + `Lrn`, + ].join('') : ''; + const mobActions = [ + hasNotes ? `` : '', + tune.references?.length > 0 ? `` : '', + ``, + ].join(''); + const tr = document.createElement('tr'); tr.innerHTML = ` - ${esc(tune.name ?? '')} - ${esc(tune.source?.name ?? '')} + ${esc(nameRaw)} + ${esc(sourceRaw)} ${esc(tune.key ?? '')} ${tune.modal ? 'Yes' : ''} ${esc(entry?.instrument_name ?? '')} @@ -430,7 +452,20 @@ function renderTable() { data-tune-id="${tune.id}" data-entry-id="${entry?.id ?? ''}" title="Edit" - ${unlocked ? '' : 'hidden'}>✏️`; + ${unlocked ? '' : 'hidden'}>✏️ + +
+
+ ${esc(nameRaw)} + ${sourceRaw ? `(${esc(sourceRaw)})` : ''} +
+ ${mobPills} +
+
+ ${esc(metaRaw)} + ${mobActions} +
+ `; tbody.appendChild(tr); }); @@ -1046,6 +1081,28 @@ function esc(str) { .replace(/"/g, '"'); } +// ── Mobile truncation tooltip ─────────────────────────────────── +{ + const tip = $('mob-tooltip'); + let timer = null; + + tbody.addEventListener('click', e => { + const el = e.target.closest('.mob-truncatable'); + if (!el) return; + if (el.scrollWidth <= el.offsetWidth) return; // not truncated + clearTimeout(timer); + tip.textContent = el.dataset.full; + const rect = el.getBoundingClientRect(); + tip.style.top = (rect.bottom + 4) + 'px'; + tip.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - 220)) + 'px'; + tip.hidden = false; + timer = setTimeout(() => { tip.hidden = true; }, 2500); + e.stopPropagation(); + }); + + document.addEventListener('click', () => { clearTimeout(timer); tip.hidden = true; }); +} + // Pressing Tab in a datalist-backed input fills the top matching suggestion. function attachTabAutocomplete(input, datalist) { input.addEventListener('keydown', e => { diff --git a/index.html b/index.html index ab1dba8..8613c52 100644 --- a/index.html +++ b/index.html @@ -182,6 +182,9 @@ + + + diff --git a/style.css b/style.css index 3b20c93..f263563 100644 --- a/style.css +++ b/style.css @@ -141,6 +141,27 @@ th[data-filter].filter-active::before { /* Short/full column header label swap (see responsive section) */ .col-short { display: none; } +/* Dim pill for inactive mobile status indicators */ +.pill-dim { background: rgba(255,255,255,.06); color: var(--muted); } + +/* Mobile card row — hidden on desktop, shown in responsive block */ +.mobile-row { display: none; } + +/* Tooltip for truncated mobile text */ +#mob-tooltip { + position: fixed; + z-index: 200; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: .35rem .65rem; + font-size: .8rem; + max-width: 80vw; + box-shadow: 0 2px 8px rgba(0,0,0,.4); + pointer-events: none; + color: var(--text); +} + /* ── Buttons ──────────────────────────────────────────────── */ button { cursor: pointer; @@ -193,6 +214,16 @@ th.sort-desc .sort-indicator::after { content: '▼'; } #tune-table tbody tr:nth-child(even) { background: rgba(255,255,255,.025); } #tune-table tbody tr:hover { background: rgba(255,255,255,.055); } +/* Truncating spans for Name + Source columns */ +.cell-trunc { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.cell-name-content { max-width: min(22vw, 260px); } +.cell-source-content { max-width: min(26vw, 200px); } + .pill { display: inline-block; padding: .15rem .45rem; @@ -349,20 +380,72 @@ fieldset.active .fs-body { display: contents; } /* ── Responsive ───────────────────────────────────────────── */ @media (max-width: 600px) { - /* Header: let actions wrap onto a second line */ + /* Header: wrap onto second line if needed */ header { flex-wrap: wrap; } #header-actions { flex-wrap: wrap; } - /* Table: scroll horizontally instead of hiding columns */ - #tune-table { width: max-content; min-width: 100%; } + /* Table: block layout so the card has a concrete width to truncate against */ + #tune-table, + #tune-table tbody, + #tune-table tr { display: block; width: 100%; } + #tune-table thead { display: none; } + #tune-table tr { border-bottom: 1px solid var(--border); } + #tune-table td:not(.mobile-row) { display: none; } + .mobile-row { display: block; padding: .45rem .6rem; } - /* Tighter cell padding on mobile */ - #tune-table th, - #tune-table td { padding: .4rem .35rem; } + /* Line 1: Name (Source) … [Call][Rev][Lrn] */ + .mob-line1 { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: .4rem; + } + .mob-name-wrap { + display: flex; + align-items: baseline; + gap: .25rem; + flex: 1; + min-width: 0; + } + .mob-name { + font-weight: 600; + font-size: .82rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1 1 0; + min-width: 1.5rem; + } + .mob-source { + font-size: .75rem; + color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 0 1 auto; + min-width: 1.5rem; + } + .mob-status { display: flex; gap: .2rem; align-items: center; flex-shrink: 0; } + .mob-pill { font-size: .62rem; padding: .1rem .3rem; } - /* Short column headers: swap to abbreviated label on mobile */ - .col-full { display: none; } - .col-short { display: inline; } + /* Line 2: Instrument (Tuning) · Key [actions] */ + .mob-line2 { + display: flex; + justify-content: space-between; + align-items: center; + gap: .4rem; + margin-top: .25rem; + } + .mob-meta { + font-size: .72rem; + color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; + } + .mob-actions { display: flex; gap: .2rem; flex-shrink: 0; } /* Modals: fullscreen */ dialog {