From 472e7bec7e4046b3cf49c92cf1a0f568c7ceac1a Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:49:56 -0400 Subject: [PATCH] Family board: fit a 12.9-inch iPad without scrolling; See all for the chore pool - Fixed-height grid for tablets (3x2 landscape, 2x3 portrait), larger text on big screens - Compact chores card with tappable pills and +N overflow - "See all" opens the full list of up-for-grabs chores to pick from Co-Authored-By: Claude Sonnet 5.5 --- public/board.html | 15 +++++++++++++++ public/board.js | 16 ++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/public/board.html b/public/board.html index ab4b2a7..0920d33 100644 --- a/public/board.html +++ b/public/board.html @@ -65,6 +65,21 @@ #ov { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: grid; place-items: center; padding: 18px; } #ov[hidden] { display: none; } #ovb { background: var(--panel); border: 1px solid var(--line); border-radius: 20px; padding: 20px; width: 100%; max-width: 380px; text-align: center; } .err { color: var(--danger); min-height: 20px; } + .pill { display: inline-flex; align-items: center; gap: 6px; margin: 3px 6px 3px 0; padding: 6px 12px; border-radius: 16px; background: var(--panel2); border: 1px solid var(--line); color: var(--text); font: inherit; font-size: 15px; cursor: pointer; } .pill.pool { border-color: var(--accent); } .pill small { color: var(--muted); font-size: 12px; } .pill.more { background: none; border-style: dashed; color: var(--muted); cursor: default; } + .chp { margin-top: 8px; } .chp .top { display: flex; justify-content: space-between; align-items: baseline; } + /* a tablet (like a 12.9-inch iPad) shows the whole board at once, with no scrolling */ + @media (min-width: 700px) and (min-height: 700px) { + html, body { height: 100%; overflow: hidden; } body { padding: 14px 18px; } + .bd { height: 100%; display: flex; flex-direction: column; max-width: none; } + .bdtop h1 { font-size: 28px; } .bdclock { font-size: 32px; } .bdtop p { margin: 0; } + .ann { margin: 8px 0; padding: 8px 14px; font-size: 20px; } .who { margin: 8px 0; } .who h2 { margin-bottom: 6px; font-size: 15px; } + .tiles { flex-wrap: nowrap; overflow: hidden; gap: 10px; } .tile { width: 92px; padding: 8px 4px; border-radius: 16px; flex: none; } .tile .av { width: 44px; height: 44px; font-size: 20px; } .tile b { font-size: 14px; } + .grid { flex: 1; min-height: 0; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(3, minmax(0, 1fr)); gap: 10px; } + .card { overflow: hidden; padding: 10px 14px; } .card h2 { margin-bottom: 4px; font-size: 13px; } .ln { padding: 4px 0; font-size: 16px; } + .bd > p.sub { display: none; } + } + @media (min-width: 900px) and (min-height: 1100px) { .ln { font-size: 20px; padding: 6px 0; } .pill { font-size: 18px; padding: 8px 14px; } .card h2 { font-size: 15px; } .tile { width: 108px; } .tile .av { width: 54px; height: 54px; font-size: 24px; } .tile b { font-size: 16px; } .bdtop h1 { font-size: 34px; } .bdclock { font-size: 40px; } .card small, .chp small { font-size: 14px; } } + @media (min-width: 1200px) and (min-height: 700px) { .grid { grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2, minmax(0, 1fr)); } } .chrow { width: 100%; background: none; border: 0; border-bottom: 1px solid var(--line); color: var(--text); font: inherit; font-size: 17px; text-align: left; cursor: pointer; } .chrow .chk { width: 24px; height: 24px; border: 2px solid var(--line); border-radius: 7px; display: grid; place-items: center; flex: none; font-size: 15px; } .chrow.dn .chk { background: var(--accent); border-color: var(--accent); color: #fff; } .chrow.dn .g { text-decoration: line-through; opacity: .6; } diff --git a/public/board.js b/public/board.js index d5eba68..dfb3725 100644 --- a/public/board.js +++ b/public/board.js @@ -19,17 +19,21 @@ function draw() { $('#tiles').innerHTML = d.people.map(p => ``).join(''); $('#tiles').querySelectorAll('.tile').forEach(b => b.onclick = () => pick(d.people.find(p => p.id === b.dataset.id))); const day = (label, ds) => { const l = d.events.filter(e => e.allDay ? e.start <= ds && e.end > ds : ymd(new Date(e.start)) === ds).sort((a, b) => (b.allDay - a.allDay) || a.start - b.start); return `${label}` + (l.map(e => `
${esc(e.title)}${e.allDay ? 'All day' : new Date(e.start).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
`).join('') || none('Nothing')); }; - const row = c => ``; + const pill = (c, cls) => ``; const pool = d.chores.filter(c => c.claimable && !c.done), per = {}; d.chores.filter(c => !(c.claimable && !c.done)).forEach(c => { (per[c.userName] = per[c.userName] || []).push(c); }); - const chores = (pool.length ? `Up for grabs: tap to take one${pool.map(row).join('')}` : '') + Object.keys(per).map(k => { const l = per[k], dn = l.filter(c => c.done).length; return `
${esc(k)}${dn} of ${l.length}
${l.map(row).join('')}
`; }).join(''); + const SHOW = 4, chores = (pool.length ? `Up for grabs: tap one
${pool.slice(0, 6).map(c => pill(c, 'pool')).join('')}${pool.length > 6 ? `` : ''}
` : '') + Object.keys(per).map(k => { const l = per[k], dn = l.filter(c => c.done).length, left = l.filter(c => !c.done); return `
${esc(k)}${dn} of ${l.length}
${left.slice(0, SHOW).map(c => pill(c)).join('')}${left.length > SHOW ? `+${left.length - SHOW}` : ''}${left.length ? '' : 'All done 🎉'}
`; }).join(''); $('#grid').innerHTML = card('Today and tomorrow', day('Today', t0) + day('Tomorrow', t1)) + card('Chores today', chores || none('No chores today')) - + card('This week', weekCard(d.week)) + card('Shopping list', d.shopping.map(i => `
${esc(i.text)}
`).join('') || none('Nothing to buy')) - + card('Family tasks', d.tasks.map(t => `
${esc(t.title)}${t.due === t0 ? 'Today' : esc(t.due)}
`).join('') || none('Nothing due')) - + card('Family notes and lists', d.notes.map(n => `
${esc(n.title)}${n.type === 'list' ? ` ${n.done} of ${n.total} done${n.next ? ' · next: ' + esc(n.next) : ''}` : ` ${esc(n.body)}`}
`).join('') || none('No shared notes')); + + card('This week', weekCard(d.week)) + card('Shopping list', d.shopping.slice(0, 10).map(i => `
${esc(i.text)}
`).join('') || none('Nothing to buy')) + + card('Family tasks', d.tasks.slice(0, 6).map(t => `
${esc(t.title)}${t.due === t0 ? 'Today' : esc(t.due)}
`).join('') || none('Nothing due')) + + card('Family notes and lists', d.notes.slice(0, 4).map(n => `
${esc(n.title)}${n.type === 'list' ? ` ${n.done} of ${n.total} done${n.next ? ' · next: ' + esc(n.next) : ''}` : ` ${esc(n.body)}`}
`).join('') || none('No shared notes')); wireChores(); } -function wireChores() { document.querySelectorAll('.chrow').forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c))); } +function poolAll() { + const list = data.chores.filter(c => c.claimable && !c.done), { box, close } = sheet(`

Up for grabs

${list.length} chores. Tap one to pick it

${list.map(c => ``).join('')}

`); + box.querySelector('#no').onclick = close; box.querySelectorAll('.pill[data-c]').forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c))); +} +function wireChores() { const all = document.getElementById('poolall'); if (all) all.onclick = poolAll; document.querySelectorAll(".pill[data-c]").forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c))); } async function load() { try { const r = await fetch('/api/board?day=' + ymd(new Date())); if (r.status === 403) { location.replace('/login'); return; } data = await r.json(); draw(); } catch {} } function pick(p) {