goes to another screengoes to another state of this screenfaded = not mocked yetEverything else — the buckets, the views, the filters, the drawer, the two admin lists — works, it just stays on this screen. The URL always names the state, tab, bucket and view you are on.
Images ›Image library
Image library
r2://merch-riot/sales-channel/
Every image the system holds, in the three libraries that own them. This is the screen for finding, tagging and administering pictures; the pickers on the product, formula and channel screens still open only the one library that belongs to them.
View
Image typeSale typeTags
Image types
13administered
Order is the order every type control offers them in — the listing builder’s column headers, the tag select in the generation popup, and the filter above. It is the team’s practiced eBay build order, so the next thing you want is where you expect it. A type in use cannot be deleted; the count is the guard.
Sale types
10administered
Offered only on an image whose Sale facet is Yes. Holiday is the catch-all and sits last on purpose — it is what you pick when the art is seasonal but not tied to one date.
Tags in use
not administered
A free pool, shown so you can see what the bench has actually invented — not a list anyone maintains. Sale and Product are deliberately absent: both are facets of their own, and a tag that duplicates a facet is how an image ends up disagreeing with itself.
Add a tag
Free text — nobody administers this list. Sale and Product are facets, set above; a tag that repeats one is how an image ends up disagreeing with itself.
Already in use — click to add
Design notes — Image library
MR-102, from Paul’s feedback on the 6.1 Images round, 2026-09-04. Requirement records: docs/requirement-inputs/RI-images.md RI-047…RI-065, RI-139…RI-157.
Why this screen exists, and what it is not
Until now there were two image surfaces and neither of them browsed: the picker modal (_image-module.js, one library per screen, four consumers) and the gallery panel on the listing builder’s 6.1 stage, scoped to sales-channel/. Paul asked for buckets, folder views, tag search and tagging — none of which fit in a 236px rail or a picker modal.
The split that resolves it: the panel is a shortlist, this screen is the library. The panel’s job is getting a picture into a lane; browsing, folders and tagging are a different job. So MR-071’s one-screen-one-library rule survives untouched for every picker — this is simply the one screen whose library is all of them.
Four facets and a free pool
Paul’s note lists five things under Tagging — Product, Sale, Sale Type, Image Type, Tags — but the last (Men, Women, Mixed Gender, Sale, Product) re-contains two that are already their own field. An image could then be Sale: No and carry a Sale tag, and nothing on the record would say which is true.
So: Product (a link to a catalog record, not a string), Sale (yes/no), Sale Type (only meaningful when Sale is yes), Image Type — structured, filterable, administered where a list is involved. Everything else is a free tag pool. Sale and Product are dropped from the pool because they are facets. The tell that this is the right cut is Paul’s own admin screen: it administers exactly two lists, and gender is not one of them.
A sale tag finds art; it does not swap it
Tagging an image Labor Day only pays off later if something swaps a listing’s pictures when the promotion opens — and the promotions rung on the listing builder makes that thought available. It is deliberately not built. Paul lists Sale and Sale Type under tagging and under views, never under behaviour, so they are retrieval facets: they cut 50 images down to the nine that are sale art. The swap is logged as an open record with its cost, not improvised here.
The vocabulary conflict, and why the admin screen dissolves it
Three Image Type lists were live at once. Paul’s Tagging section says Product Shot · UGC · Banner; his Administration section says Front · Back · Ingredient List · Instructions · UGC · Banner · Lifestyle; RI-027 settled a third the day before. The admin list is decisive evidence of a sketch rather than a replacement: the default template’s own five columns are Main · Benefit 1 · Benefit 2 · How-to · Testimonials, and not one of them appears in it. Taken verbatim it would leave every column on the screen Paul was reviewing with no type to declare.
The seed is therefore the union in the practiced order, and because the list is now administered the seed is a starting point rather than a contract — which is what makes a union safe. Two are marked new: UGC and Banner.
Open — for Paul. He wrote Ingredient List; the screens say Supplement Facts, because RI-027 exists precisely on his own point that nobody at the bench calls that panel “ingredients”. Drawn as Supplement Facts and flagged rather than flipped silently. One word, one answer needed.
What closing RI-028 cost
The sales-channel library declared six roles of its own — main, gallery, infographic, lifestyle, comparison, additional — which no tag the listing builder writes ever matched. It now reads the administered vocabulary, so there is one list rather than two that disagree, and the six fixtures were re-pointed (infographic → Benefit, gallery → Additional, comparison → Size). The other two libraries keep their own role lists — a source formula’s House label is not a marketplace image type, and merging all three was not asked for.
Fifty rows, because six could not answer the question
Paul asked what the library looks like at forty images. A panel that is comfortable at six is exactly the panel that fails at forty, so the fixture is the question: 50 sales-channel rows, four of them real files and the rest honest placeholders — the same convention this module already used, for the same reason. The answer the screen gives is that scale is handled by cutting, not by scrolling: three views, two facet filters, a tag bar and search, with the result line always stating what the filters did.
Auto-tagging suggests; it never files
Paul: “System should attempt to tag images.” Attempting is a provider call, so it lands under the LLM pipeline guardrails — a validated response, a per-call cost stated before it runs and logged after, and a failure that is visible rather than swallowed. It is drawn as suggestions an operator accepts or rejects per field, never as a silent write, which also keeps RI-014 intact: an image cannot enter the library untagged, and a machine guess is not a tag until a person says so. The amber ring on a tile marks an image with no product and no type — the queue the button works through.
A folder per brand, holding folders per product
Paul wrote both “organized by product” and “a folder per brand”, which are two grains. Drawn as both, nested: brand is the folder, product is the group inside it. That satisfies each reading without choosing between them, and it gives the images that belong to no single product — storewide banners — an honest home rather than hiding them.
Full screen, and what the arrows walk
A 230px drawer preview is not enough to approve a marketplace image on — a bad edge, a clipped logo or a cut-off claim is exactly what survives a small preview and reaches a channel. So the preview opens full screen, whole and uncropped at its true ratio, on an opaque ground: RI-029’s rule (the surface an image is approved on never crops it) following the image to the largest surface it has.
← and → move through the set the screen is currently showing, in the order it is showing it — not the whole library. Paul asked to arrow to “the other images that would be next in the library”, and the literal reading is the wrong one: a filter you set is a filter you meant, and arrowing out of sale art into the other forty-one would silently undo it. Bounding them by the view is what makes the pair useful — cut to the nine Labor Day banners, then walk exactly those nine. They stop at the ends rather than wrapping, because you should be able to tell you have reached one.
The viewer and the drawer are one selection: arrowing moves the drawer underneath, so closing leaves you on the image you stopped at and can tag it straight away. Esc closes the viewer and leaves the drawer open; a second Esc closes the drawer. One key, one thing.
No browser dialogs — and two shapes, not one
Paul: “Never use old school browser functions.” This restates MR-082, which settled it for the ladder-model manager and which this screen was first written in breach of. A native prompt cannot be labelled, cannot show the tags already in use, cannot validate without throwing a second box at you, and renders as browser chrome rather than as the product.
The shape is chosen by whether a field is already on screen. The tag lives inside the drawer and has no field of its own, so it gets a dialog — which earns itself by offering the tags already in use as one-click chips. The two admin add-rows already have their fields in front of you, so they get inline validation beside the field: a modal thrown over a form you can see is the same mistake in a nicer skin. And nothing to auto-tag is a disabled button with a reason, not news broken after you press it.
Searchable dropdowns, not native selects
Paul pointed at listing-builder.html#stage=s2’s catalog search as the model, and the five value lists on this screen now use it: a search field on top, a filtered list, a live match count, ↑/↓ and Enter, and the matched run marked so a row shows why it matched.
Each row carries what tells it apart, and search covers all of it — a product’s CP- id, an image type’s stored value, and how many images already use it. A native select can do none of those: it cannot be searched, it cannot show a second line for the stored value that RI-050 insists is a different string from the display name, and it cannot show the count that makes Banner · 9 a different proposition from Banner · 0.
One implementation serves all five, and the popup is a single element parented to <body> at position:fixed — the drawer rebuilds its body on every edit and would destroy a popup living inside it mid-keystroke, and it is a scroller that would clip one. Same trap as MR-065’s sidebar rail and RI-033’s gallery tooltip.
Drilling in from a folder title
Search already matched brand, so a brand name in By product returns that brand’s folder with a group per product. What was missing was the second half: the results were headings, and a heading you cannot act on makes you retype what you can already see. Both levels are now clickable.
They behave differently on purpose. A brand still has products worth seeing apart, so clicking one stays grouped. A single product does not — a folder view of one folder is a heading over the only group, furniture rather than information — so clicking a product flattens to the plain grid. Flattening is what makes “just that product” look like an answer.
The crumb strip exists because the drill-down is otherwise invisible once flattened: the headings that told you where you were are exactly what flattening removes. Each crumb drops one level; Back to the whole library drops them all. The no product group is deliberately not clickable — it is an absence, not a product to filter by.
Filters take several values; facets take one
The two filter dropdowns are multi-select with checkboxes. Several values on one filter mean any of them; several filters together mean all of them — Banner or Main, and Summer Sale or Black Friday. The facet dropdowns in the drawer stay single-select and show no checkbox.
That difference is the point, not an inconsistency. A filter asks which of these you want to see, and wanting banners and UGC together is ordinary. A facet asserts what the image is, and RI-048 gives an image one image type and one sale type — so letting the same-looking control mean both would let someone tick two types on one image and produce a row the model cannot hold.
OR within, AND across is the only combination that behaves. AND within would ask for an image that is both a Banner and a Main, which is empty by construction, so every second tick would blank the screen. And there is no Any type row: nothing ticked already means any, and a row meaning untick everything is a button pretending to be an option — the footer clears instead.
Not mocked
Zoom past fit-to-screen is not drawn — a 1500×1500 fits comfortably, but a phone photo’s fine print is legible rather than readable. Reordering the admin lists is drawn (the grab handle) but not wired. Upload is absent — images arrive here by approval from the listing builder or by the existing picker’s upload tab, and a third door was not asked for. Deleting an image, as opposed to a type, has no control at all: RI-031 has not settled what an unapproved generation’s lifetime is, and a delete button drawn before that answer would be inventing the answer.