Feature: The editor's Library tab, picking an image from the media library As a verified RenewCMS owner writing an article I want to browse my library from the "Add an image" dialog and insert an existing image with alt text So that I can reuse an image without uploading it again Background: Given this slice is SPA only for this feature: the API from the media library slice is used unchanged, and the Go change to remove the legacy alias is specified in features/remove_legacy_article_image_alias.feature And the screen is docs/design/screens/media-picker.md, drawn by docs/design/source/MediaFrame.dc.html, inside ImageUploader (UModal "Add an image") on /article/:id/edit And the dialog has a title "Add an image", an x to close, and two tabs under the title, "Upload" (icon upload) and "Library" (icon images), 36 px high on desktop and 44 px on mobile with a 2 px primary underline on the selected one; it is 640 px wide on desktop and a bottom sheet on mobile And the footer reads "Cancel", and on the Library tab also a primary "Insert image" that is disabled until an image is selected And the Library tab calls GET /v1/media/?q=Q&page=N&size=25 through useApi, with q only while a search is set and page 1, then 2, 3, ... when loading more (no filter and no sort parameter), and the Upload tab is specified in features/editor_image_upload.feature And the response is { items, total, page, size, counts }, newest first; "Load more" shows while the loaded count is below the response total (the matching count, not counts.all); sizes read "8 KB" and "3.9 MB", dimensions read "2400 × 1600" (an SVG reads "Vector"), dates are absolute And the 12-image fixture, ids 1 to 12, is the one in features/media_browse.feature, and the picker mocks use total 48 and counts { all: 48, unused: 2, duplicates: 2 } And this slice is amended by features/media_pagination_api.feature: cursor and limit are gone, LIBRARY_PAGE_LIMIT 24 became the page size 25, and a mock with items equal to its total is the last page And an image is "in this article" when an image node with its id as data-image-id is in the editor's current content, unsaved insertions included, when the dialog opens; in the examples below images 1 (north-wall.jpg) and 4 (logo-renew.svg) are in the article And the search field reads "Search by filename" (32 px high on desktop, 44 px with 16 px text on mobile), with a search icon and, once it has text, an x that clears it And the grid has 4 columns on desktop and 3 on mobile with a 12 px gap (8 px between columns on mobile), square tiles with a 1 px border, and the filename under each tile in 12 px toned text with an ellipsis; it scrolls inside the dialog, 300 px high with nothing selected, 190 px on desktop and 120 px on mobile with one selected And every tile is one button whose accessible name reads like the Media page's, for example "north-wall.jpg, 3.9 MB, used in 2 articles" or "window-frame.jpg, 1.8 MB, unused" And skeleton tiles show only after 150 ms: 8 on desktop and 9 on mobile for a first load or a search, one row (4 or 3) at the end for "Load more" And a 401 or 403 on any of these calls is handled globally by useApi, with no local copy Rule: Which tab opens Scenario: The library has images, so Library opens Given GET /v1/media/?page=1&size=25 resolves 200 with the 12-image fixture When the user activates the toolbar's "Image" button Then the Library tab is selected and the footer reads "Cancel" and a disabled "Insert image" And nothing is drawn in the grid for the first 150 ms, then 8 skeleton tiles show until the response arrives Scenario: The library is empty, so Upload opens Given GET /v1/media/?page=1&size=25 resolves 200 with no item and no search set When the dialog opens Then the Upload tab is selected, in its idle state When the user selects the Library tab Then the empty panel shows Scenario: A dropped file opens Upload and sends no list request When the user drops "before.png" onto the editor Then the Upload tab is selected and no GET /v1/media/?page=1&size=25 is sent When the user selects the Library tab Then GET /v1/media/?page=1&size=25 is sent Rule: Browsing Scenario: Library, nothing selected Given the first page resolved with the 12-image fixture Then the line "Newest first · 48 images" shows under the search field And 12 tiles show in fixture order, 4 per row on desktop and 3 on mobile, each with its filename And the tiles of "north-wall.jpg" and "logo-renew.svg" carry a sand "In this article" tag (bottom-left, 18 px high, 11 px text) And no other tile carries it And "Load more" shows below the grid, and "Insert image" is disabled Scenario: A search is active, so the count is not shown Given the user typed "wall" and the response resolved with 2 items Then the line reads "Newest first" Scenario: Load more is hidden on the last page Given the loaded items equal the response total (for example total 12 with the 12-image fixture) Then "Load more" is not shown Scenario Outline: A tile's accessible name Given the first page resolved with the 12-image fixture Then the tile for "" is named "" Examples: | filename | name | | north-wall.jpg | north-wall.jpg, 3.9 MB, used in 2 articles | | kitchen-before.webp | kitchen-before.webp, 1.1 MB, used in 1 article | | window-frame.jpg | window-frame.jpg, 1.8 MB, unused | Rule: Selection Scenario Outline: A tile is selected by pointer or keyboard Given the first page resolved with the 12-image fixture When the user on the tile of "kitchen-after.webp" Then that tile has a 2 px primary outline (2 px offset) and a 20 px primary check at its top-right And "Insert image" is enabled Examples: | action | | clicks | | presses Enter | | presses Space | Scenario: The selection panel Given "kitchen-after.webp" is selected Then the grid shrinks and a panel shows below it, under a 1 px top border And the panel has a 56 px thumbnail, the filename "kitchen-after.webp", "2400 × 1600 · 1.2 MB", and a field "Alt text (optional)" And the field's placeholder reads "e.g. North wall of the kitchen before plastering" And its help reads "Describes the image for people who use a screen reader, and shows if it doesn't load. Leave it empty if the image is only decoration." And "Load more" is hidden while an image is selected Scenario: Selection is single Given "kitchen-after.webp" is selected and the alt text reads "A kitchen" When the user selects "window-frame.jpg" Then only "window-frame.jpg" is selected and the alt text field is empty Scenario: Activating the selected tile again keeps it selected Given "kitchen-after.webp" is selected When the user activates its tile Then it stays selected Scenario: An SVG's panel reads "Vector" When the user selects "logo-renew.svg" Then the panel reads "Vector · 8 KB" Scenario: An image already in the article can be selected and inserted again When the user selects "north-wall.jpg", which carries "In this article" Then "Insert image" is enabled Rule: Insert Scenario: Insert with alt text Given the user is on /article/12/edit in the Saved state with "kitchen-after.webp" (id 2, url "/static/uploadedImages/2.webp") selected And the alt text reads "A kitchen with new oak worktops and white tiles" When the user activates "Insert image" Then insertImage is called with "/static/uploadedImages/2.webp", "A kitchen with new oak worktops and white tiles" and image id 2 And the image is in the editor's content at the cursor, with data-image-id 2 And SaveStatus reads "Unsaved changes" and Save is enabled And a toast appears: color success, icon i-lucide-circle-check, title "Image added", description "Save to keep it in the article." And the dialog closes at once And no POST, PATCH or DELETE has been sent Scenario: Empty alt text inserts a decorative image Given "kitchen-after.webp" is selected and the alt text is empty When the user activates "Insert image" Then insertImage is called with alt "" Scenario: Alt text lives only in the article Given "kitchen-after.webp" is selected with alt text "A kitchen" When the user activates "Insert image" Then no request carries the alt text and the library item is unchanged Scenario Outline: Leaving without inserting changes nothing Given "kitchen-after.webp" is selected with alt text "A kitchen" When the user Then the dialog closes, the editor's content and dirty state are unchanged, and no toast shows Examples: | action | | activates "Cancel" | | activates the x | | presses Escape | Rule: Search Scenario: Typing searches after a debounce Given the Library tab shows the 12-image fixture When the user types "wall" Then after a 120 ms debounce GET /v1/media/ is sent with q=wall, page=1 and size=25 And skeleton tiles show after 150 ms, and any selection is cleared Scenario: The x in the field clears the search Given the search field holds "wall" with results shown When the user activates the x in the field Then the field is empty and GET /v1/media/ is sent with page=1 and size=25 and no q Scenario: No results Given GET /v1/media/?q=wall&page=1&size=25 resolves 200 with no item Then the search icon shows with the title `No images match "wall"`, the text "Check the spelling, or search for a shorter part of the filename." and a "Clear search" button And the search field still shows "wall" above it, and "Insert image" is disabled When the user activates "Clear search" Then the field is emptied and the list is requested again with no q Rule: Load more Scenario: Loading more shows progress and appends Given the first page resolved with 12 images and total 48 When the user activates "Load more" Then GET /v1/media/ is sent with page=2 and size=25 And the button reads "Loading…" with a loader-circle and is disabled, and one row of skeleton tiles shows at the end And focus stays on the button When the request resolves 200 with 12 more images Then the new tiles are appended after the first 12, and focus moves to the first new tile Scenario: The next page and the search combine Given the search "wall" is active and the response total is above the loaded count When the user activates "Load more" Then GET /v1/media/ is sent with q=wall, page=2 and size=25 Rule: Empty and failed Scenario: Empty library Given the Library tab is selected and GET /v1/media/?page=1&size=25 resolved with no item Then the images icon shows with "Your library is empty", "Images you upload appear here, ready to use in any article." and a primary "Upload an image" And "Insert image" is disabled When the user activates "Upload an image" Then the Upload tab is selected Scenario Outline: The library fails to load Given GET /v1/media/ When the Library tab loads Then an error alert shows in the dialog: icon circle-x, title "Couldn't load your library", text "Check your connection and try again.", and a "Try again" button And "Insert image" is disabled When the user activates "Try again" Then the same request is sent again with a skeleton, and on 200 the tiles replace the alert Examples: | failure | | resolves with a 500 response | | never reaches the server |