spaces: a page can have a cover, and a view can be a gallery - #392
Open
nyblnet wants to merge 3 commits into
Open
spaces: a page can have a cover, and a view can be a gallery#392nyblnet wants to merge 3 commits into
nyblnet wants to merge 3 commits into
Conversation
Stage two of the Bases work. Stage one let a page carry properties; this makes
a view that can hold those pages, and a shape to read them in.
A view meant ONE thing — every page carrying a `status` — which is why the
tracker worked and nothing else did. Two selectors now, and deliberately only
two. `has` pairs with a property somebody just invented: the vocabulary is flat
and a page carries only the fields it uses, so "has an Author" IS "is a book".
`under` is what people reach for anyway, because nesting is how a space is
already organised. A selector language grows without limit and can never
shrink; every operator ships permanently into files on other disks.
ABSENT MEANS ISSUES, and that is the compatibility rule rather than a default:
every view block written before `source` existed carries none and must keep
showing the backlog forever. A view set back to Issues is byte-identical to one
that never moved — the rule `filter` already follows, and the reason cycling the
layout past table CLEARS the key instead of storing "board".
TABLE is the shape a base is usually looked at in and the one this app did not
have. Its columns are the fields the ROWS ACTUALLY CARRY, in the schema order:
a table of books must not grow an Estimate column because the vocabulary
happens to contain one. Select values render through their option, so a cell
reads "Read" with its colour rather than the id the model stores. It scrolls
inside its own box — a wide table must never scroll the page sideways.
Built the article use case end to end on a real build to check it: three book
pages with Author and Shelf, a view sourced by `has: author`. The table came
out with exactly Page/Author/Shelf, no Priority or Estimate; the layout button
cycled to a board; the group picker offered Shelf — the user-defined select,
because grouping only offers fields with options — and the board came out To
read / Reading / Read with one card each. That is a Kanban reading list built
from fields that did not exist in the app an hour ago.
Three sabotages, each failing the assertion that names it: a sourceless view
returning nothing, table columns taken from the vocabulary instead of the rows,
and cycling storing "board" rather than clearing.
i18n: "Show as a {what}" was written and then withdrawn. It reads fine in
English and breaks in half the catalogs, where the article and adjective agree
with the noun gender — die Liste against der Tafel, une liste against un
tableau. Three whole sentences instead, two of which already existed.
+3,028 B, reported by the tracker rather than blocked.
Stage three of the Bases work, on top of #389. A base that can hold books and films had a table to read them in and nothing that made them LOOK like books and films. This is the picture, and the shape that shows it. A COVER IS NEVER A URL. `Page.cover` takes `asset:<key>` or a `data:` URI and nothing else, for the same reason `icon` is one emoji and never an address: opening a document must not touch the network (PLATFORM §1), and a cover is the field most likely to tempt somebody into a link, because that is how every hosted notes app stores one. The refusal is one function — `coverSrc` — that the page and the gallery card both go through. A file arriving with a remote cover KEEPS the field (additivity) and renders nothing; validate() names it rather than leaving the silence. THE SAME PIPELINE AN IMAGE BLOCK USES, not a second one: prepareImage downscales the phone photo before it travels, internAsset content-addresses the bytes so two pages with one cover store it once, and IMAGE_EMBED_BUDGET asks the same question at the same size. Hashing and interning happen BEFORE the commit, so a cover is one undo step. A cover is also the only asset reference in the format that is NOT on a block, and every sweep in the app was written as a loop over blocks. Missed, the failure is silent and specific: the readout calls every cover an orphan and offers to delete it, and a page grafted into another space arrives with a cover pointing at nothing. `pageAssetKeys` is the one answer, and orphanAssets, stats(), validate(), extractSpace and planGraft all ask it. The cover is FULL-BLEED — it belongs to the page, not to the prose, so it ignores the measure and escapes the reading column's padding. That padding is now `--sp-pad-x/y` rather than four literals scattered through styles.css: a hard-coded -44px would be wrong on a phone, in reading view and on paper, all silently. The page's own icon rides up over the picture's lower edge, which is what makes it read as a cover rather than as a banner. Lifting the icon rather than the TITLE is deliberate: a line of text over an arbitrary photograph needs a scrim, contrast rules and a colour that works over every possible image; a 58px disc in the page background does not. GALLERY joins the layout cycle: board → list → table → gallery → board, where board is still the ABSENT key, so a view cycled all the way round is byte-identical to one nobody touched. Cards show every field the page carries, not the tracker's four — a gallery of books showing Priority and Estimate and not the Author would be the board wearing a different shape. A page with no cover gets a tinted panel on a hue derived from its id, carrying its icon or its first character, so a gallery nobody has given pictures to still reads as a set of distinct things. Verified in the built shell, not inferred: covers set through the model render full-bleed; the layout button cycled to `layout: 'gallery'` in the DOCUMENT; Remove deleted the key (`'cover' in page` false) and undo put it back; a remote cover rendered nothing, loaded no remote image, and validate() reported it. Every new assertion in scripts/test-spaces-model.ts was watched to fail with the thing it guards deliberately broken.
Rebase first: #390 (table edited in place), #393 (graph), #396 (remote-asset gate), #395 (canvas) and #375 (name sweep) all landed underneath this branch, conflicting on seven paths. render.ts hunk 1 was the one to get right — ours imported coverSrc, main imported loadsRemotely/assetValue, and taking either side whole would have dropped a feature or the security fix. Composed by hand; the rest kept both, per PARALLEL-WORK section 3. packed.ts regenerated, never hand-merged. Then the review findings, each measured before and after: THE LIFT NEVER HAPPENED. .sp-cover carried margin-bottom:26px and the inner column margin-top:-30px — adjacent siblings, so the margins COLLAPSED to -4px. Measured: cover bottom 317, icon top 313. The disc sat tangent to the picture while the comment beside it described an arrangement that was not happening. The cover gives up its bottom margin; only one of the two can own that space. Now +26px of the disc over the photograph. AND IN THE READING VIEW THE TITLE LANDED ON THE PHOTOGRAPH. No icon renders there, so the -30px lift was absorbed by the title instead: cover bottom 197, title top 193 — unscrimmed text on an arbitrary image, the one thing that comment says the design exists to avoid. It survived over a dark sky and would not over a light one. The lift is the ICON'S now, gated on :has, and the title clears the photo by 26px. A NEAR-WHITE COVER READ AS A HOLE — same colour as the page, so a scan or a snow shot looked like a failed image. An inset hairline on the cover and on the gallery shot, invisible on every other picture. FREE HUE WAS NOT DISTINCT. Measured 61 and 54 in one grid (the same dirty chartreuse) and five cards as peach/pink/pink/lavender/lavender. Eight curated stops instead, and the mark goes from --muted to --ink-2, which was the same value as its own tint in the dark theme. A FOUR-LINE TITLE inflated its grid row and left six neighbours carrying 60px of nothing. Clamped to two. The layout cycle reached Object.prototype through a bare `in`: a hand-authored layout:"toString" rendered a native function as the button label and stored a FUNCTION into the model. Object.hasOwn now, the same guard resolveSrc has. I18N, THE REAL FIND. t(LAYOUT_WORD[here]) — the extractor sweeps LITERALS, so no catalog ever learned those strings existed, and coverage still read 100% because the packer counts what it swept. "Board", "List" and "Show as a list" were WRITTEN in de.ts and absent from packed.ts: translations already done, dropped on the floor. Chasing it found a second instance I shipped in #388 — FIELD_TYPE_LABEL put five of six property types in NO catalog at all. Both are chosen at the call site now; eight new keys across eight locales. So the class is the assertion now. A rig check fails on any t() reading a map the extractor cannot sweep, with the one indirection it DOES implement (label: and hint: out of blocks.ts) named rather than assumed. Building it produced two false positives worth recording: it flagged its own doc comments until it stripped comments, and a literal on the line after t( because \s* backtracks and let the character class eat the newline. Verified by sabotage: labels back behind a map fails; a named map in fields.ts fails; the cover lift and the reading-view gate measured in a real build.
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.
Stage three of the Bases work, stacked on #389 (
--base spaces-bases-views). Stage one gave a page properties, stage two gave a view a set of pages and a table to read them in. A base that can hold books and films still had nothing that made them look like books and films. This is the picture, and the shape that shows it.A cover is never a URL
Page.covertakesasset:<key>or adata:URI and nothing else — for the same reasoniconis one emoji and never an address. Opening a document must not touch the network (PLATFORM §1), and a cover is the field most likely to tempt somebody into a link, because that is how every hosted notes app stores one.The refusal is one function,
coverSrc, that the page and the gallery card both go through. A file arriving with a remote cover keeps the field (additivity) and renders nothing;validate()names it rather than leaving the silence:The same pipeline an image block uses
Not a second policy.
prepareImagedownscales the phone photo before it travels,internAssetcontent-addresses the bytes so two pages with one cover store it once, andIMAGE_EMBED_BUDGETasks the same 4MB question at the same size. Hashing and interning happen before the commit, so a cover is one undo step.A cover is the only asset reference that is not on a block
Every asset sweep in the app was written as a loop over
page.blocks. Missed, the failure is silent and specific: the readout calls every cover an orphan and offers to delete it, and a page grafted into another space arrives with a cover pointing at nothing.pageAssetKeysis the one answer, andorphanAssets,stats(),validate(),extractSpaceandplanGraftall ask it.How it looks
The cover is full-bleed — it belongs to the page, not to the prose, so it ignores the measure and escapes the reading column's padding. That padding is now
--sp-pad-x/yrather than four literals scattered throughstyles.css: a hard-coded-44pxwould be wrong on a phone, in reading view and on paper, all silently.The page's own icon rides up over the picture's lower edge, in a 58px disc of the page background. Lifting the icon rather than the title is deliberate — a line of text over an arbitrary photograph needs a scrim, contrast rules and a colour that works over every possible image; a disc does not.
Gallery
Fourth shape in the layout cycle: board → list → table → gallery → board, where board is still the absent key, so a view cycled all the way round is byte-identical to one nobody touched.
auto-fillwith a minimum card width, never a column count: three across with both panels open, five on a wide screen, nobody configures anythingMeasured in the built shell, not inferred
Drove
dist-single/Bento_Spaces.bento.htmlin a browser. File pickers cannot be driven from the pane, so covers were set through the model directly and everything else was the real UI.view.layout === 'gallery'in the document, button reads Gallery'cover' in page→ false (the key is deleted, not blanked), picture gone, button back to Choose…data:images in the document, field kept in the model,validate()reportedremote-coverrgb(27,32,39)onrgb(46,53,63)hairline, tinted panel stays a tint (hslwith alpha), mark in--mutedVerified by sabotage
Every new assertion in
scripts/test-spaces-model.tswas watched to fail with the thing it guards deliberately broken:coverSrcstops refusing a remote cover → 2 failcoverSrcnever returns a picture → 2 failpageAssetKeysreturns[]→ 5 failorphanAssetsforgets covers → 1 failcoverSrc→ 1 fail""→ 1 fail"board"instead of clearing → 1 failnode scripts/test-spaces.mjs— all eight rigs pass (model 826/826). i18n 567 × 8 locales, all complete; three new keys (Cover,Choose…,A picture across the top of this page), each a literal at itst()call site.Byte cost
263,602 B, +5,148 B over the reference — about 2%, for the format field, the picker, the gallery renderer and its CSS. No ceiling on spaces; the rig reports the drift.
Not in this PR