Skip to content

spaces: a view can be a calendar - #429

Open
nyblnet wants to merge 1 commit into
mainfrom
spaces-calendar
Open

spaces: a view can be a calendar#429
nyblnet wants to merge 1 commit into
mainfrom
spaces-calendar

Conversation

@nyblnet

@nyblnet nyblnet commented Sep 9, 2026

Copy link
Copy Markdown
Owner

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
groupBy is a board-only parameter with its own button that the renderer hides
for every other shape.

span is a string (timeline; absent = month), not a boolean: week and year
are 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:

  1. page.journal, when it is a real ISO date.
  2. otherwise the first date-typed field in schema order that the page carries
    a 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.
  3. otherwise no date — and it is listed under "No date", never dropped.

A dateBy key would be a permanent format field bought for a preference nobody
has 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 undated
rather 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 by
spec 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.UTC
is 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 rather
than 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 and t(MONTHS[m]) reaches no catalog while the
packer reports 100%.

Format additivity

  • board and month stay the absent keys. Measured in the built shell: from
    a 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.
  • Verified against a build that has never heard of the calendar (grepped: no
    sp-cal-grid). It renders the full board with all four issues, its layout
    button reads "Board", and both layout:"calendar" and span:"timeline"
    survive the round trip into the serialized file untouched.

Verification

node scripts/test-spaces.mjs                     all 8 rigs pass
  model    1042/1042  × 6 timezones (UTC, Berlin, Los_Angeles,
                       Kiritimati, Niue, Lord_Howe)   [was 933]
  agent     175/175    journal 45/45 × 6    calc 90/90 × 4
  undo       25/25     invite  34/34        roundtrip 6/6    size ok
TZ=Pacific/Kiritimati node scripts/test-spaces.mjs   all pass
TZ=Pacific/Niue       node scripts/test-spaces.mjs   all pass
cd spaces && node_modules/.bin/tsc --noEmit -p tsconfig.json    clean
npm run build:single                             601KB → 274KB compressed
node ../scripts/shell-gate.mjs …                 conformance gate OK
node scripts/build-spaces-i18n.mjs --check       608 × 8, all complete

+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 with getBoundingClientRect):

month / locale cells rendered rows grid height in-month days
Feb 2026 / en-US (Sun-first) 28 4 338px 28
Sep 2026 / en-US 35 5 415px 30
Aug 2026 / en-US 42 6 492px 31
May 2026 / en-US 42 6 492px 31

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-09 highlighted in exactly one cell; adjacent-month
entries (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 ja the whole surface localised — カレンダー / 月 / 前の月 / 日付なし,
month 2026年9月 and weekdays 日月火水木金土 from Intl, week starting Sunday.

Sabotage runs

Every new assertion was broken, watched fail, and restored.

sabotage result
grid fixed at 35 cells 5 FAIL
new Date(iso) UTC parse instead of components 16 FAIL at Niue and Los_Angeles, 0 at UTC/Berlin/Kiritimati
undated rows dropped silently 2 FAIL (incl. 3/5 rows accounted for)
day index on a plain object instead of a Map 1 FAIL (__proto__)
isISO round trip removed from dateOf 1 FAIL
week hard-coded to start Monday 4 FAIL
hand-written month/weekday tables instead of Intl 3 FAIL
timeline reads oldest first 1 FAIL
calendar removed from VIEW_LAYOUTS 2 FAIL
spanOf via a prototype-reachable map 5 FAIL

Two of those runs found the rig thin and it was fixed before shipping, which
is the point of running them:

  • The UTC-parse sabotage first produced one failure, in half the world. A
    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.
  • The hand-written-weekday sabotage passed the ja vs en-GB comparison for
    the 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: defaultMonth ties.
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, and
    Pacific/Niue joins the list (the -11 mirror of Kiritimati's +14; UTC-midnight
    parsing 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.md and docs/DECISIONS.md — the docs ride with the code.

.github/workflows/ci.yml is deliberately NOT touched. CI still runs the
model 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.mjs beside the matrix so it cannot be forgotten silently.

Filed, not fixed: renderView has TWO layout === 'table' blocks on main

Noticed while inserting the calendar branch, present on origin/main at
0100083, out of scope here and filed on the board rather than folded in.
spaces/src/render.ts has a table branch at ~1289 that returns, and a second,
richer one at ~1438 carrying the sortable data-sort-col column 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's
sortable 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

  • Reading view, print and a locked space. The calendar's entries are built by
    the same opts.editable === false ? span : a path the gallery uses, and the
    @media print rule hides the month arrows, but neither was exercised in a
    browser — reasoned from the code, not measured.
  • Older engines without Intl.Locale.getWeekInfo. The fallback chain
    (method → weekInfo property → Monday) is written and typechecked, but this
    Chrome has the method, so only the first branch actually ran.
  • The byte cost is stated, not budgeted. The shell went 273,990 → 281,385 B
    (+7.4KB) against a pre-change build of the same app; test-spaces-size.mjs
    reports +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.
  • Collaboration. span and layout are ordinary block properties and become
    ordinary LWW registers; no sync rig was run because nothing under sync/
    changed.

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.
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Build size

main (0100083) → spaces-calendar (336b9c6)

app base PR change
bento/slides 677.9 KiB 677.9 KiB 0.0 KiB (0.00%)
bento/spaces 271.3 KiB 274.8 KiB +3.5 KiB (+1.30%)
bento/dash 424.1 KiB 424.1 KiB 0.0 KiB (0.00%)

Updated: 2026-09-09T01:17:55Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant