From 5baec5fb80bf56f690d837e66ac0c4d06322cd34 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Mon, 5 Oct 2026 15:30:12 +0700 Subject: [PATCH] Deck calculator: fix the mobile layout (no horizontal scrolling) The sheet tables carry 5-8 columns and were unusable on a phone: values were truncated ("900" -> "90") and columns fell off the edge, and the cost-vs-quantity table scrolled sideways. - On max-width:639px the four data tables render as stacked cards (.ss-sheet-table in index.css): thead hidden, each td a label/value row (label via data-label + td::before), cells flex-wrap so a long label plus input never overflows. - The cost-vs-quantity block is now a vertical list instead of a table, so it never scrolls horizontally. Verified at 390px: values are full and each row is a readable card. --- AGENTS.md | 11 +++- src/components/DeckCostCalculator.tsx | 80 ++++++++++++++------------- src/index.css | 41 ++++++++++++++ 3 files changed, 93 insertions(+), 39 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3a2d80e..85b3c6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -860,7 +860,16 @@ session with **no other memory of this project**. So: (Copy) action beside delete; the sticky summary chip shows the **order total** when `quantity > 1` (and uses the setup-inclusive cost); and the marketplace hand-off passes a **buyer-inclusive** unit price (tax added on top when tax - isn't marked inclusive). **Currency + isn't marked inclusive). + **Mobile layout (2026-10-05, part 3):** the sheet tables (5–8 columns) were + unusable on a phone — values truncated (`900`→`90`) and columns fell off the + edge. On `max-width:639px` the 4 data tables become **stacked cards** via + `src/index.css` `.ss-sheet-table` (`thead` hidden; each `td` a label/value row; + `data-label` supplies the label through `td::before`; cells `flex-wrap` so a + long label + input never overflows). The **Cost-vs-quantity** block is now a + vertical list, not a table, so nothing scrolls sideways. Verify with a + `--window-size=390,…` headless screenshot; add `defaultOpen` to a `CalcSection` + temporarily to see a table expanded. **Currency (2026-10-05):** a **searchable currency selector** (Popover + search) covering **sats + BTC** (both Bitcoin units) and every **fiat** Coinbase reports (crypto is filtered out via `FIAT_CODES`), and switching **converts every money field** diff --git a/src/components/DeckCostCalculator.tsx b/src/components/DeckCostCalculator.tsx index 7827df9..9d0b01d 100644 --- a/src/components/DeckCostCalculator.tsx +++ b/src/components/DeckCostCalculator.tsx @@ -1293,17 +1293,17 @@ export function DeckCostCalculator() { {/* Equipment */}
- +
{s.equipment.map((e) => ( - - - - - - + + + + +
EquipmentCost ({unit})Setup (h)Lifetime boardsPer board
patch({ equipment: s.equipment.map((x) => x.id === e.id ? { ...x, name: ev.target.value } : x) })} className="h-8 min-w-36" />{numInput(e.cost, (n) => patch({ equipment: s.equipment.map((x) => x.id === e.id ? { ...x, cost: n } : x) }))}{numInput(e.setupHrs, (n) => patch({ equipment: s.equipment.map((x) => x.id === e.id ? { ...x, setupHrs: n } : x) }))}{numInput(e.boards, (n) => patch({ equipment: s.equipment.map((x) => x.id === e.id ? { ...x, boards: n } : x) }))}{fmt(e.boards > 0 ? (e.cost + e.setupHrs * r.laborPerHr) / e.boards : 0)} + patch({ equipment: s.equipment.map((x) => x.id === e.id ? { ...x, name: ev.target.value } : x) })} className="h-8 min-w-36" />{numInput(e.cost, (n) => patch({ equipment: s.equipment.map((x) => x.id === e.id ? { ...x, cost: n } : x) }))}{numInput(e.setupHrs, (n) => patch({ equipment: s.equipment.map((x) => x.id === e.id ? { ...x, setupHrs: n } : x) }))}{numInput(e.boards, (n) => patch({ equipment: s.equipment.map((x) => x.id === e.id ? { ...x, boards: n } : x) }))}{fmt(e.boards > 0 ? (e.cost + e.setupHrs * r.laborPerHr) / e.boards : 0)}
@@ -1334,7 +1334,7 @@ export function DeckCostCalculator() { {/* Materials */}
- +
{s.materials.map((m) => { @@ -1342,14 +1342,14 @@ export function DeckCostCalculator() { const up = (p: Partial) => patch({ materials: s.materials.map((x) => x.id === m.id ? { ...x, ...p } : x) }); return ( - - - - - - - - + + + + + + +
MaterialOrder ({unit})Ship ({unit})Boards / orderOrder (m)Unload (m)Per board
up({ name: e.target.value })} className="h-8 min-w-36" />{numInput(m.orderCost, (n) => up({ orderCost: n }))}{numInput(m.shipping, (n) => up({ shipping: n }))}{numInput(m.boards, (n) => up({ boards: n }))}{numInput(m.orderMin, (n) => up({ orderMin: n }))}{numInput(m.unloadMin, (n) => up({ unloadMin: n }))}{fmt(per)} + up({ name: e.target.value })} className="h-8 min-w-36" />{numInput(m.orderCost, (n) => up({ orderCost: n }))}{numInput(m.shipping, (n) => up({ shipping: n }))}{numInput(m.boards, (n) => up({ boards: n }))}{numInput(m.orderMin, (n) => up({ orderMin: n }))}{numInput(m.unloadMin, (n) => up({ unloadMin: n }))}{fmt(per)}
@@ -1370,7 +1370,7 @@ export function DeckCostCalculator() { {/* Processes */}
- +
{advanced && }{advanced && } {s.processes.map((p) => { @@ -1379,13 +1379,13 @@ export function DeckCostCalculator() { const up = (pp: Partial) => patch({ processes: s.processes.map((x) => x.id === p.id ? { ...x, ...pp } : x) }); return ( - - - - {advanced && } - {advanced && } - - + + + {advanced && } + {advanced && } + +
ProcessTime (m)Boards / runRate/hr (0 = shop)Scrap %Per board
up({ name: e.target.value })} className="h-8 min-w-36" />{numInput(p.minutes, (n) => up({ minutes: n }))}{numInput(p.boards, (n) => up({ boards: n }))}{numInput(p.rate, (n) => up({ rate: n }))}{numInput(p.scrapPct, (n) => up({ scrapPct: Math.min(99, n) }))}{fmt(per)} + up({ name: e.target.value })} className="h-8 min-w-36" />{numInput(p.minutes, (n) => up({ minutes: n }))}{numInput(p.boards, (n) => up({ boards: n }))}{numInput(p.rate, (n) => up({ rate: n }))}{numInput(p.scrapPct, (n) => up({ scrapPct: Math.min(99, n) }))}{fmt(per)}
@@ -1406,7 +1406,7 @@ export function DeckCostCalculator() { {/* Packaging */}
- +
{s.packaging.map((p) => { @@ -1414,11 +1414,11 @@ export function DeckCostCalculator() { const up = (pp: Partial) => patch({ packaging: s.packaging.map((x) => x.id === p.id ? { ...x, ...pp } : x) }); return ( - - - - - + + + +
PackagingTime/board (m)Materials/board ({unit})Per board
up({ name: e.target.value })} className="h-8 min-w-36" />{numInput(p.minutesPerBoard, (n) => up({ minutesPerBoard: n }))}{numInput(p.materialsPerBoard, (n) => up({ materialsPerBoard: n }))}{fmt(per)} + up({ name: e.target.value })} className="h-8 min-w-36" />{numInput(p.minutesPerBoard, (n) => up({ minutesPerBoard: n }))}{numInput(p.materialsPerBoard, (n) => up({ materialsPerBoard: n }))}{fmt(per)}
@@ -1507,15 +1507,19 @@ export function DeckCostCalculator() {

)} {advanced && ( -
+

Cost vs quantity (setup amortised)

- - {r.curve.map((c) => )} - - {r.curve.map((c) => )} - {r.curve.map((c) => )} - -
Decks{c.qty}
Unit price{fmt(c.unit)}
Order total{fmt(c.total)}
+
+ {r.curve.map((c) => ( +
+ {c.qty}{c.qty === 1 ? ' deck' : ' decks'} + + {fmt(c.unit)} / deck + order {fmt(c.total)} + +
+ ))} +
)}

Does not include shipping to the buyer.

diff --git a/src/index.css b/src/index.css index 7fd1cc1..4715cd7 100644 --- a/src/index.css +++ b/src/index.css @@ -873,3 +873,44 @@ select { .skatespots-dark-tiles { filter: invert(1) hue-rotate(180deg) brightness(0.9) contrast(0.9) saturate(0.7); } + +/* ── Calculator sheet tables ────────────────────────────────────────────── + The deck-cost tables carry 5–8 columns (name + numbers + per-board + row + actions). That cannot fit a 390px phone: values get clipped ("900" → "90") + and columns fall off the edge. On phones each row becomes a stacked card + (label left, control right); on sm+ it stays a real table. `td::before` + pulls the column title from `data-label`. */ +@media (max-width: 639px) { + .ss-sheet-table, .ss-sheet-table tbody, .ss-sheet-table tr, .ss-sheet-table td { display: block; width: 100%; } + .ss-sheet-table thead { display: none; } + .ss-sheet-table tr { + margin-bottom: 0.5rem; + padding: 0.5rem 0.625rem; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 0.75rem; + background: rgba(255, 255, 255, 0.02); + } + .ss-sheet-table td { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.2rem 0; + border: 0; + } + .ss-sheet-table td[data-label]::before { + content: attr(data-label); + flex: 0 0 auto; + color: #6b7280; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + } + .ss-sheet-table td > input, + .ss-sheet-table td > div { max-width: 62%; } + .ss-sheet-table td.ss-cell-name { margin-bottom: 0.35rem; } + .ss-sheet-table td.ss-cell-name > input { max-width: 100%; } + .ss-sheet-table td.ss-cell-actions { justify-content: flex-end; padding-top: 0.35rem; } +}