A page can be saved as a template, and the daily note can start from one - #427
Open
nyblnet wants to merge 1 commit into
Open
A page can be saved as a template, and the daily note can start from one#427nyblnet wants to merge 1 commit into
nyblnet wants to merge 1 commit into
Conversation
…rt from one
Page templates live in doc.templates — a collection, not pages carrying a flag.
doc.pages is enumerated in ~40 places (search, graph, backlinks, issuesOf and
every view, the Markdown export, About, the agent API, print, preview); a
flagged page needs a gate at every one of them and at every surface added
afterwards by someone who has never heard of templates. Measured: with the
collection, search for a template-only word returns 0, the Markdown export
does not contain it, stats.pages equals doc.pages, the sidebar has no row for
it and the graph counts 15 pages, not 16. What it costs is that a template
travels with the FILE and not with a grafted subtree, which is stated in the
changelog and in docs/DECISIONS.md rather than left to be discovered.
doc.journalTemplate names the template new daily notes start from; absent means
a blank entry, so every file written before this behaves as it always did, and
turning the setting off deletes the key.
Tokens expand ONCE, at instantiation, and the model stores the result — no
field system, and the new page is an ordinary page an older build reads the
same way. {{date}}, {{date:iso}}, {{date:short}}, {{date+1:iso}}, {{time}},
{{title}}; anything else stays literal. A journal entry gets the date it is
FOR: backfilling 2026-01-05 writes 2026-01-05 and 2026-01-06, verified in the
browser.
Two prototype guards, because the ids come out of a file somebody mailed you:
templateById scans a list rather than indexing an object, and the date-format
lookup uses Object.hasOwn. Both are pinned by rig assertions that were watched
to fail under deliberate sabotage.
New logic is in spaces/src/templates.ts and spaces/src/templateui.ts; the
editor gains a narrow TemplateHost and four call sites. Rig coverage is in
scripts/test-spaces-model.ts — a cross-zone touch of the shared rig surface,
claimed on the board by path.
+7,368 B compressed (285,137 B vs 277,769 B for the same tree without it).
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.
Page templates for bento/spaces: save a page as a template, start a new page from one, and a template the daily note starts from — the workflow that makes this worth more than it looks, and the one thing a journal entry here could not do.
The format decision, and why
There was no page-template feature.
doc.templateinmodel.tsis the whole-file share export (a file that re-mints itsdocIdon open) and is untouched; a comment now says so beside it, because the name collision is exactly the trap the next reader falls into.Two honest places to put a template. A flagged page, hidden from the sidebar — no new format shape, and it is editable with the page editor for free. Or
doc.templates, a collection nothing that walks pages can see. I took the second, and the reason is a count:doc.pagesis enumerated in roughly forty places — search, the graph, backlinks and the tree (buildIndex),issuesOfand every board/table/gallery view infields.ts, the Markdown export inportable.ts, the About counts, the agent API'spages/stats/outline/validate, the archive list, print, and the file-manager preview. A flagged page needs a gate at every one, and at every surface added afterwards by someone who has never heard of templates. This zone has shipped that class twice (isRemoteapplied before the asset indirection; the source-grep assertions in #392).Measured in the built shell, not assumed — a word placed only in a template:
bento.search('qqzzmarker')0hitsbento.stats().pages14=doc.pages.lengthbento.validate()ok: true, 0 issuesWhat it costs, stated rather than hidden: a template is not a page, so it is not searched, graphed, back-linked, printed or exported — and it travels with the file, not with a page you graft into another space. A page-flag design would have grafted, and would have leaked into all thirteen surfaces above. Written up in
docs/DECISIONS.md.doc.journalTemplatenames the template new daily notes start from. Absent ⇒ a blank entry, so every file written before this behaves as it always did; turning the setting off deletes the key rather than storing a default.Tokens
Expanded once, at instantiation — the model stores the result. bento/slides resolves fields at render time because a footer must re-number when slides move; a template has no such need, and a live field would mean a page whose text changed under its author overnight. So the new page is an ordinary page an older build reads exactly as this one does.
{{date}}{{date:iso}}{{date:short}}{{date+1:iso}}{{date-14:iso}}{{time}}{{title}}. Anything else stays literal, so a{{mustache}}in somebody's prose survives.A journal entry gets the date it is FOR. Backfilling in the browser:
bento.journal('2026-01-05')producedLog for 2026-01-05/Tomorrow is 2026-01-06, not today's date.Untrusted input
Template ids come out of a file someone mailed you.
templateByIdscans a list rather than indexing an object —journalTemplate: "constructor"resolves to nothing.Object.hasOwn, so{{date:constructor}}is literal text rather thanObject's constructor stringified into the reader's page.htmlgoes throughsanitizeInlineat the door, asportable.tsdoes for a graft — the document never holds the thing.Verification
41 new assertions in
scripts/test-spaces-model.ts, all behavioural — every one imports the function and runs it; not a source grep among them. 991/991.Sabotage-verified. Nine deliberate breaks, each watched to fail the exact assertions it should and then restored to green:
Object.hasOwn(DATE_FMT, key)→ bare truthiness{{date:constructor}}/{{date:toString}}checksremoveTemplateleaves a dangling journal settingmakeTemplatespreads the whole pageapplyTemplateignoreskeepTitletemplateByIdindexes a plain record{{date+1:iso}},{{date-14:iso}}docContentKeydrops templatesReal browser, over
http://127.0.0.1:8791on a uniquely-named copy of each build, with a marker check proving the build (build 1:Templates…present in ⋯; build 2: the picker items carry<svg>; build 3:.sp-tpl-namecomputesdisplay: flex). Geometry read withgetBoundingClientRect, never by reading back a style. Verified: the + picker opens below its anchor (top 95 > bottom 89) and inside the viewport at 260px; the dialog is 560×162; instantiation gives fresh ids, a remapped block parent,<b>and an#p/link surviving the real sanitizer; the daily note arrives with its structure and keeps its ISO title; undo takes the whole thing back in one step;serialize()round-tripstemplates+journalTemplatethrough#bento-doc; no console errors.Two defects the browser found that node could not, both fixed here: page icons in this app are usually a name from the stylised set, not an emoji, so the picker read "compass Daily log" (icons now render through a
pageIconsupplied on the host, avoiding an import cycle); and.sp-result-icoisdisplay: flex, so inside a plain span the template name wrapped under its own icon (measured at 17px row height after the fix).Cost, and cross-zone touch
+7,368 B compressed (285,137 B with the feature vs 277,769 B for the same tree without it). The size rig's
+26,683 B since referenceis drift from the stored watermark, most of it other merged work.scripts/test-spaces-model.tsanddocs/DECISIONS.mdare outsidespaces/. The rig coverage rides with the feature per the contract; the shared-surface touch is claimed on the board by path (ops·spaces-templates).spaces/src/styles.cssgains six appended lines andspaces/src/editor.tsgains four call sites plus atemplateHostgetter — everything else is the two new files, so this should merge cleanly against the sibling branches.What I could not verify
doc.templatesis a top-level array and therefore one last-writer-wins register under the CRDT — two people saving a template at the same moment keeps one. Same shape asPage.rowsand documented, not fixed.docs/spaces-agents.mdis unchanged. If templates should be reachable fromwindow.bento, say so and I will add it as a follow-up rather than widening this.