CookbookHobbies and playNo. 60
A Hobonichi planning librarygoals, habits, pens and spreads to try
See it in know.sh
Techo 2026No. 1
2026 in the Techogoals and habits
9 sections, 1,380 words, about 6 minutes, filed 3 January, revised 27 September, 4 highlights.
An Original, A6, one page a day. Five habits along the top of each page: twenty pages read, a walk, a sketch, the plants watered and the phone off by ten. A square filled for each one kept.
Habits kept each day, of five, June to September
- 22 JuneThe walk moves to before work
- 10 AugustFlu, three days
- 24 AugustThe phone charges outside the bedroom
| Week of | Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|---|
| 1 Jun | 2 | 3 | 2 | 2 | 2 | 2 | 3 |
| 8 Jun | 3 | 4 | 3 | 3 | 3 | 0 | 3 |
| 15 Jun | 3 | 3 | 1 | 1 | 2 | 1 | 2 |
| 22 Jun | 4 | 4 | 3 | 4 | 4 | 2 | 4 |
| 29 Jun | 4 | 5 | 3 | 3 | 3 | 3 | 3 |
| 6 Jul | 4 | 3 | 3 | 3 | 5 | 2 | 3 |
| 13 Jul | 4 | 2 | 4 | 5 | 2 | 3 | 3 |
| 20 Jul | 3 | 4 | 4 | 2 | 4 | 3 | 4 |
| 27 Jul | 5 | 4 | 4 | 2 | 4 | 2 | 2 |
| 3 Aug | 2 | 3 | 3 | 5 | 2 | 1 | 3 |
| 10 Aug | 0 | 0 | 0 | 5 | 4 | 1 | 1 |
| 17 Aug | 4 | 3 | 3 | 4 | 5 | 3 | 3 |
| 24 Aug | 5 | 5 | 5 | 5 | 5 | 2 | 5 |
| 31 Aug | 5 | 5 | 2 | 4 | 5 | 2 | 3 |
| 7 Sep | 5 | 3 | 5 | 5 | 4 | 4 | 4 |
| 14 Sep | 4 | 5 | 4 | 4 | 5 | 3 | 3 |
| 21 Sep | 5 | 5 | 4 | 3 | 4 | 3 |
Counted from the habit strip on each daily page, 1 June to 26 September.
Habits kept each day, of five, June to September
| Week of | Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|---|
| 1 Jun | 2 | 3 | 2 | 2 | 2 | 2 | 3 |
| 8 Jun | 3 | 4 | 3 | 3 | 3 | 0 | 3 |
| 15 Jun | 3 | 3 | 1 | 1 | 2 | 1 | 2 |
| 22 Jun | 4 | 4 | 3 | 4 | 4 | 2 | 4 |
| 29 Jun | 4 | 5 | 3 | 3 | 3 | 3 | 3 |
| 6 Jul | 4 | 3 | 3 | 3 | 5 | 2 | 3 |
| 13 Jul | 4 | 2 | 4 | 5 | 2 | 3 | 3 |
| 20 Jul | 3 | 4 | 4 | 2 | 4 | 3 | 4 |
| 27 Jul | 5 | 4 | 4 | 2 | 4 | 2 | 2 |
| 3 Aug | 2 | 3 | 3 | 5 | 2 | 1 | 3 |
| 10 Aug | 0 | 0 | 0 | 5 | 4 | 1 | 1 |
| 17 Aug | 4 | 3 | 3 | 4 | 5 | 3 | 3 |
| 24 Aug | 5 | 5 | 5 | 5 | 5 | 2 | 5 |
| 31 Aug | 5 | 5 | 2 | 4 | 5 | 2 | 3 |
| 7 Sep | 5 | 3 | 5 | 5 | 4 | 4 | 4 |
| 14 Sep | 4 | 5 | 4 | 4 | 5 | 3 | 3 |
| 21 Sep | 5 | 5 | 4 | 3 | 4 | 3 |
385 habits over 118 days; the most on Tuesday 30 June, 5 habits.
Shades: 0–1, 1–2, 2–3, 3–4 and 4+ habits
Marked on Monday 22 June: The walk moves to before work.
Marked on Monday 10 August: Flu, three days.
Marked on Monday 24 August: The phone charges outside the bedroom.
Source: Counted from the habit strip on each daily page, 1 June to 26 September.
```element calendar
{
"caption": "Habits kept each day, of five, June to September",
"start": "2026-06-01",
"values": "2, 3, 2, 2, 2, 2, 3, 3, 4, 3, 3, 3, 0, 3, 3, 3, 1, 1, 2, 1, 2, 4, 4, 3, 4, 4, 2, 4, 4, 5, 3, 3, 3, 3, 3, 4, 3, 3, 3, 5, 2, 3, 4, 2, 4, 5, 2, 3, 3, 3, 4, 4, 2, 4, 3, 4, 5, 4, 4, 2, 4, 2, 2, 2, 3, 3, 5, 2, 1, 3, 0, 0, 0, 5, 4, 1, 1, 4, 3, 3, 4, 5, 3, 3, 5, 5, 5, 5, 5, 2, 5, 5, 5, 2, 4, 5, 2, 3, 5, 3, 5, 5, 4, 4, 4, 4, 5, 4, 4, 5, 3, 3, 5, 5, 4, 3, 4, 3",
"units": "habits",
"steps": 5,
"range": "zero",
"marks": [
{
"date": "2026-06-22",
"label": "The walk moves to before work"
},
{
"date": "2026-08-10",
"label": "Flu, three days"
},
{
"date": "2026-08-24",
"label": "The phone charges outside the bedroom"
}
],
"source": "Counted from the habit strip on each daily page, 1 June to 26 September."
}
```Moving the walk to the morning did more than any app ever did. Since the phone left the bedroom, five of five is the usual day.
The year’s goals, January to December
Filling every daily page runs all year, as does reading twenty-four books. A copperplate course ran February to April and is done. A sketch a week runs from March to December. The Lisbon trip journal was 12 to 19 May, done. The ink swatch book runs September and October. Setting up the 2027 book is a milestone on 20 December.
| Task | Section | Starts | Ends | Status |
|---|---|---|---|---|
| Every daily page | All year | 2026-01-01 | 2026-12-31 | Active |
| Twenty-four books | All year | 2026-01-01 | 2026-12-31 | Active |
| A sketch a week | All year | 2026-03-02 | 2026-12-31 | Active |
| Copperplate course | Projects | 2026-02-02 | 2026-04-27 | Done |
| Lisbon trip journal | Projects | 2026-05-12 | 2026-05-19 | Done |
| Ink swatch book | Projects | 2026-09-01 | 2026-10-31 | Active |
| Set up 2027 | Projects | 2026-12-20 | 0 days | Milestone |
Weeks start on Monday.
```element gantt
{
"caption": "The year’s goals, January to December",
"description": "Filling every daily page runs all year, as does reading twenty-four books. A copperplate course ran February to April and is done. A sketch a week runs from March to December. The Lisbon trip journal was 12 to 19 May, done. The ink swatch book runs September and October. Setting up the 2027 book is a milestone on 20 December.",
"width": "wide",
"dateFormat": "YYYY-MM-DD",
"sections": [
{
"id": "all",
"label": "All year"
},
{
"id": "make",
"label": "Projects"
}
],
"tasks": [
{
"id": "pages",
"label": "Every daily page",
"section": "all",
"shape": "bar",
"start": "2026-01-01",
"end": "2026-12-31",
"status": "active"
},
{
"id": "books",
"label": "Twenty-four books",
"section": "all",
"shape": "bar",
"start": "2026-01-01",
"end": "2026-12-31",
"status": "active"
},
{
"id": "sketch",
"label": "A sketch a week",
"section": "all",
"shape": "bar",
"start": "2026-03-02",
"end": "2026-12-31",
"status": "active"
},
{
"id": "copper",
"label": "Copperplate course",
"section": "make",
"shape": "bar",
"start": "2026-02-02",
"end": "2026-04-27",
"status": "done"
},
{
"id": "lisbon",
"label": "Lisbon trip journal",
"section": "make",
"shape": "bar",
"start": "2026-05-12",
"end": "2026-05-19",
"status": "done"
},
{
"id": "swatch",
"label": "Ink swatch book",
"section": "make",
"shape": "bar",
"start": "2026-09-01",
"end": "2026-10-31",
"status": "active"
},
{
"id": "next",
"label": "Set up 2027",
"section": "make",
"shape": "milestone",
"start": "2026-12-20",
"duration": "0d"
}
],
"axisFormat": "%b",
"tickInterval": "1month",
"weekStart": "monday",
"weekend": "saturday",
"today": "shown"
}
```Sections
- 1Monthly plans, January to DecemberObservation, 12 linksThe month’s three things, copied onto the calendar pages each 1st.
- 2Pens and inksEvidence, key
- 3Spreads to trySuggestionFive ideas on cards, each with the pens it needs and a link to a photo.
and 6 more sections
Techo 20262026 in the Techo
3of 9
Pens and inks
Evidence, key section, 2 highlights, 1 note, 260 words
Swatched on a spare page at the back of this year’s book. Dry times are mine, counted with a thumb test, so treat them as rough.
Pens and inks, tested on the Techo’s Tomoe River paper
| Pen and tip | Ink | On the page |
|---|---|---|
| ✓Juice Up 0.4 | Black gel | To-dos: crisp, dry in 8 s |
| Kakuno F | Iroshizuku Kon-peki | Everyday; ghosts a little |
| Preppy 03 | Carbon Black | Under watercolour, once dry |
| Micron 01 | Pigment black | Sketch lines, the finest |
| Safari EF | Yama-dori | Headings: wet, slow, lovely |
Pilot Juice Up and Kakuno, Platinum Preppy, Sakura Pigma Micron, Lamy Safari. The Juice Up is the pick for lists: it is dry before the page turns.
Pens and inks, tested on the Techo’s Tomoe River paper
| Pen and tip | Ink | On the page |
|---|---|---|
| ✓ Juice Up 0.4 | Black gel | To-dos: crisp, dry in 8 s |
| Kakuno F | Iroshizuku Kon-peki | Everyday; ghosts a little |
| Preppy 03 | Carbon Black | Under watercolour, once dry |
| Micron 01 | Pigment black | Sketch lines, the finest |
| Safari EF | Yama-dori | Headings: wet, slow, lovely |
Pilot Juice Up and Kakuno, Platinum Preppy, Sakura Pigma Micron, Lamy Safari. The Juice Up is the pick for lists: it is dry before the page turns.
```element table
{
"caption": "Pens and inks, tested on the Techo’s Tomoe River paper",
"columns": [
{
"label": "Pen and tip",
"align": "left"
},
{
"label": "Ink",
"align": "left"
},
{
"label": "On the page",
"align": "left"
}
],
"rows": [
[
"Juice Up 0.4",
"Black gel",
"To-dos: crisp, dry in 8 s"
],
[
"Kakuno F",
"Iroshizuku Kon-peki",
"Everyday; ghosts a little"
],
[
"Preppy 03",
"Carbon Black",
"Under watercolour, once dry"
],
[
"Micron 01",
"Pigment black",
"Sketch lines, the finest"
],
[
"Safari EF",
"Yama-dori",
"Headings: wet, slow, lovely"
]
],
"pick": 0,
"notes": "Pilot Juice Up and Kakuno, Platinum Preppy, Sakura Pigma Micron, Lamy Safari. The Juice Up is the pick for lists: it is dry before the page turns."
}
```Spread to try: a weather and mood stripMonthly pages
- Where
- The margin beside the monthly calendar, one square a day
- Pens
- Micron 01 for the weather sign, Kon-peki to shade the mood
- Takes
- Twenty minutes to set up, a few seconds a day
- From
- A photo I saved in March, linked in the notes
Try it in October; if the squares stay empty by the 10th, drop it.
Spread to try: a weather and mood strip · Monthly pages
Where: The margin beside the monthly calendar, one square a day
Pens: Micron 01 for the weather sign, Kon-peki to shade the mood
Takes: Twenty minutes to set up, a few seconds a day
From: A photo I saved in March, linked in the notes
Try it in October; if the squares stay empty by the 10th, drop it.
```element card
{
"label": "Spread to try: a weather and mood strip",
"aside": "Monthly pages",
"rows": [
{
"term": "Where",
"value": "The margin beside the monthly calendar, one square a day"
},
{
"term": "Pens",
"value": "Micron 01 for the weather sign, Kon-peki to shade the mood"
},
{
"term": "Takes",
"value": "Twenty minutes to set up, a few seconds a day"
},
{
"term": "From",
"value": "A photo I saved in March, linked in the notes"
}
],
"note": "Try it in October; if the squares stay empty by the 10th, drop it."
}
```Plan a daily page
Choose the Original (A6) or the Cousin (A5), start from a work, sketch or travel day, then give each block more or fewer rows of the 3.7 mm grid and see the page fill. Margins, the date line and the quote line are simplified, so the row count is close, not exact.
<div class="techo">
<div class="bar">
<div class="seg" role="group" aria-label="Format">
<button type="button" data-f="a6" aria-pressed="true" aria-label="Original, A6"><span class="long">Original, A6</span><span class="short">A6</span></button>
<button type="button" data-f="a5" aria-pressed="false" aria-label="Cousin, A5"><span class="long">Cousin, A5</span><span class="short">A5</span></button>
</div>
<div class="seg" role="group" aria-label="Start from a day">
<button type="button" data-p="work" aria-pressed="true" aria-label="Work day"><span class="long">Work day</span><span class="short">Work</span></button>
<button type="button" data-p="sketch" aria-pressed="false" aria-label="Sketch day"><span class="long">Sketch day</span><span class="short">Sketch</span></button>
<button type="button" data-p="travel" aria-pressed="false" aria-label="Travel day"><span class="long">Travel day</span><span class="short">Travel</span></button>
</div>
</div>
<div class="body">
<figure class="page"><svg id="pg" class="pg" role="img" aria-label="The planned page" preserveAspectRatio="xMidYMid meet"></svg></figure>
<div class="side">
<ul class="zones" id="zones"></ul>
<label class="hrs"><input type="checkbox" id="hours" checked> Hours down the left edge</label>
<p class="say" id="say" aria-live="polite"></p>
</div>
</div>
</div>.techo { font: 400 14px/1.4 var(--sans); color: var(--ink); }
.bar { display: flex; flex-wrap: wrap; gap: 8px 14px; }
.short { display: none; }
.seg { display: flex; }
.seg button + button { border-left: 0; }
button { font: 500 13.5px/1 var(--sans); color: var(--ink); background: var(--paper); border: 1px solid var(--ink); border-radius: 0; padding: 9px 11px; min-height: 38px; cursor: pointer; touch-action: manipulation; }
button[aria-pressed='true'] { background: var(--ink); color: var(--paper); }
button:focus-visible, input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.body { display: flex; gap: 22px; margin-top: 16px; align-items: flex-start; }
.page { margin: 0; flex: 0 0 272px; }
.pg { display: block; width: 100%; height: auto; overflow: visible; }
.side { flex: 1; min-width: 0; }
.zones { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--ink); }
.zones li { display: flex; align-items: center; gap: 6px; border-bottom: 1px solid var(--rule); padding: 3px 0; }
.zones .nm { flex: 1; font: 400 16px/1.2 var(--serif); }
.zones .nm small { display: block; font: 400 12px/1.3 var(--sans); color: var(--ink-3); }
.zones button { min-width: 34px; min-height: 34px; padding: 0; font-size: 18px; }
.zones output { min-width: 2.2ch; text-align: right; font: 500 15px/1 var(--sans); font-variant-numeric: tabular-nums; color: var(--ink); }
.hrs { display: flex; align-items: center; gap: 8px; margin-top: 10px; font: 400 14px var(--sans); color: var(--ink); }
.say { margin: 10px 0 0; padding-top: 8px; border-top: 1px solid var(--rule); font: italic 400 15.5px/1.45 var(--serif); color: var(--ink-2); min-height: 3em; }
.pg .sheet { fill: var(--paper); stroke: var(--ink); stroke-width: 1px; vector-effect: non-scaling-stroke; }
.pg .grid { fill: none; stroke: var(--rule); stroke-width: 0.6px; vector-effect: non-scaling-stroke; }
.pg .zone { fill: var(--wash); fill-opacity: 0.72; stroke: var(--ink); stroke-width: 1px; vector-effect: non-scaling-stroke; }
.pg .zone.alt { fill: var(--mark); }
.pg .over { fill: none; stroke: var(--ink); stroke-width: 2px; stroke-dasharray: 4 3; vector-effect: non-scaling-stroke; }
.pg .hcol { fill: none; stroke: var(--ink-3); stroke-width: 1px; vector-effect: non-scaling-stroke; }
.pg text { fill: var(--ink); font-family: var(--sans); }
.pg .lab { font-weight: 500; }
.pg .dim { fill: var(--ink-3); }
.pg .it { font-family: var(--serif); font-style: italic; fill: var(--ink-3); }
@media (max-width: 560px) {
.body { flex-direction: column; align-items: stretch; gap: 14px; }
.page { flex: none; width: 172px; align-self: center; }
.bar { gap: 8px; flex-wrap: nowrap; }
.seg button { padding: 9px 8px; font-size: 13px; }
.long, .zones .nm small { display: none; }
.short { display: inline; }
.zones li { padding: 2px 0; }
.say { min-height: 0; }
}(function () {
var root = document.querySelector('.techo');
var svg = root.querySelector('#pg'), list = root.querySelector('#zones'), say = root.querySelector('#say'), hours = root.querySelector('#hours');
var NS = 'http://www.w3.org/2000/svg';
var GRID = 3.7; // mm, the Techo's grid
// Page sizes in mm; margins, date line and quote line simplified.
var FORMATS = {
a6: { name: 'Original, A6', w: 105, h: 148 },
a5: { name: 'Cousin, A5', w: 148, h: 210 }
};
var ZONES = [
{ id: 'todo', name: 'To-do', hint: 'ticked off by evening' },
{ id: 'notes', name: 'Notes', hint: 'meetings, calls, lists' },
{ id: 'journal', name: 'Journal', hint: 'a few lines on the day' },
{ id: 'sketch', name: 'Sketch', hint: 'fineliner and watercolour' },
{ id: 'stuck', name: 'Stuck in', hint: 'tickets, stamps, receipts' },
{ id: 'tomorrow', name: 'Tomorrow', hint: 'what carries over' }
];
// Each preset shares out the page's rows as fractions.
var PRESETS = {
work: { hours: true, todo: 0.24, notes: 0.32, journal: 0.2, tomorrow: 0.12 },
sketch: { hours: false, todo: 0.12, sketch: 0.52, journal: 0.26 },
travel: { hours: true, todo: 0.1, journal: 0.4, stuck: 0.36 }
};
var fmt = 'a6', preset = 'work', rows = {}, total = 0, cols = 0;
function geom() {
var f = FORMATS[fmt];
var top = f.h * 0.075, bottom = f.h * 0.07, side = f.w * 0.06;
cols = Math.floor((f.w - 2 * side) / GRID);
total = Math.floor((f.h - top - bottom) / GRID);
var gw = cols * GRID, gh = total * GRID;
return { f: f, x: (f.w - gw) / 2, y: top, gw: gw, gh: gh };
}
function applyPreset(p) {
geom();
var P = PRESETS[p];
ZONES.forEach(function (z) { rows[z.id] = Math.round((P[z.id] || 0) * total); });
hours.checked = P.hours;
}
function used() { return ZONES.reduce(function (s, z) { return s + rows[z.id]; }, 0); }
function el(name, attrs, text) {
var e = document.createElementNS(NS, name);
for (var k in attrs) e.setAttribute(k, attrs[k]);
if (text != null) e.textContent = text;
svg.appendChild(e);
return e;
}
function draw() {
var g = geom(), f = g.f, fs = f.w * 0.038;
while (svg.firstChild) svg.removeChild(svg.firstChild);
svg.setAttribute('viewBox', '0 0 ' + f.w + ' ' + f.h);
el('rect', { class: 'sheet', x: 0, y: 0, width: f.w, height: f.h });
var d = '';
for (var i = 0; i <= cols; i++) d += 'M' + (g.x + i * GRID) + ' ' + g.y + 'v' + g.gh;
for (var j = 0; j <= total; j++) d += 'M' + g.x + ' ' + (g.y + j * GRID) + 'h' + g.gw;
el('path', { class: 'grid', d: d });
var dt = el('text', { class: 'lab', x: g.x, y: g.y - fs * 0.55, 'font-size': fs * 1.15 }, 'Mon 28 ');
var mo = document.createElementNS(NS, 'tspan');
mo.setAttribute('class', 'dim');
mo.setAttribute('font-size', fs * 0.9);
mo.textContent = 'September';
dt.appendChild(mo);
var qy = g.y + g.gh + f.h * 0.035;
el('text', { class: 'it', x: g.x, y: qy + fs * 0.4, 'font-size': fs * 0.95 }, 'the day’s quote');
var hc = hours.checked ? 3 : 0, zx = g.x + hc * GRID, zw = g.gw - hc * GRID;
if (hc) {
el('rect', { class: 'hcol', x: g.x, y: g.y, width: hc * GRID, height: g.gh });
for (var h = 6; h <= 24; h += 3) {
var hy = g.y + ((h - 6) / 18) * g.gh;
el('text', { class: 'dim', x: g.x + GRID * 0.35, y: Math.min(hy + fs * 0.9, g.y + g.gh - fs * 0.25), 'font-size': fs * 0.78 }, String(h));
}
}
var y = 0, n = 0;
ZONES.forEach(function (z) {
var r = rows[z.id];
if (!r) return;
var y0 = Math.min(y, total), y1 = Math.min(y + r, total);
if (y1 > y0) {
el('rect', { class: 'zone' + (n % 2 ? ' alt' : ''), x: zx, y: g.y + y0 * GRID, width: zw, height: (y1 - y0) * GRID });
if (y1 - y0 >= 2) el('text', { class: 'lab', x: zx + GRID * 0.5, y: g.y + y0 * GRID + fs * 1.15, 'font-size': fs }, z.name + ' ' + r);
}
y += r;
n++;
});
var over = y - total, free = total - y;
if (over > 0) el('line', { class: 'over', x1: zx, x2: zx + zw, y1: g.y + g.gh, y2: g.y + g.gh });
else if (free >= 2) el('text', { class: 'it', x: zx + GRID * 0.5, y: g.y + y * GRID + fs * 1.2, 'font-size': fs }, 'free, ' + free + (free === 1 ? ' row' : ' rows'));
var parts = ZONES.filter(function (z) { return rows[z.id]; }).map(function (z) { return z.name + ' ' + rows[z.id] + ' rows'; });
svg.setAttribute('aria-label', f.name + ' daily page, ' + total + ' rows of ' + cols + ' squares' + (hc ? ', hours down the left' : '') + ': ' + (parts.join(', ') || 'nothing planned') + (over > 0 ? ', ' + over + ' rows over' : ', ' + free + ' free') + '.');
var head = f.name + ': ' + total + ' rows of ' + cols + ' squares. ';
if (over > 0) say.textContent = head + over + (over === 1 ? ' row' : ' rows') + ' over the page: take ' + (over === 1 ? 'it' : 'them') + ' from somewhere.';
else if (free === 0) say.textContent = head + 'Every row spoken for. Tight, but it fits.';
else say.textContent = head + used() + ' planned, ' + free + ' left for whatever the day brings.';
ZONES.forEach(function (z) { list.querySelector('#o-' + z.id).value = rows[z.id]; list.querySelector('#o-' + z.id).textContent = rows[z.id]; });
}
ZONES.forEach(function (z) {
var li = document.createElement('li');
li.innerHTML = '<span class="nm">' + z.name + '<small>' + z.hint + '</small></span>' +
'<button type="button" data-z="' + z.id + '" data-d="-1" aria-label="One row less for ' + z.name + '">−</button>' +
'<output id="o-' + z.id + '" aria-label="' + z.name + ', rows">0</output>' +
'<button type="button" data-z="' + z.id + '" data-d="1" aria-label="One row more for ' + z.name + '">+</button>';
list.appendChild(li);
});
function pressed(sel, attr, v) {
root.querySelectorAll(sel).forEach(function (b) { b.setAttribute('aria-pressed', String(b.getAttribute(attr) === v)); });
}
list.addEventListener('click', function (e) {
var b = e.target.closest('button');
if (!b) return;
var id = b.getAttribute('data-z');
rows[id] = Math.max(0, Math.min(total, rows[id] + Number(b.getAttribute('data-d'))));
preset = null;
pressed('[data-p]', 'data-p', '');
draw();
});
root.querySelectorAll('[data-p]').forEach(function (b) {
b.addEventListener('click', function () {
preset = b.getAttribute('data-p');
pressed('[data-p]', 'data-p', preset);
applyPreset(preset);
draw();
});
});
root.querySelectorAll('[data-f]').forEach(function (b) {
b.addEventListener('click', function () {
var old = total;
fmt = b.getAttribute('data-f');
pressed('[data-f]', 'data-f', fmt);
geom();
if (preset) applyPreset(preset);
else ZONES.forEach(function (z) { rows[z.id] = Math.round(rows[z.id] * total / old); });
draw();
});
});
hours.addEventListener('change', draw);
document.addEventListener('know-tokens', draw);
applyPreset('work');
draw();
})();```element widget
{
"title": "Plan a daily page",
"caption": "Choose the Original (A6) or the Cousin (A5), start from a work, sketch or travel day, then give each block more or fewer rows of the 3.7 mm grid and see the page fill. Margins, the date line and the quote line are simplified, so the row count is close, not exact.",
"height": 480,
"html": "<div class=\"techo\">\n <div class=\"bar\">\n <div class=\"seg\" role=\"group\" aria-label=\"Format\">\n <button type=\"button\" data-f=\"a6\" aria-pressed=\"true\" aria-label=\"Original, A6\"><span class=\"long\">Original, A6</span><span class=\"short\">A6</span></button>\n <button type=\"button\" data-f=\"a5\" aria-pressed=\"false\" aria-label=\"Cousin, A5\"><span class=\"long\">Cousin, A5</span><span class=\"short\">A5</span></button>\n </div>\n <div class=\"seg\" role=\"group\" aria-label=\"Start from a day\">\n <button type=\"button\" data-p=\"work\" aria-pressed=\"true\" aria-label=\"Work day\"><span class=\"long\">Work day</span><span class=\"short\">Work</span></button>\n <button type=\"button\" data-p=\"sketch\" aria-pressed=\"false\" aria-label=\"Sketch day\"><span class=\"long\">Sketch day</span><span class=\"short\">Sketch</span></button>\n <button type=\"button\" data-p=\"travel\" aria-pressed=\"false\" aria-label=\"Travel day\"><span class=\"long\">Travel day</span><span class=\"short\">Travel</span></button>\n </div>\n </div>\n <div class=\"body\">\n <figure class=\"page\"><svg id=\"pg\" class=\"pg\" role=\"img\" aria-label=\"The planned page\" preserveAspectRatio=\"xMidYMid meet\"></svg></figure>\n <div class=\"side\">\n <ul class=\"zones\" id=\"zones\"></ul>\n <label class=\"hrs\"><input type=\"checkbox\" id=\"hours\" checked> Hours down the left edge</label>\n <p class=\"say\" id=\"say\" aria-live=\"polite\"></p>\n </div>\n </div>\n</div>",
"css": ".techo { font: 400 14px/1.4 var(--sans); color: var(--ink); }\n.bar { display: flex; flex-wrap: wrap; gap: 8px 14px; }\n.short { display: none; }\n.seg { display: flex; }\n.seg button + button { border-left: 0; }\nbutton { font: 500 13.5px/1 var(--sans); color: var(--ink); background: var(--paper); border: 1px solid var(--ink); border-radius: 0; padding: 9px 11px; min-height: 38px; cursor: pointer; touch-action: manipulation; }\nbutton[aria-pressed='true'] { background: var(--ink); color: var(--paper); }\nbutton:focus-visible, input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }\n.body { display: flex; gap: 22px; margin-top: 16px; align-items: flex-start; }\n.page { margin: 0; flex: 0 0 272px; }\n.pg { display: block; width: 100%; height: auto; overflow: visible; }\n.side { flex: 1; min-width: 0; }\n.zones { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--ink); }\n.zones li { display: flex; align-items: center; gap: 6px; border-bottom: 1px solid var(--rule); padding: 3px 0; }\n.zones .nm { flex: 1; font: 400 16px/1.2 var(--serif); }\n.zones .nm small { display: block; font: 400 12px/1.3 var(--sans); color: var(--ink-3); }\n.zones button { min-width: 34px; min-height: 34px; padding: 0; font-size: 18px; }\n.zones output { min-width: 2.2ch; text-align: right; font: 500 15px/1 var(--sans); font-variant-numeric: tabular-nums; color: var(--ink); }\n.hrs { display: flex; align-items: center; gap: 8px; margin-top: 10px; font: 400 14px var(--sans); color: var(--ink); }\n.say { margin: 10px 0 0; padding-top: 8px; border-top: 1px solid var(--rule); font: italic 400 15.5px/1.45 var(--serif); color: var(--ink-2); min-height: 3em; }\n.pg .sheet { fill: var(--paper); stroke: var(--ink); stroke-width: 1px; vector-effect: non-scaling-stroke; }\n.pg .grid { fill: none; stroke: var(--rule); stroke-width: 0.6px; vector-effect: non-scaling-stroke; }\n.pg .zone { fill: var(--wash); fill-opacity: 0.72; stroke: var(--ink); stroke-width: 1px; vector-effect: non-scaling-stroke; }\n.pg .zone.alt { fill: var(--mark); }\n.pg .over { fill: none; stroke: var(--ink); stroke-width: 2px; stroke-dasharray: 4 3; vector-effect: non-scaling-stroke; }\n.pg .hcol { fill: none; stroke: var(--ink-3); stroke-width: 1px; vector-effect: non-scaling-stroke; }\n.pg text { fill: var(--ink); font-family: var(--sans); }\n.pg .lab { font-weight: 500; }\n.pg .dim { fill: var(--ink-3); }\n.pg .it { font-family: var(--serif); font-style: italic; fill: var(--ink-3); }\n@media (max-width: 560px) {\n .body { flex-direction: column; align-items: stretch; gap: 14px; }\n .page { flex: none; width: 172px; align-self: center; }\n .bar { gap: 8px; flex-wrap: nowrap; }\n .seg button { padding: 9px 8px; font-size: 13px; }\n .long, .zones .nm small { display: none; }\n .short { display: inline; }\n .zones li { padding: 2px 0; }\n .say { min-height: 0; }\n}",
"js": "(function () {\n var root = document.querySelector('.techo');\n var svg = root.querySelector('#pg'), list = root.querySelector('#zones'), say = root.querySelector('#say'), hours = root.querySelector('#hours');\n var NS = 'http://www.w3.org/2000/svg';\n var GRID = 3.7; // mm, the Techo's grid\n // Page sizes in mm; margins, date line and quote line simplified.\n var FORMATS = {\n a6: { name: 'Original, A6', w: 105, h: 148 },\n a5: { name: 'Cousin, A5', w: 148, h: 210 }\n };\n var ZONES = [\n { id: 'todo', name: 'To-do', hint: 'ticked off by evening' },\n { id: 'notes', name: 'Notes', hint: 'meetings, calls, lists' },\n { id: 'journal', name: 'Journal', hint: 'a few lines on the day' },\n { id: 'sketch', name: 'Sketch', hint: 'fineliner and watercolour' },\n { id: 'stuck', name: 'Stuck in', hint: 'tickets, stamps, receipts' },\n { id: 'tomorrow', name: 'Tomorrow', hint: 'what carries over' }\n ];\n // Each preset shares out the page's rows as fractions.\n var PRESETS = {\n work: { hours: true, todo: 0.24, notes: 0.32, journal: 0.2, tomorrow: 0.12 },\n sketch: { hours: false, todo: 0.12, sketch: 0.52, journal: 0.26 },\n travel: { hours: true, todo: 0.1, journal: 0.4, stuck: 0.36 }\n };\n var fmt = 'a6', preset = 'work', rows = {}, total = 0, cols = 0;\n\n function geom() {\n var f = FORMATS[fmt];\n var top = f.h * 0.075, bottom = f.h * 0.07, side = f.w * 0.06;\n cols = Math.floor((f.w - 2 * side) / GRID);\n total = Math.floor((f.h - top - bottom) / GRID);\n var gw = cols * GRID, gh = total * GRID;\n return { f: f, x: (f.w - gw) / 2, y: top, gw: gw, gh: gh };\n }\n\n function applyPreset(p) {\n geom();\n var P = PRESETS[p];\n ZONES.forEach(function (z) { rows[z.id] = Math.round((P[z.id] || 0) * total); });\n hours.checked = P.hours;\n }\n\n function used() { return ZONES.reduce(function (s, z) { return s + rows[z.id]; }, 0); }\n\n function el(name, attrs, text) {\n var e = document.createElementNS(NS, name);\n for (var k in attrs) e.setAttribute(k, attrs[k]);\n if (text != null) e.textContent = text;\n svg.appendChild(e);\n return e;\n }\n\n function draw() {\n var g = geom(), f = g.f, fs = f.w * 0.038;\n while (svg.firstChild) svg.removeChild(svg.firstChild);\n svg.setAttribute('viewBox', '0 0 ' + f.w + ' ' + f.h);\n el('rect', { class: 'sheet', x: 0, y: 0, width: f.w, height: f.h });\n var d = '';\n for (var i = 0; i <= cols; i++) d += 'M' + (g.x + i * GRID) + ' ' + g.y + 'v' + g.gh;\n for (var j = 0; j <= total; j++) d += 'M' + g.x + ' ' + (g.y + j * GRID) + 'h' + g.gw;\n el('path', { class: 'grid', d: d });\n var dt = el('text', { class: 'lab', x: g.x, y: g.y - fs * 0.55, 'font-size': fs * 1.15 }, 'Mon 28 ');\n var mo = document.createElementNS(NS, 'tspan');\n mo.setAttribute('class', 'dim');\n mo.setAttribute('font-size', fs * 0.9);\n mo.textContent = 'September';\n dt.appendChild(mo);\n var qy = g.y + g.gh + f.h * 0.035;\n el('text', { class: 'it', x: g.x, y: qy + fs * 0.4, 'font-size': fs * 0.95 }, 'the day’s quote');\n\n var hc = hours.checked ? 3 : 0, zx = g.x + hc * GRID, zw = g.gw - hc * GRID;\n if (hc) {\n el('rect', { class: 'hcol', x: g.x, y: g.y, width: hc * GRID, height: g.gh });\n for (var h = 6; h <= 24; h += 3) {\n var hy = g.y + ((h - 6) / 18) * g.gh;\n el('text', { class: 'dim', x: g.x + GRID * 0.35, y: Math.min(hy + fs * 0.9, g.y + g.gh - fs * 0.25), 'font-size': fs * 0.78 }, String(h));\n }\n }\n var y = 0, n = 0;\n ZONES.forEach(function (z) {\n var r = rows[z.id];\n if (!r) return;\n var y0 = Math.min(y, total), y1 = Math.min(y + r, total);\n if (y1 > y0) {\n el('rect', { class: 'zone' + (n % 2 ? ' alt' : ''), x: zx, y: g.y + y0 * GRID, width: zw, height: (y1 - y0) * GRID });\n if (y1 - y0 >= 2) el('text', { class: 'lab', x: zx + GRID * 0.5, y: g.y + y0 * GRID + fs * 1.15, 'font-size': fs }, z.name + ' ' + r);\n }\n y += r;\n n++;\n });\n var over = y - total, free = total - y;\n if (over > 0) el('line', { class: 'over', x1: zx, x2: zx + zw, y1: g.y + g.gh, y2: g.y + g.gh });\n else if (free >= 2) el('text', { class: 'it', x: zx + GRID * 0.5, y: g.y + y * GRID + fs * 1.2, 'font-size': fs }, 'free, ' + free + (free === 1 ? ' row' : ' rows'));\n\n var parts = ZONES.filter(function (z) { return rows[z.id]; }).map(function (z) { return z.name + ' ' + rows[z.id] + ' rows'; });\n svg.setAttribute('aria-label', f.name + ' daily page, ' + total + ' rows of ' + cols + ' squares' + (hc ? ', hours down the left' : '') + ': ' + (parts.join(', ') || 'nothing planned') + (over > 0 ? ', ' + over + ' rows over' : ', ' + free + ' free') + '.');\n var head = f.name + ': ' + total + ' rows of ' + cols + ' squares. ';\n if (over > 0) say.textContent = head + over + (over === 1 ? ' row' : ' rows') + ' over the page: take ' + (over === 1 ? 'it' : 'them') + ' from somewhere.';\n else if (free === 0) say.textContent = head + 'Every row spoken for. Tight, but it fits.';\n else say.textContent = head + used() + ' planned, ' + free + ' left for whatever the day brings.';\n ZONES.forEach(function (z) { list.querySelector('#o-' + z.id).value = rows[z.id]; list.querySelector('#o-' + z.id).textContent = rows[z.id]; });\n }\n\n ZONES.forEach(function (z) {\n var li = document.createElement('li');\n li.innerHTML = '<span class=\"nm\">' + z.name + '<small>' + z.hint + '</small></span>' +\n '<button type=\"button\" data-z=\"' + z.id + '\" data-d=\"-1\" aria-label=\"One row less for ' + z.name + '\">−</button>' +\n '<output id=\"o-' + z.id + '\" aria-label=\"' + z.name + ', rows\">0</output>' +\n '<button type=\"button\" data-z=\"' + z.id + '\" data-d=\"1\" aria-label=\"One row more for ' + z.name + '\">+</button>';\n list.appendChild(li);\n });\n\n function pressed(sel, attr, v) {\n root.querySelectorAll(sel).forEach(function (b) { b.setAttribute('aria-pressed', String(b.getAttribute(attr) === v)); });\n }\n\n list.addEventListener('click', function (e) {\n var b = e.target.closest('button');\n if (!b) return;\n var id = b.getAttribute('data-z');\n rows[id] = Math.max(0, Math.min(total, rows[id] + Number(b.getAttribute('data-d'))));\n preset = null;\n pressed('[data-p]', 'data-p', '');\n draw();\n });\n root.querySelectorAll('[data-p]').forEach(function (b) {\n b.addEventListener('click', function () {\n preset = b.getAttribute('data-p');\n pressed('[data-p]', 'data-p', preset);\n applyPreset(preset);\n draw();\n });\n });\n root.querySelectorAll('[data-f]').forEach(function (b) {\n b.addEventListener('click', function () {\n var old = total;\n fmt = b.getAttribute('data-f');\n pressed('[data-f]', 'data-f', fmt);\n geom();\n if (preset) applyPreset(preset);\n else ZONES.forEach(function (z) { rows[z.id] = Math.round(rows[z.id] * total / old); });\n draw();\n });\n });\n hours.addEventListener('change', draw);\n document.addEventListener('know-tokens', draw);\n\n applyPreset('work');\n draw();\n})();"
}
```How it works
Your Techo holds the days; this library holds the plan behind them. The year’s goals sit on one chart, your habits shade in day by day, every pen and ink has its notes, and the spreads you want to try wait on cards until a quiet Sunday.