91aeacbe460440da57c11edbd1f71d497e70562f
22 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
325d5c0470 | library: the walk position belongs to the origin, so the app beside the browser carries a reading on instead of listing the roll again | ||
|
|
2f34c36131 |
The room opens folded: the studio's panels and the catalogue's tree both start shut and come back the way they were left
Four asks, one shape: the LIGHT column had a chip that opened the same two
knobs DETAIL & EFFECTS already shows, the WB table was reading as one long
line per preset, the disclosure carets were too small to be the affordance
they are, and both folds — the studio's panels and the catalogue's folder tree
— opened fully every time. What the fold is for is the same in both places: a
column of a dozen controls is a wall to scroll past, and the control being
looked for is named by the head it sits under. So both open folded, and both
remember what was opened.
GRAIN's chip is gone from the LIGHT row. Its strip held a knob `grain` and the
"size" behind it, and both are rows of DETAIL & EFFECTS now — MONOCHROME GRAIN
and GRAIN SIZE — so the chip was a second way to one pair. The strip machinery
stays (`GroupKey 'grain'`, `groupDefs.grain`, `grainInch()`): it is reachable
from nothing today, and taking it out is a diff of its own. What went with the
chip is the `/INCH` readout, which only the strip drew.
The WB presets read as a name on one line and its kelvin under it: the label is
the chip's own label and the kelvin moved from the label into the value slot,
which the grid's rule stacks (`flex-direction: column`, `font-size: 11px`,
`white-space: nowrap`). Seven buttons, 147x39 each, name
box 14px — one line at 11px/1.25 — kelvin at 10px, `scrollWidth` equal to
`clientWidth` on every one of them. The kelvin rides a swatch, so it wears the
fill's own ink rather than the panel's dim grey (`.chip.tinted .val { color:
inherit }`): readableInk already picks #0b0e12 or #ffffff off that fill for
4.5:1, and dimming it a step costs contrast on a mid grey fill. Measured off
the running page: 4.88:1 (CLOUDY) to 5.00:1 (TUNGSTEN), name and kelvin alike.
The panels: `recipescam.develop.open` holds the slugs that are open, comma-
joined, and a slug no panel answers to is dropped on the way in, so a renamed
or removed panel leaves no hole. A visit that touches nothing writes nothing —
the column opens folded from an empty store — and a click on a head writes the
list as it stands. The caret is drawn a size up from the label it sits beside
(17px against an 11px head) and takes full opacity under the pointer, because
a line of uppercase does not read as a control on its own.
The tree: a row's click still opens it and folds it again, but the set it moves
in is inverted — what is *drawn open* rather than what is folded away — which
is what makes the state storable at all (a folded set is the whole tree minus
what is open, and the whole tree is not in that state to be stored). The
column opens with every folder shut, and the remembered set comes back over
it. A first visit under the new rule has no stored set and can still have a
folder remembered from before, so that one case seeds its ancestors: a screen
that opens on a node shrunk away behind a shut row is worse than a
remembered fold. COLLAPSE ALL folds the top-level row too now, which is what
makes it the way back to the column's rest — it was written when a top-level
row could not be folded, and the item is disabled when there is nothing open.
The caret is a leaf on a row with nothing under it, as it was.
`remember.ts` is the two functions both screens now share; Library had its own
copy of them.
The two checks that read these screens were wrong about the new fold and are
right about it now, not the other way round. `library-check` gains the rest
state (`the tree opens with the folders folded away — 1 rows`), walks the
tree a level per click, and reads the visit it left behind off a reload:
`lib-node-CheckRoll:true lib-node-CheckRoll/2026:true .../04:- .../Empty:-`,
with the screen marked on the subfolder. Two of its steps were stale
expectations rather than a fault: the fold-the-tree claim is one click over
four rows once the rows are opened for it, and the folder the screen reopens
on is the last row clicked, not the one clicked before it. `scan-nav-check`
asks the same tree for its deepest folder, so it opens the two rows above it
and folds them back — the reading it is watching does not move with rows.
Verified: `npx tsc --noEmit` clean, `npm run build` clean
(`dist/assets/index-T721P-Nv.js` 711.33 kB, `dist/assets/index-Co_dQD6t.css`
65.69 kB). `scripts/library-check.mjs` — "all checks passed", 55 steps, none
failing — including `the tree opens with the folders folded away — 1 rows,
aria-expanded=false`, `a row draws its own children, not the whole branch — 3
rows`, `and one click folds every row, leaving the frames where they were — 4
rows → 1, lib-node-CheckRoll:false, 2 tiles kept`, `the tree reopens with the
rows that were left open`, `the screen reopens on the folder it was left on —
lib-node-CheckRoll/2026`, `the screen reopens on the frame that was raised`,
and the column `198px, then 198px`. `scripts/scan-nav-check.mjs` — all steps
passed, its tree walk reading
`["lib-node-SlowRoll","lib-node-SlowRoll/2026","lib-node-SlowRoll/2026/04"]`.
In the running page: 5 panel heads and 0 panel bodies at rest with
`recipescam.develop.open` unset, caret `17px` `▸`; the WB table's seven rows
at 147x39 with name 14px one line, kelvin below at 10px, `nowrap`, no
overflow, ink 4.88–5.00:1; the LIGHT chip row `["auto","curve","fix",
"gradient-mask","mono","wb-temp:*","reset-all"]` — no `grp-grain` — with
MONOCHROME GRAIN and GRAIN SIZE still the two rows of DETAIL & EFFECTS; two
heads opened, `wb,effects` stored, and the same two open after a reload with
the other three shut. The deployed bundle was read too: `docker compose build
frontend && docker compose up -d frontend`, and `index-DpYWAhJ3.js` off
`127.0.0.1:8090` carries `recipescam.develop.open` and
`recipescam.library.open` and no `grp-grain`, with the same probe readings
taken against it.
Co-authored-by: PenguinHarness <noreply@penguin.local>
|
||
|
|
53cb1c97e9 |
web: a frame is scored under the picture, and the wall is read through the score
The catalogue could say what a frame was and when it was shot, and nothing about
whether it was any good. A reader who had been through a roll of two thousand
had no way to say "these four", and the thumbnail wall drew every frame the open
folder held in the one order it had: newest shutter first, always.
The frame that is up now carries its own score — five stars under the picture,
and the star the click lands on is the score it gets. The star it already has
takes the score back, so a score that was given by mistake is taken off without
a sixth control. It is the catalogue's own row and nothing on the disk is
touched: the file is not written and not read, the browser only stores one more
number against a frame it already holds, and the number is the reader's.
The wall is read through three of them. The score is the first — four stars and
up is the floor, any rating is the wall as it was — because that is what a
reader who has just been through a roll wants next. The year the shutter fired
in is the second, offered as the years the open folder actually holds and not a
century of empty ones. The hours it fired in are the third, as a from and a to:
16:00 to 18:00 is the afternoon the reader was out, and either end alone is
"from 16:00" or "up to 18:00". All three read the shutter time, on this
machine's own clock — the same reading the frame's date line prints, so the
afternoon is the afternoon and not an offset of it. Beside them, the order: the
newest shutter first, the oldest first, or the most stars first.
What the filters hold is the wall and only the wall. The strip under the picture
is the shelf the open folder is, and a shelf that quietly loses three quarters
of its frames is not a shelf any more; the frame that is up has to stay reachable
while the wall is narrowed around it. That is also why the filters are drawn in
the thumbnail view: they are a way of looking through the shelf, not a property
of the roll, and nothing about them outlives the visit.
Verified:
library-check.mjs — 52 steps, all passed, five of them new. The frame that is
up is scored from the row under it and the catalogue holds star 4 against it
(aria-pressed on the fourth star true); the wall narrows to one tile at 4★,
to none at 5★, and back to three with the rating cleared; the year 2016 holds
the two JPEGs of three and 16:00–18:00 the same two, out of a roll whose
frames are written years apart in different parts of the day (the JPEG in the
afternoon of 15 Feb 2016, the RAW at seven in the morning of 1 Jan 2026);
read by score, the scored frame comes first. Every earlier step still holds,
including the two that count what a reading costs and the stand-in folder's
two frames — the JPEG carrying the GPS tag and the RAW printing
"26.4mm · f/2.8 · 1/320s".
scan-nav-check.mjs, roll-walk-check.mjs — all passed. frontend tsc --noEmit
clean, vite build clean.
ponytail: the score is one number on the frame's own row — no rating table, no
votes, no accounts; the catalogue is a per-browser shelf and so is the score. The
filters live in the thumbnail view's own state, so a reload starts with the whole
shelf again, which is what a filter is for. No text search: a roll of date folders
is three levels deep at most, and the three that a reader actually digs with are
the ones here — add the box when a folder of mixed names makes one worth typing
into.
Co-authored-by: PenguinHarness <noreply@penguin.local>
|
||
|
|
477c71d1f4 |
web: the frame under the preview says where it was shot and what it was shot with
A negative is opened to be looked at, and the two things a photographer reads
off it first are the numbers the camera wrote and where it stood when it wrote
them. The stage gave the name, the folder, the date and the weight of the file,
which is what the catalogue knows; the rest was a trip into the studio.
Both are now under the picture. The numbers are the camera's own — ISO, focal
length, aperture, shutter, frame size — printed in the order a photographer
says them, and every one the file does not carry is left out rather than stood
in for: a Fuji RAF gets no line at all, a Panasonic RW2 gets the glass and the
shutter and no ISO, and a JPEG gets the lot. Where the frame was shot is the
GPS it carries, named by the geocoder when one answers and left as the
coordinates it holds when none does — a naming is a network round trip on a PRO
account, and the numbers do not wait for it, so a refusal or a miss costs the
reader nothing.
What this costs is one read of the frame's first few hundred kilobytes, the
same head a scan hands the parser, and only the frame that is up pays it: the
strip walks past a hundred negatives without reading one of them. The read is
held by the read itself and not by the frame object under it, because the
catalogue is read back on a timer while a scan runs, which hands the screen a
fresh object every few hundred milliseconds — a screen that went by the object
would read the same file again and again for as long as the scan lasts.
The check's stand-in folder had no frame of its own with a GPS tag on it, and
none of the samples carries one, so the check writes one: an APP1 segment
holding a GPS IFD and nothing else, spliced in right after the frame's SOI. It
is deliberately the first APP1 — a JPEG carries one EXIF segment and the
catalogue reads the first, so a file that has a place is a file that spent its
EXIF on the coordinates. That is also what makes the RAW the frame that shows a
spec line, and the two frames now check the two halves of the same feature.
The run counts what a reading costs, and a head is not what it counted before:
it counts the whole file a lane develops apart from the head a parser is handed,
because "the RAW was read" is a claim about the scan and the preview legitimately
reads the same file's head.
Verified:
library-check.mjs — 47 steps, all passed, two of them new. The JPEG off the
check's server carries a GPS tag and the page keeps 16.0544, 108.2022 under
the frame, with no geocoder behind it to name them; the RAW prints
"26.4mm · f/2.8 · 1/320s" — the glass and the shutter it has, no ISO and no
frame size it does not. Every earlier step still holds, including the one
that says a scan does not read a RAW whose size and write time have not
moved: whole reads {}, heads {"P1010256.RW2":1,"P1010256.JPG":2}, the one
RAW head being the frame that is up.
scan-nav-check.mjs, roll-walk-check.mjs — all passed. frontend tsc --noEmit
clean, vite build clean.
ponytail: the place is named by the API's geocoder and nothing else — no map, no
picker, no place a reader can type. The coordinates are what the file carries,
and a file that carries none shows no line, which is the honest answer and the
common case for a phone frame with location off. The line is read for the raised
frame only; a grid of hundreds is a list of names, and a row of ISO numbers
under each tile is not what it is for.
Co-authored-by: PenguinHarness <noreply@penguin.local>
|
||
|
|
753eea0d7b |
web: a reload in the middle of a reading goes on, it does not start over
The reader opens a roll of a few thousand frames and holds Ctrl+Shift+R while
it is being read — over a reading that takes minutes, that is the one thing
they do. What came back was a reading that started again at the top: every
folder listed a second time, the toolbar counter back at zero, the frames that
were already in the catalogue walked over so that their size and their write
time could say they had not moved.
None of that was in the catalogue, because the catalogue only holds the frames
that were read. What was lost with the page was the reading's position: the
folders the walk had been through, the folders still in its queue, and the
frames it had found and not yet read. The catalogue alone can never answer
where a reading was.
The position is now written to session storage as the reading goes — walked,
pending and the frames in hand — and read back on the way in. Session storage
and not local storage, because a position belongs to the tab: the tab that
reloads carries on from the frame it stopped at, and a tab opened beside it
starts a reading of its own. The key goes when the reading finishes, and goes
with the folder when the folder is removed.
The frames in hand are the part worth spelling out. A frame a lane is in the
middle of, and a frame whose row is in the batch and not yet in the catalogue,
are on neither side of the line the position is written on: the catalogue does
not hold them and the walk will not find them again, so both are written into
the queue and read again. They also were counted when they left the queue, and
the count is written with them — else the reloaded reading would count them a
second time. The counter now carries on from where it was instead of restarting
from zero.
One thing was hidden behind the other: the stand-in folder the check hands the
page had no `getFileHandle` and no `getDirectoryHandle`, so the path that asks
the root for the frames a position names threw and answered nothing — and the
reading then walked the roll from the top, exactly the behaviour the check was
meant to catch. The check's folder answers both now, the way the browser's does.
Verified:
library-check.mjs — 45 steps, all passed, two of them new. The reading is
stopped on the frame it is reading (`check.hold`), the tab is reloaded, and
it comes back at "Scanning 3/3 — 0 new…" — the same line it was stopped at,
not a reading starting over — with the position still holding 4 folders
walked, an empty queue and 3 frames in hand. Listing 3 tiles and letting the
key go, the reading finishes the roll: 3 tiles in the strip, the catalogue
written, the position cleared, and every folder listed once
({"2026":1,"CheckRoll":1,"Empty":1,"04":1}) where a reading that started
over lists each of them twice.
scan-nav-check.mjs, roll-walk-check.mjs — all passed. frontend tsc --noEmit
clean, vite build clean.
ponytail: the position is the tab's, so a reload keeps it and a new tab does
not — the tab that reloaded is the one the reader is looking at, and a second
tab reading the same roll from the top costs a walk and no bytes, because a
frame that has not moved is dropped on its size and its time. The position is
written at the batch beat, so a reload reads at most one batch again, and the
frames in hand are read again on purpose: their rows were never stored.
Co-authored-by: PenguinHarness <noreply@penguin.local>
|
||
|
|
fbe9a1bb5b |
web: a roll is read where it was left, and only for the frames that moved
A reader with a large roll ran into three things at once, and they were one
thing: a reading is dropped the moment the tab goes, and the second one over
the same folder took as long as the first.
The catalogue was never emptied — `scanFolder` has no delete anywhere in it —
but it read every frame again. The test that was meant to skip a frame that has
not moved compared the frame's *shutter* time with the file's write time
(`seen.taken === file.lastModified`), two numbers that are equal only by
accident: a still's EXIF date is when the picture was taken, not when the file
was written. So a rescan of any indexed roll went back to the disk for every
file, decoded every frame and wrote it back — which is what reads as "it threw
the index away and started over", and it cost the same minutes the first read
did. A frame that cannot say when it was taken was worse off: it falls back to
the file's own time, so it matched, was skipped forever, and never picked up an
edit.
A row now carries `mtime`, the write time the browser reports for the file, and
a frame is skipped on the same size and the same write time — which is what the
comment over that line always claimed. A row filed before the field existed has
no `mtime` and is read one last time. On the 36-frame roll the bench serves (24
JPEG 8.2MB + 12 RAW 22.6MB, two levels deep):
first reading 5209ms
the same roll again 2887ms 24/36 frames read again
first reading 5320ms
the same roll again 603ms 0/36 frames read again
And the screen starts that reading itself. Opening LIBRARY on a roll whose
reading ended when the app did now walks it again on the way in — and again
when the tab is raised — so the frames it never got to are read with no one
asking, and frames that landed in the folder since are picked up by the same
walk. The scan belongs to the tab and the walk skips what the catalogue already
holds, so a frame that has not moved is a name, a size and a time and nothing
else; the run that does it says nothing in the toolbar, the ring on the row and
the progress line are the report.
The folder menu's commands lead with a mark of their own — fold ▴, rename ✎,
scan ↻, forget ✕, reconnect ⚿, add + — the way the tool rail and the view
switch already do: a column of marks reads at a glance where a block of
uppercase does not.
Verified:
library-check.mjs — 43 steps, all passed, six of them new: the folder menu's
three marks, the row menu's four, the add-only menu's one, the refused
folder's lone reconnect carrying its ⚿, a reading cut short that goes on by
itself (three rows back, and the bytes read are the two frames the
catalogue had lost, not the one it still held), and the folder read again
from its own menu. The stand-in folder now carries `__fake` on both handle
kinds — a folder handle that does not is one the screen cannot ask about
after a reload, which is a folder it offers to reconnect — and the frames
it hands out report one write time instead of `Date.now()` per call, which
is what a real handle does and what a frame is skipped on.
scan-nav-check.mjs — all passed, the row still counting the reading as it
comes rather than the catalogue standing still. roll-walk-check.mjs — all
passed. frontend tsc --noEmit clean.
ponytail: nothing watches the folder, so a roll that changes under a screen
left open is picked up on the next visit or the next raise, not on the change —
a FileSystemObserver when the browsers ship one. A frame is skipped on size and
time alone, so an edit that keeps both is invisible until that frame is read
again; the row's own rescan is the way to ask for exactly that.
Co-authored-by: PenguinHarness <noreply@penguin.local>
|
||
|
|
36cd711302 |
web: the header wears the theme, and the rail says which build it is
Two things the studio could not say about itself.
The preset the header has open was painted in the dim text colour, the same grey
as the page name beside it — so the one label up there that changes with the look
read as chrome. It now takes the theme's own accent, the colour the brand mark
and the open tab already carry, which means it follows both the light/dark mode
and the accent group the reader picked, out of the CSS token and with no colour
written into the component.
And an image has no other mark on it: once the frontend tarball is loaded on the
NAS as `:latest`, nothing on the box says which revision came off. The rail now
names the build at its foot — quiet, mono, wrapping rather than widening the
column, and gone under 860px where the rail is the phone's scrolling tab bar
instead of a column.
The name is stamped into the bundle at build time: vite.config reads VERSION
from the environment (`docker compose build frontend --build-arg
VERSION=$(git rev-parse --short HEAD)`) and falls back to the package version
plus the minute it was built, so two builds of the same tree are never the same
name. `ARG VERSION=` in the Dockerfile is the knob; the bare `npm run build` —
dev server, check scripts — still stamps its own time, and the dev server passes
nothing at all, so the line only draws when there is something to say.
Verified:
library-check.mjs — 37 steps, all passed, the two new ones reading the header
off the running studio: the rail foot names the build (0.1.0+202609281504)
and the preset chip's computed colour is the brand's accent, not a grey —
rgb(206, 117, 9) amber, rgb(93, 24, 191) after switching to violet.
scan-nav-check.mjs and roll-walk-check.mjs — all passed. frontend tsc --noEmit
clean. Live 8090 on index-… matching dist/: /, /library and /app 200 with 0
console errors.
ponytail: the name is the package version plus a caller-supplied tag, and nothing
bumps the package version, so the tag is the whole identity — have the release
job write it into package.json if the numbers ever need to mean something.
|
||
|
|
134489a5ec |
web: come back to the frame that was raised, and zoom it by the wheel
Two things the preview owed a reader. The frame raised in the strip outlived nothing: leaving for the studio and coming back dropped the choice and the node opened on its first negative instead. It is now remembered beside the folder and the column width — the same one-line store, read on the way in, written on the way out — and a frame that is gone from the catalogue falls through to the first, the way a node that is gone falls back to its folder. A wheel over the frame now magnifies it. The listener goes on the element rather than through onWheel, because React's own wheel is passive and this one has to hold the page still while it zooms; the origin is the point under the pointer, which is what keeps a reader's aim on the thing being looked at, and the step comes from how far the wheel turned so a notched mouse and a trackpad travel the same. Six times is where it stops — the thumbnail behind the preview has no more pixels to give — and a frame that has just come up is fitted again. library-check: 35 steps, the two new ones raising a nested frame, reloading on it, and ticking the wheel up and back down. Co-authored-by: PenguinHarness <noreply@penguin.local> |
||
|
|
067b32131e |
web: drop the heading over the folder column
The column carried the name of the folder its tree belongs to, painted above the rows. The rows already say it — the head of the tree is a row like any other — so the heading only said it twice, and because it was not a row it stayed on screen when the tree was folded away: the one label left with nothing under it. The right click that raised it now finds the row itself, which is where a reader aims anyway. The label, its menu hook, its translation and its rule go; the tree, the row menus and fold-all keep working as before. Co-authored-by: PenguinHarness <noreply@penguin.local> |
||
|
|
5e28b42067 |
web: fold all keeps the top-level row open
COLLAPSE ALL folded the top-level row as well, and that row is already its own
fold: one click on its name shuts it and takes the whole subtree with it. Folding
it from the menu hid every row of the tree — the item repeated a click the reader
already had, while the one thing a click there cannot reach, the folders nested
below, was the part that got lost with it.
So the item folds the folders nested below the top level: every row with a
subfolder under it that is not itself a folder picked at the top level. The
top-level row stays drawn and stays open, its own subfolders are drawn folded,
and no top-level row ever leaves the column. `nested` is computed off `parents`
once, and the item is disabled on a folder with nothing nested under it.
Verified:
library-check.mjs — 35 steps, all passed. The fold step now reads the rows the
click leaves behind instead of a row count: the fully open roll (4 rows)
becomes exactly CheckRoll aria-expanded=true, CheckRoll/2026
aria-expanded=false, CheckRoll/Empty (a leaf, so no aria-expanded) —
CheckRoll/2026/04 is away, the top-level row is not — with the strip's 2
tiles unmoved and the row menu still leading with lib-menu-collapse.
frontend tsc --noEmit clean. Live 8090 on index-DAIpyDNV.js matching dist/:
/, /library and /app 200 with 0 console errors.
ponytail: with several folders picked, one COLLAPSE ALL folds the nested rows of
all of them, not only the row that was clicked — no top-level row is hidden
either way; scope it to the clicked folder's prefix when a column routinely
holds many picked folders.
|
||
|
|
4a740b1e28 |
web: fold the tree from the row a reader right-clicks
COLLAPSE ALL lived only on the column's own name — the label above the tree —
and that label scrolls with the list it heads: on a roll read deep the column is
scrolled past it, so the item looked like it only appeared once the top-level row
had been folded to bring the list back to the top. The row is where the pointer
already is, and it already carries the folder's menu.
The top-level folder row now leads its own menu with COLLAPSE ALL — rename,
rescan and remove follow it, and a subfolder row is unchanged, since folding a
whole tree is what a row that a tree hangs from can do and a row inside one
cannot. Both entry points run the same one: `root` on the menu state now means
"the head of a tree", the column's own name or the row of a folder picked at the
top level, and the menu draws the item first and then whatever the subject
itself has — the folder's items, or, on the empty part of the column, ADD
FOLDER. The empty part keeps ADD FOLDER alone: it is not the head of a tree.
Verified:
library-check.mjs — 35 steps, all passed. The new step right-clicks
lib-node-CheckRoll with the tree fully open (4 rows) and gets back exactly
[lib-menu-collapse, lib-rename-CheckRoll, lib-rescan-CheckRoll,
lib-drop-CheckRoll]; a right click on the column's name still answers with
lib-menu-collapse alone; the subfolder menu above is still the three folder
items with no fold in it; the empty column still offers lib-menu-add alone.
The fold itself is unchanged and still measured by opening the root back up:
4 rows → 1, and 3 back, with CheckRoll/2026/04 still away.
frontend tsc --noEmit clean. Live 8090 on index-DEvUJDNY.js matching dist/:
/, /library and /app 200 with 0 console errors.
ponytail: the column's name is still the second way to the same item and still
scrolls with the list — left alone because the row is now the one that matters;
make the name sticky when a roll routinely fills the column past one screen.
|
||
|
|
c1ee2cf30a |
web: fold the whole roll from the head of the tree
A roll read to the bottom of its dates is a long column of indented rows, and
the only way to shut it was one row at a time — the click that folds a row also
opens it, which is the right trade for reading and a poor one for putting away.
The column's own name is now the whole tree's control: a right click there opens
a menu whose one item is COLLAPSE ALL / THU GỌN TẤT CẢ, and it folds every row
that has something under it at once — the root's own row included, so the tree
comes back to one line per picked folder and the subfolder two deep is away with
the rest. The menu is the folder menu's shape at a third subject rather than a
second menu: one `root` flag on the state that already knows where a right click
landed, and the same Escape, same click-away, same clamp to the window edge.
The frames do not move with the rows: folding is a way of looking at the tree,
and the strip already reads the open folder, which the fold leaves alone. The
name keeps reading as the folder it belongs to — the folder's label, with the
hint appended to its title — and the item is disabled rather than hidden when
nothing in the column has children, so a fresh folder does not open a menu with
a dead line in it.
Verified:
library-check.mjs — 34 steps, all passed, 2 new: a right click on the root
name offers exactly [lib-menu-collapse] while the name still reads "Roll A",
and one click takes the fully open roll (4 rows: the picked folder, its
subfolder, the subfolder under that, and one holding no frame) to a single
row. The fold is measured against the ruler of opening the root back up:
the 3 rows that return are CheckRoll, CheckRoll/2026 and CheckRoll/Empty —
CheckRoll/2026/04 is still shut, which is what says the deep row went with
the fold and not just the root. The strip held its 2 tiles throughout, and
the folder the visit was on (lib-node-CheckRoll/2026) is what the reload
below still reopens on.
scan-nav-check.mjs — all steps passed; roll-walk-check.mjs — passed.
frontend tsc --noEmit clean. Live 8090, served asset index-DZFlTBDG.js
matching dist/: /, /library and /app all 200 and 0 console errors.
ponytail: only folding, no unfold-all to match — the click on a row already
opens it and the root's own row is one click away; add the pair when the column
routinely holds many picked folders and opening them one by one stops being
cheap.
|
||
|
|
3aedd67ca0 | web: open the catalogue without a version another tab can block | ||
|
|
52732a17ee | web: name a roll's folders before reading their frames | ||
|
|
d0fddba1e8 | web: walk a roll layer by layer, and let the strip drop the branch | ||
|
|
bd206a8be9 | web: let a wheel tick walk the strip sideways | ||
|
|
2c78ff80e8 | web: fold a folder's rows without folding its frames | ||
|
|
2b45419ec4 |
web: scan the whole roll, title and size the column, reopen where you left
The tree column now walks every folder under the one you picked instead of its first level, so a roll of dated folders inside dated folders is walked to the bottom. A folder being read turns a ring on its own row, the column is titled with the folder the tree belongs to, and a divider beside it drags the width, which is remembered along with the folder the screen was left on. |
||
|
|
4c65923eff |
web: rename a folder, and add one from the empty column
A folder's own menu gains a rename, which paints a label over the folder without moving it — the frame ids and the recipes hang off the directory's name, not the label. The empty part of the column answers a right click with ADD FOLDER, the FOLDERS heading goes (the count already rides on each row), and the column narrows to 118px so the stage takes the room. |
||
|
|
19fd5b644f |
web: trim the library to one toolbar and a folder menu
Fold the hint, ADD FOLDER and the two view switches onto a single row, with the switches drawn as icons, and move a folder's rescan and remove onto a right click — they belong to the folder, so they are asked for rather than always on screen. |
||
|
|
d1abda141a |
web: browse the catalogue as a tree of folders
The library page was a flat wall of thumbnails. It now reads like the admin pictures tab: a folder tree on the left, the picked node's frame on the stage, that node's frames in the strip below, and a chip pair to swap the stage for a wall of every thumbnail in the node. Folders are walked recursively (dot/@-prefixed names skipped, an unreadable subfolder is dropped rather than failing the scan), so a roll with a dated subfolder keeps both levels. Node counts include the subfolders underneath. |
||
|
|
81637b8502 |
web: a catalogue of the folders on the visitor's own disk
A RAW studio that cannot see a folder is one photo at a time. /library now takes a folder through Chromium's directory picker, keeps the handle in IndexedDB so the folder is there on the next visit, and walks it into a grid: one thumbnail per frame, the frame's own date, and the recipe it was last graded with. Nothing is uploaded and nothing is read twice — the RAW itself is opened only when a tile is clicked, at which point the studio develops it and the recipe comes back on top. The studio files every change back against the frame, debounced, so reopening a RAW is not doing the grade again. Thumbnails come off LibRaw's unpack_thumb for a RAW, which is a seek and a copy where a develop is a full decode of every pixel, and off createImageBitmap for anything else. A RAW with no preview inside it gets a placeholder tile rather than a minute of decoding per file. node scripts/library-check.mjs ok both frames indexed as tiles — 2 tiles from P1010256.JPG + P1010256.RW2 ok thumbnail for P1010256.JPG — 40400 bytes, jpeg=true ok thumbnail for P1010256.RW2 — 39895 bytes, jpeg=true (LibRaw preview) ok studio developed the frame from its handle ok the address was handed back — url=/app (no ?lib= left behind) ok the look was filed back against the frame — baseFilter=none, 19 knobs ok the tile says the frame is edited |