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.
This commit is contained in:
Esa Kataja
2026-09-05 17:35:37 +03:00
commit 6ed047aafd
13 changed files with 2067 additions and 0 deletions
+214
View File
@@ -0,0 +1,214 @@
<!doctype html>
<html lang="en" data-theme="auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Foodster — B · Enamel tile</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=Bricolage+Grotesque:opsz,[email protected],500;12..96,700;12..96,800&family=Karla:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root{
color-scheme: light dark;
--grout: light-dark(#D9D6CE, #191A1C);
--card: light-dark(#FFFFFF, #232529);
--ink: light-dark(#1B1C18, #EDEDE8);
--muted: light-dark(#71736B, #94968D);
--line: light-dark(#C7C4BB, #32343A);
--glaze: light-dark(rgba(255,255,255,.30), rgba(255,255,255,.10));
--meat: light-dark(#A8452F, #8E3826);
--chicken: light-dark(#C68A16, #A87211);
--fish: light-dark(#2C6B92, #23566F);
--vegetarian:light-dark(#4E7A38, #3F6430);
--on-tile:#FFFCF4;
}
html[data-theme="light"]{ color-scheme: only light; }
html[data-theme="dark"] { color-scheme: only dark; }
*{box-sizing:border-box;}
body{margin:0; background:var(--grout); color:var(--ink);
font-family:Karla,system-ui,sans-serif; font-size:16px; line-height:1.5;
-webkit-font-smoothing:antialiased;}
.wrap{max-width:1120px; margin:0 auto; padding:0 20px 80px;}
.top{display:flex; align-items:center; gap:18px; flex-wrap:wrap; padding:22px 0;}
.logo{font-family:"Bricolage Grotesque",sans-serif; font-weight:800; font-size:22px;
letter-spacing:-.03em;}
nav{display:flex; gap:4px; margin-left:auto;}
nav a{font-size:14px; font-weight:500; color:var(--muted); text-decoration:none;
padding:7px 14px; border-radius:999px;}
nav a[aria-current]{background:var(--card); color:var(--ink);}
nav a:hover{color:var(--ink);}
.themebtn{font-family:Karla,sans-serif; font-size:13px; background:var(--card);
border:1px solid var(--line); color:var(--muted); padding:7px 13px;
border-radius:999px; cursor:pointer;}
.themebtn:hover{color:var(--ink);}
header.lede{padding:34px 0 22px; max-width:44ch;}
h1{font-family:"Bricolage Grotesque",sans-serif; font-weight:700; font-size:clamp(38px,6vw,60px);
line-height:1.0; letter-spacing:-.035em; margin:0 0 12px;}
.lede p{margin:0; color:var(--muted); font-size:16px;}
/* ---- signature: coverage bar ---- */
.coverage{margin:26px 0 8px;}
.covbar{display:flex; height:10px; border-radius:3px; overflow:hidden; gap:2px;}
.covbar i{display:block;}
.covkey{display:flex; gap:16px; flex-wrap:wrap; margin-top:10px;
font-size:13px; color:var(--muted);}
.covkey span{display:flex; align-items:center; gap:7px;}
.covkey b{width:9px; height:9px; border-radius:2px;}
.covkey em{font-style:normal; color:var(--ink); font-weight:500;}
.actions{display:flex; gap:10px; margin:26px 0 22px; flex-wrap:wrap;}
.regen{font-family:Karla,sans-serif; font-weight:700; font-size:15px; background:var(--ink);
color:var(--grout); border:0; padding:12px 22px; border-radius:8px; cursor:pointer;}
.regen:hover{opacity:.86;}
/* ---- tiles ---- */
.tiles{display:grid; gap:6px; grid-template-columns:repeat(auto-fill,minmax(232px,1fr));}
.tile{position:relative; border-radius:5px; padding:20px 18px 16px; color:var(--on-tile);
min-height:196px; display:flex; flex-direction:column; overflow:hidden;}
.tile::before{content:""; position:absolute; inset:0;
background:linear-gradient(158deg, var(--glaze) 0%, transparent 46%); pointer-events:none;}
.t-meat{background:var(--meat);} .t-chicken{background:var(--chicken);}
.t-fish{background:var(--fish);} .t-vegetarian{background:var(--vegetarian);}
.tile.wild{background:linear-gradient(112deg,
var(--meat) 0 25%, var(--chicken) 25% 50%, var(--fish) 50% 75%, var(--vegetarian) 75% 100%);}
.tag{font-size:11px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
opacity:.82; margin:0 0 auto;}
.dish{font-family:"Bricolage Grotesque",sans-serif; font-weight:700; font-size:27px;
line-height:1.05; letter-spacing:-.03em; margin:16px 0 8px;}
.sides{margin:0 0 12px; font-size:14px; opacity:.88;}
.last{font-size:12px; opacity:.7; margin:0 0 14px;}
.swaps{display:flex; gap:6px; position:relative;}
.swaps button{flex:1; font-family:Karla,sans-serif; font-size:12px; font-weight:700;
background:rgba(255,255,255,.16); color:var(--on-tile); border:0;
padding:9px 6px; border-radius:6px; cursor:pointer; backdrop-filter:blur(2px);}
.swaps button:hover:not(:disabled){background:rgba(255,255,255,.30);}
.swaps button:disabled{opacity:.4; cursor:not-allowed;}
/* ---- log ---- */
.logstrip{margin-top:44px;}
.logstrip h2{font-family:"Bricolage Grotesque",sans-serif; font-weight:700; font-size:19px;
letter-spacing:-.02em; margin:0 0 12px;}
.logrow{display:flex; gap:14px; align-items:center; background:var(--card);
border-radius:7px; padding:11px 15px; margin-bottom:5px; font-size:15px;}
.logrow time{color:var(--muted); font-size:13px; min-width:78px;}
.logrow i{width:10px; height:10px; border-radius:2px;}
.logrow em{color:var(--muted); font-style:normal; font-size:13px; margin-left:auto;}
.k-meat{background:var(--meat);} .k-chicken{background:var(--chicken);}
.k-fish{background:var(--fish);} .k-vegetarian{background:var(--vegetarian);}
:focus-visible{outline:2.5px solid var(--ink); outline-offset:2px;}
@media (prefers-reduced-motion:reduce){ *{transition:none!important;} }
</style>
</head>
<body>
<div class="wrap">
<div class="top">
<div class="logo">Foodster</div>
<nav><a href="#" aria-current="page">Plan</a><a href="#">Log</a><a href="#">Catalog</a></nav>
<button class="themebtn" id="t">Theme: auto</button>
</div>
<header class="lede">
<h1>Seven dinners, no schedule.</h1>
<p>Pick any of these on any night. Swap what doesn't appeal.</p>
</header>
<div class="coverage">
<div class="covbar" role="img" aria-label="This week covers meat, chicken, fish and vegetarian">
<i class="t-meat" style="flex:3"></i>
<i class="t-chicken" style="flex:1"></i>
<i class="t-fish" style="flex:1"></i>
<i class="t-vegetarian"style="flex:2"></i>
</div>
<div class="covkey">
<span><b class="t-meat"></b><em>Meat</em> ×3</span>
<span><b class="t-chicken"></b><em>Chicken</em> ×1</span>
<span><b class="t-fish"></b><em>Fish</em> ×1</span>
<span><b class="t-vegetarian"></b><em>Vegetarian</em> ×2</span>
</div>
</div>
<div class="actions"><button class="regen">New seven</button></div>
<div class="tiles">
<article class="tile t-meat">
<p class="tag">Meat</p>
<h3 class="dish">Jauhelihakastike</h3>
<p class="sides">Perunamuusi · Vihersalaatti</p>
<p class="last">Last eaten 23 days ago</p>
<div class="swaps"><button>Similar</button><button>Anything</button></div>
</article>
<article class="tile t-chicken">
<p class="tag">Chicken</p>
<h3 class="dish">Broilerikastike</h3>
<p class="sides">Riisi</p>
<p class="last">Last eaten 31 days ago</p>
<div class="swaps"><button>Similar</button><button>Anything</button></div>
</article>
<article class="tile wild">
<p class="tag">Meat · Chicken · Fish · Veg</p>
<h3 class="dish">Tortillat</h3>
<p class="sides">Served on its own</p>
<p class="last">Last eaten 40 days ago</p>
<div class="swaps">
<button disabled title="Nothing else covers all four">Similar</button>
<button>Anything</button>
</div>
</article>
<article class="tile t-fish">
<p class="tag">Fish</p>
<h3 class="dish">Uunilohi</h3>
<p class="sides">Riisi · Vihersalaatti</p>
<p class="last">Last eaten 34 days ago</p>
<div class="swaps"><button>Similar</button><button>Anything</button></div>
</article>
<article class="tile t-vegetarian">
<p class="tag">Vegetarian</p>
<h3 class="dish">Kasvispyörykät</h3>
<p class="sides">Perunamuusi · Höyrytetyt Porkkanat</p>
<p class="last">Last eaten 52 days ago</p>
<div class="swaps"><button>Similar</button><button>Anything</button></div>
</article>
<article class="tile t-meat">
<p class="tag">Meat</p>
<h3 class="dish">Makaronilaatikko</h3>
<p class="sides">Served on its own</p>
<p class="last">Last eaten 19 days ago</p>
<div class="swaps"><button>Similar</button><button>Anything</button></div>
</article>
<article class="tile t-vegetarian">
<p class="tag">Vegetarian</p>
<h3 class="dish">Hernekeitto</h3>
<p class="sides">Served on its own</p>
<p class="last">Last eaten 27 days ago</p>
<div class="swaps"><button>Similar</button><button>Anything</button></div>
</article>
</div>
<section class="logstrip">
<h2>Recently eaten</h2>
<div class="logrow"><time>Thu 3 Sep</time><i class="k-fish"></i><span>Lohikeitto</span><em>Ruisleipä</em></div>
<div class="logrow"><time>Wed 2 Sep</time><i class="k-chicken"></i><span>Kanacurry</span><em>Riisi</em></div>
<div class="logrow"><time>Tue 1 Sep</time><i class="k-meat"></i><span>Uunimakkara</span><em>Perunamuusi · Vihersalaatti</em></div>
</section>
</div>
<script>
const t=document.getElementById('t'), h=document.documentElement, m=['auto','light','dark'];
t.onclick=()=>{const n=m[(m.indexOf(h.dataset.theme)+1)%3]; h.dataset.theme=n; t.textContent='Theme: '+n;};
</script>
</body>
</html>