spaces: a view can be a calendar - #429
Open
nyblnet wants to merge 1 commit into
Open
Conversation
The fifth shape on the one layout button — board, list, table, gallery,
calendar — and the one that answers "when".
ONE ENTRY IN THE CYCLE, TWO SHAPES BEHIND IT. A month grid and a
chronological timeline are both needed (journals are dense and daily; a
reading list's dates are sparse across years, where a month grid is
thirty-six mostly-empty pages), but they answer ONE question at two
densities rather than being peers of the other four. The layout control is
a cycle, whose cost is linear — a sixth entry is one more click for
everybody who did not want it — so the choice is a second button that
appears only while the calendar is on, exactly as groupBy is a board-only
parameter with its own button.
WHICH DATE IS A RULE, NOT A SETTING, and the view prints it above the grid:
page.journal when it is a real ISO date, else the first date-typed field in
schema order carrying a real value, else no date. Pages the rule cannot
date are listed under "No date" rather than dropped — a calendar holding
fewer pages than the count beside its own title is a view lying about what
it contains, and those are the pages somebody forgot to date. A
digit-shaped non-date (2026-13-99) is undated, never rolled forward into
some other real day.
Dates are built from components and formatted through Intl, never parsed:
new Date('2026-01-01') is UTC midnight and shifts the whole grid one column
west of Greenwich. Month names, weekday names and the reader's first day of
the week all come from Intl — the last shifts the grid rather than
relabelling it. The one correct UTC is subtracting two calendar dates,
where Date.UTC makes a day exactly a day across a DST boundary.
Additive both ways, verified against a build that has never heard of
either key: it renders the board and round-trips layout:"calendar" and
span:"timeline" untouched. board and month stay the ABSENT keys, so a view
cycled all the way round is byte-identical to one nobody touched.
The layout logic lives in its own file so the shared files take six lines
between them; VIEW_LAYOUTS gains one entry and both callers follow it.
Rig: 109 new behavioural checks in test-spaces-model.ts (1042 total, up
from 933), run under six timezones including Pacific/Niue and
Pacific/Kiritimati. Measured in a built shell: February 2026 draws 28 cells
in four rendered rows Sunday-first and 35 in five Monday-first, August 2026
draws 42 in six, September 35 in five.
Build size
Updated: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A fifth view layout in bento/spaces: calendar, on the one layout button —
board → list → table → gallery → calendar → board.
One layout or two? One, with two shapes behind it
A month grid and a chronological timeline are genuinely different pictures, and
this app holds both kinds of dated page: journal entries are dense and daily, a
reading list's dates are sparse across years. A month grid is useless on the
second (thirty-six mostly-empty months to page through) and a timeline cannot
show you the shape of a week. Both are needed.
They are still not peers of board/list/table/gallery. Those four answer four
different questions; these two answer ONE question — when? — at two densities.
And the layout control is a cycle, whose cost is linear: a sixth entry is one
more click for everybody who did not want it, in both directions. So the cycle
gains one entry, and the month/timeline choice is a second button that appears
only while the calendar is on — which is the pattern already in the file, since
groupByis a board-only parameter with its own button that the renderer hidesfor every other shape.
spanis a string (timeline; absent = month), not a boolean:weekandyearare the obvious next two and a boolean cannot be widened afterwards.
Which date, and what happens to pages that have none
A fixed rule, stated in the UI rather than configured:
page.journal, when it is a real ISO date.date-typed field in schema order that the page carriesa real value for — not "the first date field", so a page with an empty Due
and a filled Published is dated by Published rather than dropped for carrying
the wrong empty box.
A
dateBykey would be a permanent format field bought for a preference nobodyhas asked for. What the view owes instead is honesty, so it prints the rule
above the grid: "Dated by the journal date, then Due."
The undated bucket is the half it would be tempting to skip. A calendar silently
holding fewer pages than the count beside its own title is a view lying about
what it contains, and the pages it drops are exactly the ones somebody forgot to
date. Same reasoning for
2026-13-99: digit-shaped, not a day, so it is undatedrather than rolled confidently forward into 9 January 2027.
Timezones and locale
Every date is built from components in the reader's timezone and formatted
through Intl at display time.
new Date('2026-01-01')is UTC midnight byspec and shifts the whole grid one column for every reader west of Greenwich.
The one place UTC is correct is subtracting two calendar dates, where
Date.UTCis what makes a day exactly a day across a daylight-saving boundary — asserted,
not commented, for the Berlin and Los Angeles DST dates in every timezone the
rig runs under.
Month names, weekday names and which day the week starts on all come from
Intl.Locale/Intl.DateTimeFormat. The last of those shifts the grid ratherthan relabelling it, so a hand-written table would put every entry in the wrong
column across half the world — as well as being untranslatable, since the
extractor sweeps
t()literals andt(MONTHS[m])reaches no catalog while thepacker reports 100%.
Format additivity
boardandmonthstay the absent keys. Measured in the built shell: froma pristine view block, five clicks of the layout button and a there-and-back on
the span toggle both return
{"id":"v1","type":"view","html":"Everything"}—byte-identical.
sp-cal-grid). It renders the full board with all four issues, its layoutbutton reads "Board", and both
layout:"calendar"andspan:"timeline"survive the round trip into the serialized file untouched.
Verification
+109 behavioural checks. Every one imports and runs the function; there are
no source greps in the new section, because this zone has measured twice that a
grep passes straight through a live regression.
Real browser (built shell copied to a uniquely-named file, served over
http://127.0.0.1, marker-checked, measured withgetBoundingClientRect):Rows counted by grouping cells on their measured
top, seven per row every time,and the height grows 77px per extra week — so the rows are really drawn, not
overlapping. February 2026 is the rare exactly-four-week grid and is the case a
hard-coded 35 or 42 gets wrong in opposite directions.
Also measured live:
2026-09-09highlighted in exactly one cell; adjacent-monthentries (31 Aug, 1 Oct) drawn in their trailing/leading cells; 7 dated + 2
undated = the view's own count of 9; the timeline's 8 day headings newest-first;
and in
jathe whole surface localised — カレンダー / 月 / 前の月 / 日付なし,month
2026年9月and weekdays 日月火水木金土 from Intl, week starting Sunday.Sabotage runs
Every new assertion was broken, watched fail, and restored.
new Date(iso)UTC parse instead of components3/5rows accounted for)__proto__)isISOround trip removed fromdateOfcalendarremoved fromVIEW_LAYOUTSspanOfvia a prototype-reachable mapTwo of those runs found the rig thin and it was fixed before shipping, which
is the point of running them:
uniform one-day shift still contains every day of the month exactly once, so
the day-coverage checks all passed — it just puts them in the wrong columns.
Added: every grid must begin on the reader's own first weekday and end the day
before it. Now 16 failures.
javsen-GBcomparison forthe wrong reason — ja is Sunday-first and en-GB Monday-first, so two
identical English arrays come back rotated and compare unequal. Compared as
sets now, where rotation has nowhere to hide.
A third assertion was simply wrong and the rig caught it:
defaultMonthties.20 August → 1 June is 80 days back and → 1 November is 73 days on, so the future
one is nearer; the first draft asserted June because June looks closer on the
page.
Cross-zone touches, flagged
scripts/test-spaces-model.ts— the rig, as briefed. Ops-owned surface.scripts/test-spaces.mjs— the model rig gains the timezone matrix, andPacific/Niuejoins the list (the -11 mirror of Kiritimati's +14; UTC-midnightparsing is the same day at +14 and the previous day at -11, so one of them
alone cannot tell a correct implementation from a broken one — measured above).
docs/spaces-agents.mdanddocs/DECISIONS.md— the docs ride with the code..github/workflows/ci.ymlis deliberately NOT touched. CI still runs themodel rig in one timezone, so the calendar's date arithmetic has multi-TZ
coverage locally and single-TZ coverage in CI until a one-line change to that
step lands. Per the contract that is a queued follow-up rather than part of a
feature PR while the queue is contended; filed on the board, and noted in
test-spaces.mjsbeside the matrix so it cannot be forgotten silently.Filed, not fixed:
renderViewhas TWOlayout === 'table'blocks on mainNoticed while inserting the calendar branch, present on
origin/mainat0100083, out of scope here and filed on the board rather than folded in.spaces/src/render.tshas a table branch at ~1289 thatreturns, and a second,richer one at ~1438 carrying the sortable
data-sort-colcolumn headers.The second is unreachable, so table headers do not sort. The rig's check for it
is a source grep against
editor.ts— the wiring exists, the renderer'ssortable header does not run — which is exactly the "a source grep passes
through a live regression" class this zone has already measured twice.
What I could NOT verify
the same
opts.editable === false ? span : apath the gallery uses, and the@media printrule hides the month arrows, but neither was exercised in abrowser — reasoned from the code, not measured.
Intl.Locale.getWeekInfo. The fallback chain(method →
weekInfoproperty → Monday) is written and typechecked, but thisChrome has the method, so only the first branch actually ran.
(+7.4KB) against a pre-change build of the same app;
test-spaces-size.mjsreports +22,931 B against its longer-standing watermark. spaces has no size
ceiling since spaces: no hard size ceiling, but the number stays in view #378, so this is reported rather than gated.
spanandlayoutare ordinary block properties and becomeordinary LWW registers; no sync rig was run because nothing under
sync/changed.