Files
foodster/mockups/log-fi.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

346 lines
17 KiB
HTML
Raw 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="fi" data-theme="auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Foodster — kirjaus ja historia</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>
: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);
--warn: light-dark(#AF4230, #DE7561);
--liha: light-dark(#AF4230, #DE7561);
--kana: light-dark(#B57E10, #DFA83B);
--kala: light-dark(#25688F, #63AAD8);
--kasvis:light-dark(#457A3C, #82BE7A);
--tap: 48px;
}
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-liha{background:var(--liha);} .d-kana{background:var(--kana);}
.d-kala{background:var(--kala);} .d-kasvis{background:var(--kasvis);}
.d-sek{background:conic-gradient(var(--liha) 0 25%,var(--kana) 25% 50%,
var(--kala) 50% 75%,var(--kasvis) 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;}
.switch button{font-size:14px; font-weight:600; background:var(--card);
border:1px solid var(--line); color:var(--muted); padding:10px 12px;
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;}
/* ---- app chrome ---- */
.screen{flex:1; overflow-y:auto; overscroll-behavior:contain; position:relative;}
.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;}
.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;}
/* ---- 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;}
.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);}
/* ---- already-logged state ---- */
.logged{background:var(--card); border:1px solid var(--line); border-radius:14px; padding:18px;}
.logged .k{font-family:"Martian Mono",monospace; font-size:10px; letter-spacing:.09em;
text-transform:uppercase; color:var(--accent); margin:0 0 10px;}
.logged .nm{font-size:26px; font-weight:700; letter-spacing:-.035em; margin:0 0 3px;
display:flex; align-items:center; gap:10px;}
.logged .sd{font-size:14.5px; color:var(--muted); margin:0 0 16px;}
.logged .pair{display:flex; gap:8px;}
.logged .pair button{flex:1; min-height:var(--tap); font-size:15px; font-weight:600;
background:var(--sunk); border:1px solid var(--line); color:var(--ink);
border-radius:10px; cursor:pointer;}
.logged .pair button.del{color:var(--warn);}
.orlog{margin:22px 0 0; font-size:14px; color:var(--muted); text-align:center;}
.orlog button{background:none; border:0; color:var(--accent); font:inherit;
font-weight:600; text-decoration:underline; cursor:pointer; min-height:38px;}
/* ---- journal ---- */
.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:54px; 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:54px; 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;}
@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 · vaihe 1</h1>
<p>390&nbsp;px · Kirjaa, Historia, ja jo kirjattu tila</p>
<button class="themebtn" id="t">Teema: auto</button>
</div>
<div class="switch">
<button aria-pressed="true" data-v="f1">Kirjaa</button>
<button aria-pressed="false" data-v="f2">Kirjattu</button>
<button aria-pressed="false" data-v="f3">Historia</button>
</div>
<div class="frames">
<!-- ============ 1 · KIRJAA ============ -->
<div class="framewrap on" id="f1">
<p class="framelbl">1 · Kirjaa <span>— napauta ruoka, valitse lisukkeet</span></p>
<div class="phone">
<div class="screen">
<div class="appbar">
<h2>Mitä syötiin?</h2>
<div class="dayseg">
<button aria-pressed="true">Tänään</button>
<button aria-pressed="false">Eilen</button>
<input type="date" value="2026-09-05" aria-label="Muu päivä">
</div>
</div>
<div class="pad" style="padding-bottom:280px">
<input class="filter" type="search" placeholder="Etsi" aria-label="Etsi">
<div class="board">
<button class="pill xl"><i class="dot d-liha"></i>Lihapullat <span class="n">12</span></button>
<button class="pill xl"><i class="dot d-liha"></i>Jauhelihakastike <span class="n">11</span></button>
<button class="pill lg picked"><i class="dot d-kala"></i>Lohikeitto <span class="n">9</span></button>
<button class="pill lg"><i class="dot d-kana"></i>Kanacurry <span class="n">8</span></button>
<button class="pill lg"><i class="dot d-liha"></i>Makaronilaatikko <span class="n">7</span></button>
<button class="pill lg"><i class="dot d-kana"></i>Broilerikastike <span class="n">6</span></button>
<button class="pill md"><i class="dot d-kala"></i>Uunilohi <span class="n">5</span></button>
<button class="pill md"><i class="dot d-kasvis"></i>Kasvispyörykät <span class="n">5</span></button>
<button class="pill md"><i class="dot d-sek"></i>Tortillat <span class="n">4</span></button>
<button class="pill md"><i class="dot d-kasvis"></i>Hernekeitto <span class="n">4</span></button>
<button class="pill md"><i class="dot d-liha"></i>Uunimakkara <span class="n">3</span></button>
<button class="pill sm"><i class="dot d-liha"></i>Kaalilaatikko <span class="n">2</span></button>
<button class="pill sm"><i class="dot d-sek"></i>Kotipizza <span class="n">2</span></button>
<button class="pill sm"><i class="dot d-kala"></i>Kalapuikot <span class="n">1</span></button>
</div>
</div>
<div class="sheet">
<div class="grab"></div>
<h3>Lohikeitto</h3>
<p class="q">Lisukkeita?</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">Tallenna</button>
<button class="ghost">Peruuta</button>
</div>
</div>
<nav class="tabbar">
<a href="#" aria-current="page"><i></i>Kirjaa</a>
<a href="#"><i></i>Historia</a>
<a href="#"><i></i>Ruoat</a>
</nav>
</div>
</div>
<!-- ============ 2 · JO KIRJATTU ============ -->
<div class="framewrap" id="f2">
<p class="framelbl">2 · Päivä jo kirjattu <span>— yksi merkintä per päivä</span></p>
<div class="phone">
<div class="screen">
<div class="appbar">
<h2>Mitä syötiin?</h2>
<div class="dayseg">
<button aria-pressed="true">Tänään</button>
<button aria-pressed="false">Eilen</button>
<input type="date" value="2026-09-05" aria-label="Muu päivä">
</div>
</div>
<div class="pad">
<div class="logged">
<p class="k">Tänään kirjattu</p>
<p class="nm"><i class="dot d-kala"></i>Lohikeitto</p>
<p class="sd">Ruisleipä</p>
<div class="pair">
<button>Muokkaa</button>
<button class="del">Poista</button>
</div>
</div>
<p class="orlog">Väärä päivä? <button>Kirjaa toinen päivä</button></p>
</div>
</div>
<nav class="tabbar">
<a href="#" aria-current="page"><i></i>Kirjaa</a>
<a href="#"><i></i>Historia</a>
<a href="#"><i></i>Ruoat</a>
</nav>
</div>
</div>
<!-- ============ 3 · HISTORIA ============ -->
<div class="framewrap" id="f3">
<p class="framelbl">3 · Historia <span>— lista, ei kalenteria</span></p>
<div class="phone">
<div class="screen">
<div class="appbar">
<h2>Historia</h2>
</div>
<div class="pad">
<div class="monthrule">Syyskuu</div>
<div class="gapline"><time>pe 4.9.</time><span>Ei merkintää</span><button>Merkitse</button></div>
<div class="entry">
<time>to 3.9.</time>
<div><div class="nm"><i class="dot d-liha"></i>Lihapullat</div><div class="sd">Vihersalaatti</div></div>
<span class="chev"></span>
</div>
<div class="entry">
<time>ke 2.9.</time>
<div><div class="nm"><i class="dot d-kala"></i>Lohikeitto</div><div class="sd">Ruisleipä</div></div>
<span class="chev"></span>
</div>
<div class="entry">
<time>ti 1.9.</time>
<div><div class="nm"><i class="dot d-kana"></i>Kanacurry</div><div class="sd">Riisi</div></div>
<span class="chev"></span>
</div>
<div class="monthrule">Elokuu</div>
<div class="entry">
<time>ma 31.8.</time>
<div><div class="nm"><i class="dot d-liha"></i>Uunimakkara</div><div class="sd">Perunamuusi</div></div>
<span class="chev"></span>
</div>
<div class="entry">
<time>su 30.8.</time>
<div><div class="nm"><i class="dot d-kasvis"></i>Kasvispyörykät</div><div class="sd">Ei lisukkeita</div></div>
<span class="chev"></span>
</div>
<div class="entry">
<time>la 29.8.</time>
<div><div class="nm"><i class="dot d-liha"></i>Lihapullat</div><div class="sd">Perunamuusi</div></div>
<span class="chev"></span>
</div>
<div class="gapline"><time>pe 28.8.</time><span>Ei merkintää</span><button>Merkitse</button></div>
<div class="entry">
<time>to 27.8.</time>
<div><div class="nm"><i class="dot d-kana"></i>Kanacurry</div><div class="sd">Riisi</div></div>
<span class="chev"></span>
</div>
<div class="entry">
<time>ke 26.8.</time>
<div><div class="nm"><i class="dot d-liha"></i>Jauhelihakastike</div><div class="sd">Perunamuusi, vihersalaatti</div></div>
<span class="chev"></span>
</div>
</div>
</div>
<nav class="tabbar">
<a href="#"><i></i>Kirjaa</a>
<a href="#" aria-current="page"><i></i>Historia</a>
<a href="#"><i></i>Ruoat</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='Teema: '+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>