Skip to content

Editor: reach save-as from a phone; tray: signing config and README corrections - #156

Merged
nyblnet merged 3 commits into
mainfrom
tray-phone-saveas
Jul 27, 2026
Merged

Editor: reach save-as from a phone; tray: signing config and README corrections#156
nyblnet merged 3 commits into
mainfrom
tray-phone-saveas

Conversation

@nyblnet

@nyblnet nyblnet commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Two commits: the phone save-as gap (the functional one), and the tray signing wiring plus a README that had drifted into stating things that are no longer true.

Save-as was unreachable on a phone

The Save split button's caret is hidden on a phone — it does not fit beside a 44px target — which left Save a copy, Duplicate as new deck, every password action, Version history and the whole JSON round-trip with no route at all. The CSS said so in a KNOWN GAP comment; this closes it.

They are file operations, so they now sit at the bottom of ⋯ ("everything occasional") rather than behind a second nested dropdown, which on glass is worse than a long list.

One function builds the list into whichever container asks for it, so the phone copy cannot drift from the desktop one. It is rebuilt on every open because it is state-dependent — an encrypted file offers Change/Remove password where a plain one offers Encrypt — and torn down by tag, never by clearing the container: the buttons beside it are the real toolbar nodes on loan from the bar, and clearing would destroy them.

Three defects found by opening the menu, not by trusting the build

1. ⋯ refused to open at all — mine. Object.assign(el, {dataset: {...}}) throws, because dataset is a getter-only accessor, and the throw landed before the menu's own toggle. It type-checks perfectly, so tsc was silent and the symptom pointed nowhere near the cause.

2. ⋯ came up with the language list already unrolled inside it — pre-existing. .ed-dropdown.open .ed-menu is a DESCENDANT selector, and phone chrome demotes whole dropdown widgets (Share, Language) into that menu, so opening ⋯ displayed their menus too. Now >; every .ed-menu is appended straight onto its own .ed-dropdown, so the scope is exact.

3. Language sat there as a bare globe while every neighbour was captioned — also pre-existing. The borrow-the-tooltip labeller asked the demoted node whether it already had a label, and a dropdown WRAPPER always answers yes: it contains the menu it hides, and that menu is full of spans. It asks the trigger now.

Separately, these menus are start-anchored but open from buttons in the right half of a 402px bar, so they grew away from the screen — ⋯ sits at x≈374 and even the old six short rows were partly cut off. End-anchored now, width and height bounded, and scrollable, since a dropdown is absolutely positioned and nothing else could scroll it back into reach.

Verified

At 402x760 in the built single file: all 15 rows present and labelled, menu box 196..396 of 402 and inside the viewport vertically, an item fires its action and closes the menu, nested submenus stay display:none until opened. Then at 1280x800: caret back, same list, 0 stray phone rows, 0 stray borrowed labels, Help back in the bar. Conformance gate, test-preview 18/18, test-packs 17/17.

Signing, without committing anybody's Team ID

The simulator needs none — it signs ad-hoc, which is why every build so far has just worked. A device needs a team, so the project sets CODE_SIGN_STYLE and takes DEVELOPMENT_TEAM from the environment at generation time:

BENTO_TEAM_ID=ABCDE12345 xcodegen

A Team ID identifies a person or company, and the .xcodeproj carrying it is generated and gitignored. Verified both ways: set, the setting appears; unset, DEVELOPMENT_TEAM is absent entirely and the simulator build still succeeds.

The launch screen needed nothing

Recorded so it is not "fixed" later. UILaunchScreen is already declared (empty dict = adapt to the system), and holding a cold start with --wait-for-debugger shows a blank screen that is white in light and black in dark — already matching the document browser it hands over to. A branded splash would ADD a flash on the way there, and the HIG asks for a launch screen resembling the first screen rather than an ad.

README corrections

