Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6390,3 +6390,60 @@ chance to run and it is cheap. Reconciliation for this cycle: 41 commits, 40
mapped, 1 correctly absent, run by bento-team-slides.

Claude-Session: https://claude.ai/code/session_01Jcfdy8A69nonyATtm8vRy8

## 2026-09-09 — a calendar is ONE layout with two shapes, and its date is a rule

bento/spaces gained a fifth view layout, `calendar`. Three choices in it are
the kind a later session would otherwise re-open and settle differently.

**A month grid and a timeline are ONE entry in the layout cycle, not two.**
They are not peers of board/list/table/gallery. Those four answer four
different questions; these two answer one question — *when?* — at two
densities, and both densities are real in this app: journal entries are daily
and dense, 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 the shape of a week. So both ship, behind ONE cycle entry,
with the choice on a second button that appears only while the calendar is on.

The reason it is not six entries is that the layout control is a CYCLE, and a
cycle's cost is linear: every added shape is one more click for everybody who
did not want it, in both directions. The precedent for the alternative was
already in the file — `groupBy` is a board-only parameter with its own button,
hidden for every other shape — so this is the existing answer to "one shape,
one parameter" rather than a new mechanism. `span` is a STRING (`timeline`,
absent = month) and not a boolean, because `week` and `year` are the obvious
next two and a boolean cannot be widened afterwards.

**Which date a page sits on is FIXED and stated, not configured.** The rule is:
`page.journal` when it is a real ISO date, else the first `date`-typed field in
schema order the page carries a real value for, else no date. A `dateBy` key on
the view would be a permanent format field bought to express a preference
nobody has asked for; the format's own rule is that every key ships forever
into files on other people's disks. What the UI owes instead is HONESTY, so the
view prints the rule above the grid.

**A page with no date is SHOWN, in its own bucket.** This is the half that
would be tempting to skip. A calendar that silently holds fewer pages than the
count beside its own title is a view lying about what it contains — and the
pages it drops are precisely the ones somebody forgot to date, which is the
thing they most need to see. Same reasoning for a digit-shaped non-date
(`2026-13-99`): it is undated, never rolled forward into a real day it is not,
because every Date-based formatter will do that silently and confidently.

**Dates are built from COMPONENTS and formatted through Intl, never parsed.**
`new Date('2026-01-01')` is UTC midnight by spec and is the previous day for
every reader west of Greenwich; journal.ts already carried this argument and
the calendar is where it bites hardest, because a whole grid shifts by one
column. 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.
Month names, weekday names and the reader's FIRST DAY OF THE WEEK all come from
`Intl` — the last of those shifts the grid rather than relabelling it, so a
hand-written table gets the columns wrong in half the world as well as being
untranslatable (the extractor sweeps `t()` literals, so `t(MONTHS[m])` reaches
no catalog while the packer reports 100%).

Measured in a built shell rather than asserted: February 2026 draws 28 cells in
four rendered rows in a Sunday-first locale and 35 in five in a Monday-first
one, August 2026 draws 42 in six, September 35 in five. The cell count is
derived from the month AND the reader; a fixed 35 silently loses the last days
of a six-week month, which is the classic failure of every calendar grid.
18 changes: 17 additions & 1 deletion docs/spaces-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ unique ids the first time.
| `pagelink` | `page` | a card linking to another page |
| `link` | `url`, `title`, `desc`, `site`, `icon`, `image`, `html` | a card linking OUT of the space — see **Link cards** |
| `prop` | `key`, `value`, `html` | one field value — see **The issue tracker** |
| `view` | `layout`, `groupBy`, `html` | a board or list of this space's issues |
| `view` | `layout`, `span`, `groupBy`, `html` | a board, list, table, gallery or calendar of this space's pages |

`type` is a **string**, not a closed set: an unknown type survives a round trip
and renders its `html` as a fallback. Properties are **flat on the block** —
Expand Down Expand Up @@ -266,6 +266,22 @@ list**: `{ "type": "view", "layout": "board", "groupBy": "status",
"html": "Issues by status" }`. Put it on a page of its own — a page carrying a
view is laid out wide.

`layout` is one of `list`, `table`, `gallery`, `calendar` — **or absent, which
means a board.** Never write `"layout": "board"`: absence is what every view
written before layouts existed carries, and a stored `"board"` is a byte
difference that says nothing.

A **calendar** lays the view's pages out by date, and has two shapes: a month
grid (`span` absent) and a chronological timeline (`span: "timeline"`, newest
first). Which date a page sits on is a **fixed rule, not a setting** — its
`journal` date if it has one, otherwise the first `date`-typed field in the
schema it carries a real `YYYY-MM-DD` value for. A page the rule finds no date
for is listed under "No date" rather than dropped, and a value that is
digit-shaped but not a real day (`2026-13-99`) counts as no date rather than
being rolled into some other day. Month names, weekday names and the first day
of the week come from the reader's locale at display time; nothing formatted is
ever stored.

**Not in this format, deliberately**: teams, per-user permissions,
notifications, automation. The file is the team boundary and the capability.

Expand Down
Loading
Loading