14 KiB
Media
Route: /media, /media (detail panel), /media?filter=duplicates. API: GET /v1/media/ · POST /v1/media/ · PATCH /v1/media/{id} · DELETE /v1/media/{id} · POST /v1/media/merge · GET /v1/media/{id}/usage · GET /v1/media?variants=with|without&sort=total_size · GET /v1/media/{id}/variants · POST /v1/media/{id}/variants/{name} · DELETE /v1/media/{id}/variants/{name} · owner and admin only.
New route for verified users, in the sidebar between Articles and Users with the images icon. Images belong to the library, not to an article: one image can be used in many articles, and deleting an article never deletes images. Uploads from this page are not cropped or resized, and the file is stored as chosen. "Used in" is read from article bodies each time the list loads. Duplicates are images with identical content, whatever their filenames. Alt text is not stored on the image; it is set when inserting into an article. Search, filter, sort and view are stored in the query string. Keyboard: grid items are buttons in one tab stop each; Enter opens the detail panel, Space toggles selection, and Escape closes the panel or clears the selection. Selection changes are announced ("north-wall.jpg selected, 1 selected").
Visual reference: RenewCMS Screens Back-office.dc.html, groups "Media", "Media library", "Image detail", "Media duplicates entry". Frames: AppFrame.dc.html (shell, sidebar, topbar, drawer) plus the screen frame where listed in components.md.
Copy per state (generated from the board source, pass 2)
Every state below is drawn in light and dark (theme switch on the board), at 1440×900 and 390×844 (both frames sit side by side). Board: RenewCMS Screens Back-office.dc.html. Copy is exact; where a cell is empty the state reuses the fixed copy.
Media (group "Media", 43 states)
Route: /media. API: GET /v1/media/ · POST /v1/media/ · PATCH /v1/media/{id} · DELETE /v1/media/{id} · POST /v1/media/merge · GET /v1/media/{id}/usage.
New route for verified users, in the sidebar between Articles and Users with the images icon. Images belong to the library, not to an article: one image can be used in many articles, and deleting an article never deletes images. Uploads from this page are not cropped or resized, and the file is stored as chosen. "Used in" is read from article bodies each time the list loads. Duplicates are images with identical content, whatever their filenames. Alt text is not stored on the image; it is set when inserting into an article. Search, filter, sort and view are stored in the query string. Keyboard: grid items are buttons in one tab stop each; Enter opens the detail panel, Space toggles selection, and Escape closes the panel or clears the selection. Selection changes are announced ("north-wall.jpg selected, 1 selected").
| # | State | Trigger | Copy |
|---|---|---|---|
| 1 | Grid, many images | Default view. 12 images, newest first. Each item is a button labelled like "north-wall.jpg, 3.9 MB, used in 2 articles". Badges: "Unused" (warm), "Duplicate" (info), "Used in N" (outline). The Duplicates tab carries a count of groups only while duplicates exist. | |
| 2 | Grid, few images | Three images. Cards keep their size instead of stretching. Checkerboard shows behind the SVG. | |
| 3 | List view | The list icon in the toggle. A table from md up (Filename, Dimensions, Size, Type, Used in, Uploaded by, Uploaded) and a card list below md. The "Used in" count opens the article list in the detail panel. | |
| 4 | Loading | The first load, a retry, or a change of search or filter. Skeleton cards appear after 150 ms. | |
| 5 | Empty library, first run | Zero images. Toolbar and tabs are hidden; one call to action. Copy: "No images yet" / "Upload your first image. You can use it in any article." / "Upload an image". | |
| 6 | No results | Search or filter matched nothing. "Clear filters" resets the search and goes back to All. | |
| 7 | Hover, focus and keyboard | Hover (second item) darkens the border, adds the pop shadow and reveals the checkbox. Keyboard focus (third item) shows the 2 px primary ring with a 2 px offset and reveals the checkbox. Tab moves between items, Enter opens the detail panel, Space toggles selection, and arrow keys move focus across the grid. | "aria-label: kitchen-before.webp, 1.1 MB, used in 1 article" |
| 8 | Selection and bulk bar | Checkbox on any item, or Space. Selected items take the sand fill with a primary outline, and Select all shows a partial state. The bar is sticky at the bottom (above the bottom edge on mobile, replacing the Upload button). Announced: "3 selected". Escape or Clear empties it. | |
| 9 | Bulk delete: confirm | Delete in the bulk bar. The dialog says how many of the selection are in use and lists them. The confirm button carries the count. | |
| 10 | Bulk delete: in progress | Requests in flight. The dialog can't be dismissed. | |
| 11 | Bulk delete: partly failed | Some requests failed. Deleted images leave the grid, failed ones stay selected, and the toast stays until retried or dismissed. | "Couldn't delete 1 image" / "2 images were deleted. window-frame.jpg is still in your library. Try again." / "Retry" |
| 12 | Bulk delete: done | 204 for all. Focus moves to the next item. | "3 images deleted" / "They're gone from your library." |
| 13 | Detail panel: in use | Click or Enter on an item. Right-hand slideover on desktop, full screen on mobile with a back arrow. Actions: Copy URL, Rename, Delete. "Used in 2 articles" lists title, Online or Draft, and links to the editor. Focus returns to the item on close. | |
| 14 | Detail panel: unused | Used in nothing. The section reads "Not used in any article" and says the image is safe to delete. | |
| 15 | Detail panel: duplicate | The image has identical copies. An info alert links to the duplicates view. | |
| 16 | Copy URL | Copy URL. Clipboard write succeeded. Success toasts last 5 s. | "URL copied" / "Paste it wherever you need the image address." |
| 17 | Rename: dialog | Rename in the panel. The field holds the name without its extension, and the .jpg ending is fixed. The description says the URL stays the same. | |
| 18 | Rename: saving | PATCH in flight. Field and buttons disabled, dialog can't be dismissed. | |
| 19 | Rename: validation | Empty or only symbols, on submit or blur. Nothing is sent. | "Enter a filename." |
| 20 | Rename: failed | 500 or network failure. The error shows inside the dialog. | "Couldn't rename the image. Your text is still here. Try again in a moment." |
| 21 | Rename: done | 200: the panel title and list update in place. | "Image renamed" / "It's now "north-wall-01.jpg". The URL is the same." |
| 22 | Delete: unused image | Delete in the panel. Plain confirm. Says what happens and that it can't be undone. | |
| 23 | Delete: image in use | Stronger confirm. Lists the affected articles and states that they will show a broken image. | "delUsed" |
| 24 | Delete: in progress | DELETE in flight. The dialog can't be dismissed. | "delUsed" |
| 25 | Delete: failed | 500 or network failure. The error shows inside the dialog, as in the article delete. | "Couldn't delete the image, so it's still there. Try again in a moment." |
| 26 | Delete: done | 204: the panel closes, the item leaves the grid and focus moves to the next item. | "Image deleted" / ""window-frame.jpg" is gone from your library." |
| 27 | Image already gone (404) | Any action on an image that was deleted elsewhere. The panel closes and the item leaves the list. | "That image no longer exists" / "It was deleted somewhere else, so we removed it from the library." |
| 28 | Upload: drag over the page | Files dragged onto the page. A dashed drop area covers the content. Dropping starts the upload straight away; there is no crop step on this page. | |
| 29 | Upload: several files, mixed | Upload button or drop, several files. Each file has its own row and progress. Files upload two at a time. A row can end as added, "Already in your library", rejected in the browser, or failed. Rejected and failed rows stay until dismissed. | "2.2 MB" / "2.6 MB" / "1.1 of 2.6 MB" / "1.9 MB" |
| 30 | Upload: summary | All rows settled. The summary counts each outcome. "Use existing" selects the existing image and drops the row; "Upload anyway" stores a second copy, which then appears under Duplicates. "Try again" retries one file. Announced when it finishes. | "2.2 MB" / "2.6 MB" / "Upload finished" / "5 images" / "2 uploaded · 1 already in your library · 2 need attention" |
| 31 | Upload: rejected files (413, 400) | Type and size are checked in the browser first, so nothing is sent. A server 413 or 400 for a file that got through shows the same row with its own message. | "210 KB" / "notes.pdf isn't an image" / "Images can be up to 10 MB. Use a PNG, JPEG, WEBP or SVG image." / "3.0 MB" / "broken.png couldn't be read" / "The server couldn't read this image. Save it again as a PNG or JPEG, then try again." / "Nothing was uploaded" / "3 files were rejected" |
| 32 | Upload: too many requests (429) | Rate limited. Retry-After is counted down in the button. It re-enables at zero. Already-added files are kept. | |
| 33 | Duplicates: groups | The Duplicates tab. Groups of identical images side by side, each with its uploader, upload date and "Used in N articles". The suggested copy to keep is the most used, then the oldest. | |
| 34 | Purge duplicates: confirm | "Purge duplicates" in the toolbar, next to the sort menu (in the stacked toolbar on mobile). Opens the same dialog as Settings; every other state is on the Media cleanup board. | |
| 35 | Duplicates: no purge button (member) | Verified users who are not owner or admin. The "Purge duplicates" button is hidden. "Merge into this one" is unchanged. | |
| 36 | Duplicates: empty | No groups. The Duplicates tab loses its count badge. | |
| 37 | Merge: confirm | "Merge into this one" on floorboards.jpg. The dialog says articles using the other copies will switch to this image and the other copies will be deleted, with the article count. | |
| 38 | Merge: in progress | Request in flight. The dialog can't be dismissed. | |
| 39 | Merge: failed | 500 or network failure. Nothing is switched or deleted. | "Couldn't merge the copies. Nothing was changed. Try again in a moment." |
| 40 | Merge: conflict (409) | A copy was deleted, or a new copy appeared, since the page loaded. | "One of these images changed, so nothing was merged. The list is up to date now. Check it and try again." |
| 41 | Merge: done | 200: the group leaves the list. When it was the last one, "No duplicates" shows. | "Copies merged" / "2 copies merged. 5 articles updated." |
| 42 | List failed to load | A 500 or network failure on the list request. The alert replaces the images; Try again repeats the request. | |
| 43 | Offline | navigator.onLine is false. Loaded images stay browsable. Upload, Rename, Delete and Merge are disabled, and the banner says why. |
3. Media library (group "Media library", 6 states)
Route: /media. API: GET /v1/media?variants=with|without&sort=total_size.
The existing grid with three additions: a "N variants" badge on cards that have cached files, an All / With variants / Without filter, and the sort option "Largest total size (original + variants)". When sorted by total size, the meta line shows the total.
| # | State | Trigger | Copy |
|---|---|---|---|
| 1 | Grid with badges | Default view. Images with no cached variants have no badge. | "New: badge" |
| 2 | Sort menu | The sort select is open. The new option is last. | "New: sort option" |
| 3 | Filter: With variants, sorted by total size | The meta line becomes total · original + variants. | "With variants" |
| 4 | Filter: Without variants | Images no variant has been generated for. | "Without" |
| 5 | Filter: nothing to show | With variants, and the cache is empty. | "With variants" |
| 6 | Delete confirm | Delete on an image that has cached variants. | "deleteImage" |
4. Image detail: Variants tab (group "Image detail", 8 states)
Route: /media (detail panel). API: GET /v1/media/{id}/variants · POST /v1/media/{id}/variants/{name} · DELETE /v1/media/{id}/variants/{name}.
The detail panel gets a Variants tab with one row per enabled variant. On desktop the row actions are icon buttons with tooltips and accessible names; on mobile they are labeled buttons on their own line. Only actions that apply are shown: generated rows offer Copy URL, Preview and Purge; others offer Copy URL and Generate now. The tab shows output dimensions as they will be generated, capped at the source size.
| # | State | Trigger | Copy |
|---|---|---|---|
| 1 | Partly generated | Four of five generated. Saving is shown against the original. | |
| 2 | Nothing generated yet | No cached files for this image. | |
| 3 | Generating | Generate all or Generate now was pressed. Rows show a spinner. | |
| 4 | Generation failed | The row shows the cause and Retry. Both causes are shown together for reference. | "Unsupported source format" / "Image too large" |
| 5 | No variants defined | There are no enabled variants. | |
| 6 | Source is an SVG | Variants aren't generated for vector images. | "SVG images are served as they are." |
| 7 | Source is an animated GIF | Same rule as SVG. | "Animated GIFs are served as they are." |
| 8 | Preview dialog | Preview on a generated row. Both at actual size. |
Media: Duplicates tab entry point (group "Media duplicates entry", 2 states)
Route: /media?filter=duplicates. API: owner and admin only.
A secondary "Purge duplicates" button (icon merge) in the toolbar next to the sort menu, and in the stacked toolbar on mobile. The dialog and the member state are listed with the Media states above. Per-group "Merge into this one" is unchanged.
| # | State | Trigger | Copy |
|---|---|---|---|
| 1 | Duplicates tab with the button | Owner or admin. | |
| 2 | Duplicates tab: offline | The button is disabled with the existing banner. |