#meta
The Bakers’ home page: who’s cooking, whose chores are left and what’s coming up. The shopping list is written on the home page itself.
family = {}
-- Widget links open pages through editor.navigate, so they work wherever the space is served
local function go(ref)
return function()
editor.navigate(ref)
end
end
local function avatar(handle)
return dom.span {
class = "fam-avatar fam-" .. handle,
handle:sub(1, 1):upper(),
}
end
-- The fridge door: a note with everyone’s faces, and one saying what’s for dinner tonight
function family.header()
local handles = query[
from p = index.contentPages "person"
order by p.name
select p.handle
](<
from p = index.contentPages "person"
order by p.name
select p.handle
>)
local faces = {}
for _, handle in ipairs(handles) do
table.insert(faces, avatar(handle))
end
local today = os.date "%a"
local meals = query[
from m = index.items "meal"
where m.day == today
select m
](<
from m = index.items "meal"
where m.day == today
select m
>)
local tonight = meals[1]
return widget.htmlBlock(dom.div {
class = "fam fam-header",
dom.div {
class = "fam-note fam-title",
dom.h1 { "The Bakers" },
dom.p { "Meals, shopping, chores and plans." },
dom.div {
class = "fam-faces",
table.unpack(faces),
},
},
tonight and dom.div {
class = "fam-note fam-tonight",
dom.span { "Tonight" },
dom.strong { tonight.name },
dom.span { "cooking: ", tonight.cook },
} or nil,
})
end
local DAYS = {
Mon = 1,
Tue = 2,
Wed = 3,
Thu = 4,
Fri = 5,
Sat = 6,
Sun = 7,
}
function family.meals()
local meals = query[
from m = index.items "meal"
select m
](<
from m = index.items "meal"
select m
>)
table.sort(meals, function(a, b)
return DAYS[a.day] < DAYS[b.day]
end)
local today = os.date "%a"
local cards = {}
for _, m in ipairs(meals) do
table.insert(cards, dom.div {
class = m.day == today and "fam-meal fam-today" or "fam-meal",
dom.div { class = "fam-day", m.day },
dom.div { class = "fam-dish", m.name },
dom.div { class = "fam-cook", m.cook },
})
end
return widget.htmlBlock(dom.div {
class = "fam fam-meals",
table.unpack(cards),
})
end
function family.chores()
local people = query[
from p = index.contentPages "person"
order by p.name
select p
](<
from p = index.contentPages "person"
order by p.name
select p
>)
local chores = query[
from t = index.tasks()
where t.page == "Chores" and not t.done
select t
](<
from t = index.tasks()
where t.page == "Chores" and not t.done
select t
>)
local rows = {}
for _, p in ipairs(people) do
local mine = {}
for _, t in ipairs(chores) do
if t.text:find("@" .. p.handle, 1, true) then
if #mine > 0 then
table.insert(mine, ", ")
end
table.insert(mine, dom.a {
onclick = go(t.ref),
(t.text:gsub("@%w+ ", "")),
})
end
end
if #mine == 0 then
mine = { "all done ✓" }
end
table.insert(rows, dom.div {
class = "fam-chore",
avatar(p.handle),
dom.span { table.unpack(mine) },
})
end
return widget.htmlBlock(dom.div {
class = "fam fam-chores",
table.unpack(rows),
})
end
function family.events()
local events = query[
from e = index.items "event"
order by e.date
select e
](<
from e = index.items "event"
order by e.date
select e
>)
local rows = {}
for _, e in ipairs(events) do
local y, m, d = e.date:match "(%d+)-(%d+)-(%d+)"
local time = os.time {
year = tonumber(y),
month = tonumber(m),
day = tonumber(d),
hour = 12,
}
table.insert(rows, dom.div {
class = "fam-event",
dom.div {
class = "fam-date",
dom.span { os.date("%b", time) },
dom.strong { tostring(tonumber(d)) },
},
dom.span { e.name },
})
end
return widget.htmlBlock(dom.div {
class = "fam fam-events",
table.unpack(rows),
})
end
/* The family’s accent colour, on a warm paper background */
html[data-theme="light"]:root {
--ui-accent-color: #ea580c;
--root-background-color: #f4ead8;
--top-background-color: #eadcc2;
}
/* Let the home widgets sit on the page without the usual widget frame */
.sb-lua-directive-block:has(.fam),
.sb-lua-directive-block:has(.fam) .content {
border: none !important;
background: none !important;
overflow: visible !important;
}
/* The widget buttons float above the notes without a grey box */
.sb-lua-directive-block:has(.fam) .button-bar {
z-index: 2;
background: none !important;
}
/* Linked headings and cooks without the usual link chip */
#sb-main .cm-editor .sb-line-h1 .sb-wiki-link,
.fam-cook a {
background: none !important;
color: #c2410c !important;
}
/* Notes stuck on the fridge: slightly crooked, with a strip of tape */
.fam-note,
.fam-meal,
.fam-chores,
.fam-events {
position: relative;
border-radius: 2px;
box-shadow: 0 4px 10px rgba(67, 20, 7, 0.18);
color: #431407;
}
.fam-note::before,
.fam-chores::before,
.fam-events::before {
content: "";
position: absolute;
top: -9px;
left: calc(50% - 40px);
width: 80px;
height: 18px;
background: rgba(255, 255, 255, 0.55);
transform: rotate(-3deg);
}
/* The header: a title note and tonight’s dinner */
.fam-header {
display: flex;
align-items: flex-start;
gap: 28px;
padding: 30px 6px 6px;
}
.fam-title {
flex: 1;
padding: 16px 18px;
background: #fef3c7;
transform: rotate(-1.5deg);
}
.fam-title h1 {
margin: 0;
font-size: 1.9em;
color: #c2410c;
}
.fam-title p {
margin: 4px 0 10px;
font-size: 0.85em;
}
.fam-tonight {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 10px;
padding: 14px 16px;
background: #fb923c;
color: #fff;
transform: rotate(3deg);
}
.fam-tonight span {
font-size: 0.75em;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.fam-tonight strong {
font-size: 1.3em;
}
.fam-tonight a {
color: #fff !important;
}
.fam-faces {
display: flex;
padding-left: 8px;
}
.fam-faces .fam-avatar {
width: 32px;
height: 32px;
margin-left: -8px;
box-shadow: 0 0 0 3px #fef3c7;
}
/* Avatars: one colour per person */
.fam-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
width: 26px;
height: 26px;
border-radius: 50%;
color: #fff;
font-size: 0.8em;
font-weight: 700;
}
.fam-mia {
background: #be123c;
}
.fam-leo {
background: #0369a1;
}
.fam-noor {
background: #7e22ce;
}
.fam-finn {
background: #15803d;
}
/* A little note per dinner, each one a bit crooked; today’s is orange */
.fam-meals {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
padding: 6px 2px;
}
.fam-meal {
display: flex;
flex-direction: column;
gap: 2px;
padding: 7px 7px 9px;
background: #fffbeb;
font-size: 0.75em;
line-height: 1.25;
}
.fam-meal:nth-child(odd) {
background: #ffedd5;
transform: rotate(-2deg);
}
.fam-meal:nth-child(even) {
transform: rotate(1.5deg);
}
.fam-day {
color: #c2410c;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.fam-dish {
font-weight: 600;
}
.fam-cook,
.fam-cook a {
color: #9a3412 !important;
text-decoration: none !important;
}
.fam-meal.fam-today {
background: #fb923c;
}
.fam-today .fam-day,
.fam-today .fam-dish,
.fam-today .fam-cook,
.fam-today .fam-cook a {
color: #fff !important;
}
/* Chores and plans, each on a sheet of paper */
.fam-chores,
.fam-events {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 16px;
margin: 4px 2px;
padding: 16px 16px 12px;
background: #fffdf8;
font-size: 0.85em;
}
.fam-chores {
transform: rotate(0.6deg);
}
.fam-events {
transform: rotate(-0.6deg);
}
.fam-chore a {
color: inherit !important;
text-decoration: none !important;
}
.fam-chore,
.fam-event {
display: flex;
align-items: center;
gap: 10px;
}
.fam-date {
display: flex;
flex-direction: column;
align-items: center;
width: 44px;
padding: 4px 0;
border: 1px solid #fed7aa;
background: #fff7ed;
line-height: 1.1;
}
.fam-date span {
font-size: 0.7em;
text-transform: uppercase;
color: #c2410c;
}
.fam-date strong {
font-size: 1.2em;
}
/* Widget links navigate on click, so give them a pointer */
.fam a {
cursor: pointer;
}