It had drifted into claims that are no longer true, which is worse than saying nothing:

  • "the app has never been linked, launched or run" — it has now been built, installed and driven on iPhone and iPad simulators.
  • a nav bar with a Documents button that "auto-hides in landscape" — that bar is gone in both orientations, replaced by the floating chevron, and hidesBarsWhenVerticallyCompact never fired for a modal nav controller anyway.
  • "presenting takes real element fullscreen" — element fullscreen is declined on every device now. The section explaining why replaces it, with the measured letterboxing.

State: scaffold, not shippable is renamed to what is true: it runs, it is unsigned, and nothing is verified on real hardware — no signing, no provisioning, no file providers, and the share-sheet and AirDrop routes in are still untried.

nyblnet added 3 commits July 27, 2026 19:28
…enus

On a phone the Save split button's caret is hidden — it does not fit beside
a 44px target — which left Save a copy, Duplicate as new deck, every
password action, Version history and the whole JSON round-trip with NO route
at all. The CSS said so in a KNOWN GAP comment. They are file operations, so
they now sit at the bottom of ⋯ ("everything occasional") rather than behind
a second nested dropdown, which on glass is worse than a long list.

The list is built by one function into whichever container asks for it, so
the phone copy cannot drift from the desktop one, and it is rebuilt on every
open because it is state-dependent: an encrypted file offers Change/Remove
password where a plain one offers Encrypt. The phone copy is torn down BY
TAG, never by clearing the container — the buttons beside it are the real
toolbar nodes on loan from the bar, and clearing would destroy them.

Three defects found by opening the menu rather than by trusting the build:

1. ⋯ REFUSED TO OPEN AT ALL, and that one was mine. `Object.assign(el,
   {dataset: {...}})` throws — dataset is a getter-only accessor — and the
   throw landed before the menu's own toggle. It type-checks perfectly, so
   tsc was silent and the symptom pointed nowhere near the cause.

2. ⋯ CAME UP WITH THE LANGUAGE LIST ALREADY UNROLLED INSIDE IT. Pre-existing:
   `.ed-dropdown.open .ed-menu` is a DESCENDANT selector, and phone chrome
   demotes whole dropdown widgets (Share, Language) into that menu, so
   opening ⋯ displayed their menus too. Now `>`; every .ed-menu is appended
   straight onto its own .ed-dropdown, so the scope is exact.

3. LANGUAGE SAT THERE AS A BARE GLOBE while every neighbour was captioned.
   Also pre-existing: the borrow-the-tooltip labeller asked the demoted node
   whether it already had a label, and a dropdown WRAPPER always answers yes
   — it contains the menu it hides, and that menu is full of spans. It asks
   the trigger now.

These menus also open from buttons in the right half of a 402px bar while
being start-anchored, so they grew away from the screen: ⋯ sits at x≈374 and
even the old six short rows were partly cut off. End-anchored now, width and
height bounded, and scrollable — a dropdown is absolutely positioned, so
nothing else could scroll it back into reach.

Verified at 402x760 in the built single file: all 15 rows present and
labelled, menu box 196..396 of 402 and inside the viewport vertically, an
item fires its action and closes the menu, nested submenus stay display:none
until opened. Then at 1280x800: caret back, same list, 0 stray phone rows,
0 stray borrowed labels, Help back in the bar. Gate, test-preview 18/18 and
test-packs 17/17 all green.
…EADME

SIGNING. The simulator needs none — it signs ad-hoc, which is why every build
so far has just worked and why this never surfaced. A device needs a team, so
the project now sets CODE_SIGN_STYLE and takes DEVELOPMENT_TEAM from the
ENVIRONMENT at generation time:

    BENTO_TEAM_ID=ABCDE12345 xcodegen

A Team ID identifies a person or company, and the .xcodeproj that carries it
is generated and gitignored, so nothing personal is committed. Verified both
ways: with the variable set the setting appears in the project, and with it
unset DEVELOPMENT_TEAM is absent entirely and the simulator build still
succeeds.

