Files
foodster/mockups/log-layouts.html
T
Esa Kataja 6ed047aafd Initial commit: PRD, build tooling, and design mockups
Foodster is a self-hosted dinner log and meal suggester for one household.
Stage 1 (eating history) is in development; stage 2 (the suggester) follows
once there is enough history to weight against.

Replace the PRD's original stack (Nuxt, Postgres, Drizzle, Pico CSS) with
Go 1.27, net/http, templ, Datastar and SQLite — one static binary, no
Node.js in the build. Sections 3, 4, 5, 9, 10 and 11 are rewritten to match.

Record the decisions made while reviewing mockups:

- the UI is written in Finnish; PRD, code and comments stay English
- access is one shared password over HTTP Basic rather than open on the LAN
- images are CalVer vYYYYMMDD-N, built with Podman, run under Docker Compose
- registry coordinates live in .env, so no infrastructure detail is
  committed and the MIT publication option stays open

mockups/ holds standalone HTML design studies. log-fi.html is the current
one; the others are superseded exploration kept for reference.
2026-09-05 17:35:37 +03:00

441 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en" data-theme="auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Foodster — stage 1 log · mobile layouts</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700&family=Martian+Mono:wght@300;500&display=swap" rel="stylesheet">
<style>
/* ============ shared skin ============ */
:root{
color-scheme: light dark;
--paper: light-dark(#ECEDE8, #121316);
--card: light-dark(#FFFFFF, #1C1E22);
--sunk: light-dark(#E3E4DE, #17181B);
--ink: light-dark(#14161A, #E9EAE5);
--muted: light-dark(#6B6F6A, #8B8F89);
--line: light-dark(#D5D6D0, #2C2F34);
--accent:light-dark(#15616D, #58C6D2);
--onacc: light-dark(#FFFFFF, #0C1417);
--meat: light-dark(#AF4230, #DE7561);
--chicken: light-dark(#B57E10, #DFA83B);
--fish: light-dark(#25688F, #63AAD8);
--vegetarian:light-dark(#457A3C, #82BE7A);
--tap: 48px; /* minimum touch target */
}
html[data-theme="light"]{color-scheme:only light;}
html[data-theme="dark"] {color-scheme:only dark;}
*{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
body{margin:0; background:var(--sunk); color:var(--ink);
font-family:Archivo,system-ui,sans-serif; font-size:16px; line-height:1.45;
-webkit-font-smoothing:antialiased;}
button,input{font-family:inherit;}
:focus-visible{outline:2.5px solid var(--accent); outline-offset:2px;}
@media (prefers-reduced-motion:reduce){*{transition:none!important;}}
.dot{width:9px; height:9px; border-radius:2px; flex:none; display:inline-block;}
.d-meat{background:var(--meat);} .d-chicken{background:var(--chicken);}
.d-fish{background:var(--fish);} .d-vegetarian{background:var(--vegetarian);}
.d-wild{background:conic-gradient(var(--meat) 0 25%,var(--chicken) 25% 50%,
var(--fish) 50% 75%,var(--vegetarian) 75% 100%);}
/* ============ comparison rig (desktop only) ============ */
.rig{padding:20px; max-width:1400px; margin:0 auto;}
.rigtop{display:flex; align-items:center; gap:14px; flex-wrap:wrap; margin-bottom:18px;}
.rigtop h1{font-size:17px; font-weight:700; letter-spacing:-.02em; margin:0;}
.rigtop p{margin:0; font-size:13px; color:var(--muted);}
.themebtn{font-size:12px; background:var(--card); border:1px solid var(--line);
color:var(--muted); padding:7px 12px; border-radius:7px; cursor:pointer; margin-left:auto;}
.switch{display:none; gap:6px; margin-bottom:14px; flex-wrap:wrap;}
.switch button{font-size:14px; font-weight:600; background:var(--card);
border:1px solid var(--line); color:var(--muted); padding:10px 14px;
border-radius:8px; cursor:pointer; flex:1;}
.switch button[aria-pressed="true"]{background:var(--accent); border-color:var(--accent); color:var(--onacc);}
.frames{display:flex; gap:22px; align-items:flex-start; overflow-x:auto; padding-bottom:14px;}
.framewrap{flex:none;}
.framelbl{font-size:13px; font-weight:600; margin:0 0 9px; letter-spacing:-.01em;}
.framelbl span{font-weight:400; color:var(--muted);}
.phone{width:390px; height:788px; background:var(--paper); border:1px solid var(--line);
border-radius:26px; overflow:hidden; display:flex; flex-direction:column;}
/* ============ in-app chrome ============ */
.screen{flex:1; overflow-y:auto; overscroll-behavior:contain;}
.appbar{position:sticky; top:0; z-index:5; background:var(--paper);
border-bottom:1px solid var(--line); padding:14px 16px 12px;}
.appbar h2{margin:0; font-size:22px; font-weight:700; letter-spacing:-.03em;}
.appbar .meta{font-size:12.5px; color:var(--muted); margin:2px 0 0;}
.pad{padding:16px;}
.tabbar{flex:none; display:flex; background:var(--card); border-top:1px solid var(--line);
padding-bottom:env(safe-area-inset-bottom);}
.tabbar a{flex:1; text-align:center; text-decoration:none; color:var(--muted);
font-size:11.5px; font-weight:600; padding:11px 0 13px; letter-spacing:.01em;}
.tabbar a[aria-current]{color:var(--accent);}
.tabbar i{display:block; font-style:normal; font-size:18px; line-height:1.3;}
.primary{display:block; width:100%; min-height:var(--tap); background:var(--accent);
color:var(--onacc); border:0; border-radius:11px; font-size:16.5px; font-weight:700;
cursor:pointer; letter-spacing:-.01em;}
.ghost{display:block; width:100%; min-height:var(--tap); background:none; color:var(--muted);
border:0; font-size:15px; cursor:pointer;}
/* ============ 1 · CALENDAR-LED ============ */
.mini{display:grid; grid-template-columns:repeat(7,1fr); gap:4px;}
.mini .wn{font-size:9.5px; letter-spacing:.06em; text-transform:uppercase;
color:var(--muted); text-align:center; padding-bottom:3px;}
.mini button{aspect-ratio:1; background:var(--card); border:1px solid var(--line);
border-radius:8px; cursor:pointer; display:flex; flex-direction:column;
align-items:center; justify-content:center; gap:4px; padding:0; color:var(--muted);
font-size:11px;}
.mini button .dot{width:11px; height:11px; border-radius:3px;}
.mini button.empty{background:none; border-style:dashed;}
.mini button.empty::after{content:"+"; font-size:14px; opacity:.45;}
.mini button.today{border:2px solid var(--accent); color:var(--accent); font-weight:700;}
.mini button.future{opacity:.3; border-color:transparent; background:none;}
.tally{display:flex; align-items:baseline; gap:8px; margin:14px 0 0; font-size:12.5px;
color:var(--muted);}
.tally b{color:var(--ink); font-size:14px;}
.bar{flex:1; height:5px; border-radius:3px; background:var(--line); overflow:hidden;}
.bar i{display:block; height:100%; background:var(--accent);}
.tonight{background:var(--card); border:1px solid var(--line); border-radius:13px;
padding:16px; margin:18px 0 0;}
.tonight .k{font-family:"Martian Mono",monospace; font-size:10px; letter-spacing:.08em;
text-transform:uppercase; color:var(--muted); margin:0 0 9px;}
.dayrow{display:flex; gap:12px; align-items:center; padding:13px 0;
border-bottom:1px solid var(--line); min-height:var(--tap);}
.dayrow time{font-family:"Martian Mono",monospace; font-size:11px; color:var(--muted);
min-width:56px; flex:none;}
.dayrow .nm{font-size:16px; font-weight:600; letter-spacing:-.02em;}
.dayrow .sd{font-size:12.5px; color:var(--muted);}
.dayrow .chev{margin-left:auto; color:var(--muted); font-size:18px;}
.sechead{font-family:"Martian Mono",monospace; font-size:10px; letter-spacing:.09em;
text-transform:uppercase; color:var(--muted); margin:24px 0 4px;}
/* ============ 2 · TAP BOARD ============ */
.dayseg{display:flex; gap:6px; margin-top:11px;}
.dayseg button{flex:1; min-height:42px; font-size:14px; font-weight:600; background:var(--card);
border:1px solid var(--line); color:var(--ink); border-radius:9px; cursor:pointer;}
.dayseg button[aria-pressed="true"]{background:var(--ink); border-color:var(--ink); color:var(--paper);}
.dayseg input{flex:none; width:52px; font-size:16px; background:var(--card); color:var(--ink);
border:1px solid var(--line); border-radius:9px; padding:0 8px;}
.filter{width:100%; min-height:var(--tap); font-size:16px; background:var(--card);
color:var(--ink); border:1px solid var(--line); border-radius:10px;
padding:0 14px; margin-bottom:14px;}
.board{display:flex; flex-wrap:wrap; gap:8px;}
.pill{display:flex; align-items:center; gap:8px; background:var(--card);
border:1px solid var(--line); border-radius:11px; cursor:pointer; color:var(--ink);
font-weight:600; letter-spacing:-.022em; min-height:var(--tap);}
.pill .n{font-family:"Martian Mono",monospace; font-weight:300; color:var(--muted); font-size:10px;}
.pill.xl{font-size:22px; padding:14px 18px; flex:1 1 100%;}
.pill.lg{font-size:18px; padding:12px 16px;}
.pill.md{font-size:15.5px; padding:11px 14px;}
.pill.sm{font-size:14px; padding:10px 13px; color:var(--muted);}
.pill.picked{background:var(--accent); border-color:var(--accent); color:var(--onacc);}
.pill.picked .n{color:var(--onacc); opacity:.65;}
.sheetwrap{position:relative;}
.sheet{position:absolute; left:0; right:0; bottom:0; z-index:9; background:var(--card);
border-top:1px solid var(--line); border-radius:18px 18px 0 0; padding:8px 16px 16px;
box-shadow:0 -10px 30px -14px rgba(0,0,0,.4);}
.grab{width:38px; height:4px; border-radius:3px; background:var(--line); margin:0 auto 13px;}
.sheet h3{margin:0 0 2px; font-size:19px; letter-spacing:-.025em;}
.sheet .q{margin:0 0 12px; font-size:13px; color:var(--muted);}
.chips{display:flex; gap:7px; flex-wrap:wrap; margin-bottom:14px;}
.chips button{font-size:14.5px; min-height:42px; background:var(--sunk);
border:1px solid var(--line); color:var(--ink); padding:0 14px;
border-radius:999px; cursor:pointer;}
.chips button[aria-pressed="true"]{background:var(--accent); border-color:var(--accent); color:var(--onacc);}
/* ============ 3 · JOURNAL ============ */
.jtonight{background:var(--accent); color:var(--onacc); border-radius:14px;
padding:16px; margin-bottom:20px;}
.jtonight .k{font-family:"Martian Mono",monospace; font-size:10px; letter-spacing:.09em;
text-transform:uppercase; opacity:.75; margin:0 0 5px;}
.jtonight p{margin:0 0 13px; font-size:19px; font-weight:600; letter-spacing:-.025em;}
.jtonight button{width:100%; min-height:var(--tap); background:var(--onacc); color:var(--accent);
border:0; border-radius:10px; font-size:16px; font-weight:700; cursor:pointer;}
.entry{display:flex; gap:12px; align-items:center; padding:14px 0;
border-bottom:1px solid var(--line); min-height:var(--tap);}
.entry time{font-family:"Martian Mono",monospace; font-size:11px; color:var(--muted);
min-width:52px; flex:none;}
.entry .nm{font-size:17px; font-weight:600; letter-spacing:-.025em;
display:flex; align-items:center; gap:8px;}
.entry .sd{font-size:12.5px; color:var(--muted); margin-top:1px;}
.entry .chev{margin-left:auto; color:var(--muted); font-size:19px; flex:none;}
.gapline{display:flex; gap:12px; align-items:center; padding:12px 0;
border-bottom:1px dashed var(--line); font-size:13.5px; color:var(--muted);
min-height:var(--tap);}
.gapline time{font-family:"Martian Mono",monospace; font-size:11px; min-width:52px; flex:none;}
.gapline button{margin-left:auto; font-size:13.5px; background:none; border:0;
color:var(--accent); font-weight:600; cursor:pointer; min-height:38px; padding:0 4px;}
.monthrule{font-family:"Martian Mono",monospace; font-size:10px; letter-spacing:.12em;
text-transform:uppercase; color:var(--muted); padding:22px 0 5px;}
/* ============ real phone: drop the rig, go full bleed ============ */
@media (max-width:900px){
body{background:var(--paper);}
.rig{padding:0;}
.rigtop{padding:12px 16px 0; margin-bottom:10px;}
.switch{display:flex; padding:0 16px;}
.frames{display:block; overflow:visible; padding:0;}
.framewrap{display:none;}
.framewrap.on{display:block;}
.framelbl{display:none;}
.phone{width:auto; height:calc(100svh - 118px); border:0; border-radius:0;}
}
</style>
</head>
<body>
<div class="rig">
<div class="rigtop">
<h1>Foodster · stage 1 log</h1>
<p>390&nbsp;px. Three structures, one skin.</p>
<button class="themebtn" id="t">Theme: auto</button>
</div>
<div class="switch">
<button aria-pressed="true" data-v="f1">Calendar</button>
<button aria-pressed="false" data-v="f2">Tap board</button>
<button aria-pressed="false" data-v="f3">Journal</button>
</div>
<div class="frames">
<!-- ================= 1 · CALENDAR-LED ================= -->
<div class="framewrap on" id="f1">
<p class="framelbl">1 · Calendar-led <span>— history first, calendar navigates</span></p>
<div class="phone">
<div class="screen">
<div class="appbar">
<h2>What we ate</h2>
<p class="meta">Last five weeks · 3 Aug 6 Sep</p>
</div>
<div class="pad">
<div class="mini" aria-hidden="true">
<div class="wn">M</div><div class="wn">T</div><div class="wn">W</div><div class="wn">T</div>
<div class="wn">F</div><div class="wn">S</div><div class="wn">S</div>
</div>
<div class="mini">
<button>3<i class="dot d-meat"></i></button>
<button>4<i class="dot d-fish"></i></button>
<button class="empty">5</button>
<button>6<i class="dot d-meat"></i></button>
<button>7<i class="dot d-wild"></i></button>
<button>8<i class="dot d-vegetarian"></i></button>
<button>9<i class="dot d-fish"></i></button>
<button>10<i class="dot d-chicken"></i></button>
<button>11<i class="dot d-vegetarian"></i></button>
<button>12<i class="dot d-meat"></i></button>
<button class="empty">13</button>
<button>14<i class="dot d-chicken"></i></button>
<button>15<i class="dot d-meat"></i></button>
<button>16<i class="dot d-fish"></i></button>
<button>17<i class="dot d-meat"></i></button>
<button>18<i class="dot d-vegetarian"></i></button>
<button>19<i class="dot d-meat"></i></button>
<button class="empty">20</button>
<button>21<i class="dot d-wild"></i></button>
<button>22<i class="dot d-chicken"></i></button>
<button>23<i class="dot d-meat"></i></button>
<button>24<i class="dot d-fish"></i></button>
<button>25<i class="dot d-vegetarian"></i></button>
<button>26<i class="dot d-meat"></i></button>
<button>27<i class="dot d-chicken"></i></button>
<button class="empty">28</button>
<button>29<i class="dot d-meat"></i></button>
<button>30<i class="dot d-vegetarian"></i></button>
<button>31<i class="dot d-meat"></i></button>
<button>1<i class="dot d-chicken"></i></button>
<button>2<i class="dot d-fish"></i></button>
<button>3<i class="dot d-meat"></i></button>
<button class="empty">4</button>
<button class="today">5</button>
<button class="future">6</button>
</div>
<div class="tally">
<b>30</b><span>of 35 logged</span>
<span class="bar"><i style="width:86%"></i></span>
</div>
<div class="tonight">
<p class="k">Tonight · Sat 5 Sep</p>
<button class="primary">Log tonight's dinner</button>
</div>
<p class="sechead">Recent</p>
<div class="dayrow">
<time>Thu 3</time>
<div><div class="nm">Lihapullat</div><div class="sd">Vihersalaatti</div></div>
<span class="chev"></span>
</div>
<div class="dayrow">
<time>Wed 2</time>
<div><div class="nm">Lohikeitto</div><div class="sd">Ruisleipä</div></div>
<span class="chev"></span>
</div>
<div class="dayrow">
<time>Tue 1</time>
<div><div class="nm">Kanacurry</div><div class="sd">Riisi</div></div>
<span class="chev"></span>
</div>
</div>
</div>
<nav class="tabbar">
<a href="#" aria-current="page"><i></i>Log</a>
<a href="#"><i></i>History</a>
<a href="#"><i></i>Catalog</a>
</nav>
</div>
</div>
<!-- ================= 2 · TAP BOARD ================= -->
<div class="framewrap" id="f2">
<p class="framelbl">2 · Tap board <span>— fastest entry, sheet in the thumb zone</span></p>
<div class="phone">
<div class="screen sheetwrap">
<div class="appbar">
<h2>Tap what you ate</h2>
<div class="dayseg">
<button aria-pressed="true">Today</button>
<button aria-pressed="false">Yesterday</button>
<input type="date" value="2026-09-05" aria-label="Another date">
</div>
</div>
<div class="pad" style="padding-bottom:270px">
<input class="filter" type="search" placeholder="Filter dishes…" aria-label="Filter dishes">
<div class="board">
<button class="pill xl"><i class="dot d-meat"></i>Lihapullat <span class="n">12</span></button>
<button class="pill xl"><i class="dot d-meat"></i>Jauhelihakastike <span class="n">11</span></button>
<button class="pill lg picked"><i class="dot d-fish"></i>Lohikeitto <span class="n">9</span></button>
<button class="pill lg"><i class="dot d-chicken"></i>Kanacurry <span class="n">8</span></button>
<button class="pill lg"><i class="dot d-meat"></i>Makaronilaatikko <span class="n">7</span></button>
<button class="pill lg"><i class="dot d-chicken"></i>Broilerikastike <span class="n">6</span></button>
<button class="pill md"><i class="dot d-fish"></i>Uunilohi <span class="n">5</span></button>
<button class="pill md"><i class="dot d-vegetarian"></i>Kasvispyörykät <span class="n">5</span></button>
<button class="pill md"><i class="dot d-wild"></i>Tortillat <span class="n">4</span></button>
<button class="pill md"><i class="dot d-vegetarian"></i>Hernekeitto <span class="n">4</span></button>
<button class="pill md"><i class="dot d-meat"></i>Uunimakkara <span class="n">3</span></button>
<button class="pill sm"><i class="dot d-meat"></i>Kaalilaatikko <span class="n">2</span></button>
<button class="pill sm"><i class="dot d-wild"></i>Kotipizza <span class="n">2</span></button>
<button class="pill sm"><i class="dot d-fish"></i>Kalapuikot <span class="n">1</span></button>
</div>
</div>
<div class="sheet">
<div class="grab"></div>
<h3>Lohikeitto</h3>
<p class="q">Anything on the side?</p>
<div class="chips">
<button aria-pressed="true">Ruisleipä</button>
<button aria-pressed="false">Perunamuusi</button>
<button aria-pressed="false">Riisi</button>
<button aria-pressed="false">Vihersalaatti</button>
</div>
<button class="primary">Save Saturday</button>
<button class="ghost">Cancel</button>
</div>
</div>
<nav class="tabbar">
<a href="#" aria-current="page"><i></i>Log</a>
<a href="#"><i></i>History</a>
<a href="#"><i></i>Catalog</a>
</nav>
</div>
</div>
<!-- ================= 3 · JOURNAL ================= -->
<div class="framewrap" id="f3">
<p class="framelbl">3 · Journal <span>— one column, gaps called out inline</span></p>
<div class="phone">
<div class="screen">
<div class="appbar">
<h2>Dinner journal</h2>
<p class="meta">One line per night</p>
</div>
<div class="pad">
<div class="jtonight">
<p class="k">Tonight · Sat 5 Sep</p>
<p>Nothing written down yet.</p>
<button>What did we eat?</button>
</div>
<div class="monthrule">September</div>
<div class="gapline"><time>Fri 4</time><span>Not logged</span><button>Fill in</button></div>
<div class="entry">
<time>Thu 3</time>
<div><div class="nm"><i class="dot d-meat"></i>Lihapullat</div><div class="sd">Vihersalaatti</div></div>
<span class="chev"></span>
</div>
<div class="entry">
<time>Wed 2</time>
<div><div class="nm"><i class="dot d-fish"></i>Lohikeitto</div><div class="sd">Ruisleipä</div></div>
<span class="chev"></span>
</div>
<div class="entry">
<time>Tue 1</time>
<div><div class="nm"><i class="dot d-chicken"></i>Kanacurry</div><div class="sd">Riisi</div></div>
<span class="chev"></span>
</div>
<div class="monthrule">August</div>
<div class="entry">
<time>Mon 31</time>
<div><div class="nm"><i class="dot d-meat"></i>Uunimakkara</div><div class="sd">Perunamuusi</div></div>
<span class="chev"></span>
</div>
<div class="entry">
<time>Sun 30</time>
<div><div class="nm"><i class="dot d-vegetarian"></i>Kasvispyörykät</div><div class="sd">Served on its own</div></div>
<span class="chev"></span>
</div>
<div class="entry">
<time>Sat 29</time>
<div><div class="nm"><i class="dot d-meat"></i>Lihapullat</div><div class="sd">Perunamuusi</div></div>
<span class="chev"></span>
</div>
<div class="gapline"><time>Fri 28</time><span>Not logged</span><button>Fill in</button></div>
<div class="entry">
<time>Thu 27</time>
<div><div class="nm"><i class="dot d-chicken"></i>Kanacurry</div><div class="sd">Riisi</div></div>
<span class="chev"></span>
</div>
</div>
</div>
<nav class="tabbar">
<a href="#" aria-current="page"><i></i>Log</a>
<a href="#"><i></i>History</a>
<a href="#"><i></i>Catalog</a>
</nav>
</div>
</div>
</div>
</div>
<script>
const h=document.documentElement, tb=document.getElementById('t'), m=['auto','light','dark'];
tb.onclick=()=>{const n=m[(m.indexOf(h.dataset.theme)+1)%3];h.dataset.theme=n;tb.textContent='Theme: '+n;};
document.querySelectorAll('.switch button').forEach(b=>b.onclick=()=>{
document.querySelectorAll('.switch button').forEach(x=>x.setAttribute('aria-pressed',x===b));
document.querySelectorAll('.framewrap').forEach(f=>f.classList.toggle('on',f.id===b.dataset.v));
});
document.querySelectorAll('.chips button,.dayseg button').forEach(b=>b.onclick=()=>
b.setAttribute('aria-pressed', b.getAttribute('aria-pressed')!=='true'));
</script>
</body>
</html>