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 {