THE LAUNCH SCREEN NEEDED NOTHING, which is worth recording so it is not
"fixed" later. UILaunchScreen is already declared (an empty dict = adapt to
the system), and sampling the first frames of a cold start shows black on
black in dark mode and white on white in light — it already matches the
document browser it hands over to. A branded splash would ADD a flash on the
way to that browser, and the HIG asks for a launch screen resembling the
first screen rather than an ad.

THE README HAD DRIFTED into stating things that are no longer true, which is
worse than saying nothing:

- "the app has never been linked, launched or run" — it has now been built,
  installed and driven on iPhone and iPad simulators.
- a nav bar with a Documents button that "auto-hides in landscape" — that bar
  is gone in both orientations, replaced by the floating chevron, and
  hidesBarsWhenVerticallyCompact never fired for a modal nav controller
  anyway.
- "presenting takes real element fullscreen" — element fullscreen is now
  declined on every device, and the section explaining why replaces it,
  with the measured letterboxing.

The State section is renamed to what is actually true: it runs, it is
unsigned, and nothing has been verified on real hardware — no signing, no
provisioning, no file providers, and the share-sheet and AirDrop routes in
are still untried.
…o zero

Pressing Save in Bento Tray destroyed the document. The file on disk went to
ZERO BYTES while the deck carried on rendering perfectly in the editor, so
nothing looked wrong until you reopened it.

The FSA polyfill's write() accepted two shapes: a plain payload, or the
spec's params form {type:'write'|'seek'|'truncate', data, position, size}.
It told them apart with `typeof data.type === 'string'`.

A Blob is an object, and its `type` is a string — its MIME type. So

    new Blob([html], { type: 'text/html' })

which is exactly what kernel/src/save.ts hands to write(), parsed as a
params object. None of the three branches matched 'text/html', so it fell
through to put(await asText(data.data)) — and `.data` is undefined on a
Blob, which asText() maps to ''. The polyfill then closed with an empty
buffer and the host wrote zero bytes over the user's file.

Fixed by excluding Blob explicitly and matching the three type values
exactly, which is what the spec actually defines.

WHY THIS SURVIVED SO LONG. The save contract was verified against the real
Bento build in a browser "with the native side emulated" — the emulation
took the text directly and never exercised this branch. Every later check
confirmed a file EXISTED and was the right size, but the sizes that looked
right were the untouched 642KB seed: a document that had never been saved
was indistinguishable, at a glance, from one saved correctly. Only saving a
document and then measuring the bytes on disk catches this.

Verified: the write payload goes 0 -> 880,346 chars, the file on disk 0 ->
881,005 bytes, and the document Bento Tray saved parses as bento/slides
with 17 slides and passes scripts/shell-gate.mjs — the same conformance
gate release.mjs runs before signing.
@nyblnet
nyblnet merged commit 44dfba6 into main Jul 27, 2026
1 check passed
juntower pushed a commit to juntower/bento that referenced this pull request Jul 28, 2026
…he rest

docs/DECISIONS.md gets a NEW entry superseding the 2026-07-26 one rather than
an edit to it, which is what that file's own rules ask for. It supersedes
exactly one point — where the preview lives — and says so, because the rest
of that decision still holds: written at save time, replaced never appended,
PREVIEW_BUDGET tiers, and above all encrypted decks get no preview.

The entry also records the two dead ends, so the next person does not spend
an evening rediscovering them: a QLThumbnailProvider extension registers
correctly and never launches, because iOS owns public.html and will not
consult third-party extensions for a type it already handles; and
NSURLThumbnailDictionaryKey via UIDocument.fileAttributesToWrite is accepted
and then silently dropped for local files, which you can only see by
inspecting the xattrs afterwards and finding only com.apple.lastuseddate.

CLAUDE.md described the <noscript> design as current. It now describes what
ships, including the probe that settled it (red default / green from inline
script / blue in noscript renders RED on iOS) and the rule that nothing may
sit between the preview and its remover.

CHANGELOG.md carried a "known gap: the save-as menu is unreachable on a
phone" that nyblnet#156 closed. A changelog that contradicts the code is worse than
one that says less, so it now describes where that list actually lives.
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