Repository navigation
ol-shelf-actions - #13399
ol-shelf-actions#13399
Conversation
POST /works/OLβ¦W/ratings.json and POST /people/β¦/lists.json both answered 405 on the web.py port. Ratings had no entry in DEPRECATED_PATHS at all; the lists entries were there but never took effect, because infogami keys its `pages` registry by the literal regex text, so an equivalent pattern spelled differently registers alongside the old GET-only handler instead of replacing it. Collapsing the five per-seed-type patterns into the one string lists.py actually registers covers all of them β works, books, authors and subjects were shadowed the same way people was. The editions read in the request context also has to declare _method="GET". Without it web.py parses multipart POST bodies there, draining wsgi.input before the handler sees it, which surfaces as an empty form β "Invalid bookshelf" on a shelf post that carried one. This is dev-only routing, but it takes the sitewide star-rating form and the list dropper's "create list" with it, not just the components that follow. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg
The book components render whatever shelf and rating they are handed, so a server-rendered page sets them directly. Anything that renders books on the client β a carousel, a search result list β has to ask, and asking once for the whole batch is what keeps the public payload cacheable and user-agnostic. Ratings had no batch query, only per-work reads; Bookshelves had one but its signature disagreed with the numeric work ids every caller actually passes. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg
Two components that let a reader shelve, rate and list a book from wherever the book appears. ol-shelf-button is the control β a split button for a row, a bookmark for a cover β and ol-book-actions is the popover both shapes open: the four reading-log shelves, a star rating, and an "Add to list" pane with a filter and inline list creation. State is optimistic. The UI updates first and an error toast rolls it back, and neither component writes its own shelf: they emit ol-book-state-change and the surface that owns the book applies it, which then flows back down. That is what keeps two cards for the same work in step, and it makes an optimistic update and its rollback the same code path. Signed out, the trigger stands alone and its click goes to login with the intent remembered β without user-key no popover is built at all. ol-icon gains a `filled` attribute along the way, for the on/off states these need: a saved bookmark, a rated star. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg
The domain layer gets its own group: everything in it knows what a book is, while the generic pieces it composes β Button, Popover β stay where they are. Both demos write to the signed-in reader's real reading log and lists, so the gallery hands them the reader's key; empty sends the demo to log in instead. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg
The surface the icon-shaped shelf button was built to sit on: a cover at a fixed 2:3 ratio with an `overlay` slot that owns its top-right corner, so a consumer never positions the control itself and clicking it does not follow the cover's link. With no artwork it draws a title/author panel rather than leaving a hole, so a shelf of covers keeps its rhythm. With `tooltip` a pointer gets the title, year and author over the cover, for layouts with no room to print them underneath. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg
700 on a generated cover was heavier than any real jacket; 500 reads as a title without shouting. The padding drops a step at both sizes β a step further at small, where a 72px cover was giving up a quarter of its width to inset β and the small title drops to 12px, below where the title scale bottoms out. `size` reflects now, so the small-size styles apply when it is set as a property and not only when it is authored as an attribute. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg
Both components were overriding ol-popover's own bottom-center default with bottom-end. Right-aligning a 300px panel to a narrow trigger throws almost all of it to one side, so it reads as unattached β centering keeps the panel under the thing that opened it. Nothing is at risk near a viewport edge: the positioner clamps horizontally either way, and transform-origin already tracks the trigger's centre, so the open still radiates from the button. `placement` is still there for a caller that needs an edge alignment. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg
A popover panel is nearly always wider than the control that opens it, so centering makes it straddle the trigger and edge-aligning to the far side throws it clear across. Leading edges together is the alignment that reads as attached, and it was already what every menu on the site asked for by hand: ol-menu-popover, ol-select-popover and ol-options-popover each spelled out bottom-start, and those overrides can go. The book components stop naming a placement at all β the pass-through only sets the attribute when a caller supplies one, so an unset `placement` is ol-popover's choice rather than a copy of it. The one surface that genuinely wants centering, the masthead's Browse menu, already says so explicitly, so nothing changes for it. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg
The book-cover overlay example never passed user-key, so its shelf button rendered permanently signed-out and every click went to the login page instead of opening the actions popover. It joins the shelf-button demos on the same work, so the shared listener there keeps all three in step. The example gets an explicit code snippet so the copyable markup shows /people/you rather than the reader's own key.
"On a shelf" hardcoded shelf 1, 2 and 3 on three placeholder works to illustrate the three states, but the buttons were live: clicking one wrote to the reader's real account while the shelf it displayed stayed whatever the attribute said. A demo that shows one thing and does another is worse than no demo, and the note admitting it was longer than the example earned. The two remaining demos open on the reader's real state for The Secret Garden, so the shelved appearance is already on show β and honestly. Claude-Session: https://claude.ai/code/session_01XJ9QNXNg3UfbtH3dmDxUfV
Found while putting these on a real surface; they belong with the components rather than with the first page to use them. bookshelves.json answers a rejected write with 200 and an `error` key, so checking the response status alone let a failed write look like a save β the optimistic update stayed on screen and the reader believed it stuck. Signed out, the button sent the reader to the book's own page after logging in. On a book page that is where they already were, but from a list of results it strands them somewhere they never asked to go. It now resumes on the page they were on, as the reading-log dropper does. Matomo's click trigger matches `data-ol-link-track` attributes, which it cannot see inside a shadow root β events retarget to the host on the way out. So shelf, rating and list actions now push to `_paq` directly, under the same category/action names the dropper's forms use, and the existing dashboards stay continuous across a migration. Adds `hide-rating`, which drops the popover's stars for surfaces that already show a rating control for the same book. The reading log is the case that needs it: its `macros.StarRatings` is a real input, not the aggregate byline that search results carries. Claude-Session: https://claude.ai/code/session_01XJ9QNXNg3UfbtH3dmDxUfV
Marking a book Already Read now slides in a pane asking for a finish date, the way "Add to list" already slides in. Answering was previously a separate prompt rendered outside the popover, which opened a modal β two overlay paradigms for one question. The check-ins schema stores partial dates: a year alone, or a year and month, are both valid. That is what makes "In 2026" offerable as one tap rather than a date picker. "Other date" swaps in year/month/day selects, each enabling the next, in the same pane rather than a fourth one β the lists pane swaps in its create form the same way. Month names come from Intl in the page's language, so they cost no translated strings. Only an explicit shelf choice opens it. Rating a book moves it to Already Read server-side too, and interrupting that would turn one tap into two. Nor does it open when the reader is taking a book *off* the shelf. The pane track was hardcoded to two panes β 200% wide, halves, a class that translated by -50%. Width and slide now both derive from the PANES list, so a third pane is an entry there plus a `_render*`. Escape in any sub-pane goes back one step instead of only in the lists pane. Not yet handled: showing or editing a date that already exists. The finish date is write-once from here until we decide how the row surfaces it β relabelling the main button "Read in 2026" is one idea. Claude-Session: https://claude.ai/code/session_01XJ9QNXNg3UfbtH3dmDxUfV
The tooltip opt-in on ol-book-cover is gone β every cover renders the hover card, with ol-tooltip's pointer media query still deciding when it arms. Also drops the implementation-detail notes from the shelf-button and book-actions gallery pages. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD
β¦ve#13400 Remove from shelf row, read-date display and amending (readDate/eventId through to setCheckIn's event_id), quickYears, the Other-date disclosure, the ol-list-created bridge and ol-book-check-in event, press feedback, fixed row heights, 16px list checkboxes, per-option select seeding, and the accent-color token fix β taken from the search-results branch so this PR carries the component work. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD
testing.openlibrary.org only routes already-launched paths to its FastAPI container, so the new /reading-state.json (and the bookshelves route its nginx is missing) 500 with the web.py deprecation error there. The /_fast/ prefix exposes the whole FastAPI app on testing, so prefix every request with it on that host until the olsystem nginx routes land. Production has no /_fast and is unaffected. Claude-Session: https://claude.ai/code/session_01D5yRkR7WnUVDXxjc9Gv6Tz
Only the design-system demos called the endpoint: search results pass the state as server-rendered attributes, and the client-rendered surfaces that genuinely need a batch endpoint (carousels) are not in this PR β it can land with them instead, alongside its olsystem routing. The demos now get shelf/rating the same way real surfaces do, from design.py at render time. Also reverts the /_fast testing shim; the testing nginx routing is being fixed properly instead. Claude-Session: https://claude.ai/code/session_01D5yRkR7WnUVDXxjc9Gv6Tz
The Lit bundle can import the webpack modules directly, so the copied analytics and pending-action-cookie helpers go away, the lists partial URL regains its lang handling via buildPartialsUrl, and the shelf label/event vocabulary now lives once in books-api. The popover's three panes render on first click instead of at page load, which was building thousands of nodes on book-heavy pages for popovers never opened. Also drops the fictional legacy-dropper list-created protocol, a fake fmt compatibility re-export, and shares the lists route regex between lists.py and deprecated_handler.py instead of a keep-in-sync comment. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD
RayBB
left a comment
There was a problem hiding this comment.
Looks beautiful but have some first draft code changes here.
Here is some feedback I think we should really consider. It's just the first round based on high level things.
1. The date helpers do not belong in OlBookActions.js
The file has date functions inside it (MONTHS(), formatReadDate(), quickYears(), partialDate()).
We checked the client code. There is no shared date utility yet β these are the first. But the component also does not own this idea. Dates are not part of a book-actions popover; other components will need them next.
Suggestion: Give the date helpers a shared home beside the other shared modules (lit/utils/books-api.js, lit/utils/labels.js). Then the component keeps only its panes, and the date logic is there for whatever comes next.
2. Four copies of the same write-then-undo pattern
Four places repeat the same flow: change the UI, send the request, on success record the event, on failure undo the change.
OlShelfButton._onMainClickOlBookActions._postShelfOlBookActions._onRateOlBookActions._onListToggle
Problem: Four copies of one idea will drift apart over time.
Suggestion: We think there is a better shape here than four copies. Please look at these four flows together before merge and see if they can share something.
3. The lists cache state looks tangled
The list data is held in more than one place: a module-level promise, a copy on each component instance, and manual sync code after creates.
Problem: When one piece of data lives in several places, they can stop agreeing, and readers must trace all the sync code to trust it.
Suggestion: This looks like it should be improved before more surfaces build on it. We do not have one answer in mind β please consider how to keep fewer copies of this state.
The row rendered the date but nothing ever supplied one: the popover did not keep its own copy after a save, and the design demo never server-rendered it. Now the component updates readDate/eventId optimistically like shelf and rating β which also makes a re-save amend the event instead of recording a second finish β and the demo passes the latest finish event down. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD
Review feedback (internetarchive#13399): the date helpers belonged beside the other shared utils, and the lists cache lived in three places (a module-level promise, per-instance copies, and event-based syncing) that could drift. lit/utils/dates.js takes MONTHS, formatReadDate, quickYears and partialDate unchanged. lit/utils/lists-store.js holds the one canonical lists object; components read getLists() and re-render on subscribe, and the store owns each optimistic update and its rollback. That retires the per-instance copies, the document-level ol-list-created listener, and the promise-swap after a create. The event still fires outward as documented API. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD
|
@RayBB Thanks for the review. 1 and 3 are addressed in 1a1dd17; here's where each landed. 1. Date helpers β Moved to 3. Lists state β There's now one canonical copy in 2. The write-then-undo copies β Looked at all four together and decided to leave them for now, for a couple of reasons. The store refactor already absorbed one: the list-toggle undo now lives in Ready for another review. |
The pane could set a finish date and replace it, but never take it back. DELETE /check-ins/:id closes that gap; `send` splits out of `request` because that endpoint answers 200 with an empty body. The three take-back actions now read alike. Clear rating, Clear date and Remove from shelf are quiet text links rather than icon rows, leaving the rows that give an answer as the only ones carrying weight. Remove from shelf is offered on Already Read alone, since clicking any other active shelf row toggles it off β Already Read's row leads to the date pane instead, so it was the one shelf with no way out. The selected date row takes the active shelf's blue for the same reason both mark an answer. The date also rides the Already Read row only while the book is on that shelf. Moving a book keeps its check-ins and only coming off the shelves entirely deletes them, so the date outlives the shelf it was given on and would otherwise show against a shelf the book has left. The date pane still opens on it.
|
@cdrini
Added 'Clear date' action on the check-in pane.
Two changes to Remove from shelf:
1. Removed icon and used smaller font size to match the other quieter links for the clear/remove behaviors in this popover
2. Only showing the Remove from shelf action when the book is on the Already Read shelf
|
RayBB
left a comment
There was a problem hiding this comment.
I finally got around to giving this one another look in depth.
Code wise, all of my concerns were fully addressed and I don't see anything else at all that should be a show stopper.
So, as far as I am concerned this is good to go in terms of the code.
I also have tried it on testing and the UX seems really solid to me.
Menu and popover rows each sized themselves, so a shelf row, a select option and a menu item all landed on slightly different heights. Promote the shelf-actions local row height to a shared --menu-row-height token and apply it as a min-height across the menu, select and options popovers, letting described or wrapping rows grow past it. The link, text and date fields sit below 16px, which makes iOS zoom the page on focus; bump them to the large body size on mobile. On mobile the lists pane now focuses its back button rather than the filter input, so the soft keyboard doesn't cover the lists the user came to see.
Curation runs in bursts β many books into one list in a sitting β but every book is a fresh popover, so the working list had to be hunted for each time. The server already orders lists by last_modified and a seeds write bumps it, so the recency was there; the client just froze the order it loaded with and, on a listing surface, that page is the whole session. The store now moves a toggled list to the front, matching what a reload would show, and utils/recent-lists.js carries the last three lists per user across page loads in localStorage. The most recent one gets a one-tap row on the main pane, so the common add never opens the lists pane at all β which matters most on mobile, where the filter costs a tap and a soft keyboard over the names and ordering is the only lever left. From five lists up the recent ones are also pinned above the rest, and Enter in the filter toggles the first row for readers on a keyboard. Order and recents are snapshotted when the popover opens and held until it closes, so a row never moves under the reader mid-session. The filter itself now answers to a shared FILTER_THRESHOLD, the count ol-select-popover was already using, so the two pickers offer their field on the same terms instead of by coincidence. Below it the pane is rows and nothing else, and with no lists at all the pane becomes the create form rather than a filter above an empty-state line.
β¦-book-actions Resolves a semantic conflict git could not see: upstream 5c46a77 dropped Component.use_when from design.py and the line rendering it from _example.html.jinja, while this branch added three Books rows that still passed a use_when string positionally. Both sides merged cleanly and the result raised at import time, taking mypy and the whole pytest collection with it. The three rows drop the string; each of their partials already opens with the same description in its ds-lead paragraph.
β¦-book-actions # Conflicts: # openlibrary/components/lit/index.js
internetarchive#13490 tightened Ratings.add so rating a book only auto-shelves it as Already Read when it's unshelved or on Want to Read β Currently Reading and Stopped Reading are explicit choices the server won't overwrite. The legacy dropper got the matching client-side guard there; <ol-shelf-actions> was written against the old rule, so rating a Currently Reading book showed Already Read in the panel and in ol-book-state-change while the server kept it where it was, with no failure to roll the optimistic update back.
Picking a shelf or star disabled the focused control for the request, which dropped focus to the document with nothing announced. Rows now stay enabled behind an aria-busy dim, and a live region reads out shelf, rating and date changes. Shelf rows become aria-pressed toggle buttons (the menuitemradio role had no menu and failed axe), stars keep stable names with one tab stop and arrow keys, the check-in question labels its group, and the split button's main half names the book and its pressed state. Labels can be plural sets picked by Intl.PluralRules, so list counts no longer read "1 items". ol-popover drops the aria-controls that could never resolve across the shadow boundary and marks the mobile tray aria-modal.
The panel offered two ways to take back an answer β a "Remove from shelf" link on the main pane and a "Clear date" link on the check-in pane β and neither said what the other did. Both are gone, replaced by a single "I didn't read this" row at the foot of the check-in pane. It takes the book off the shelf, and the server deletes the book's check-ins with it, so one request clears the status and the date together. Adds lucide's ban icon for the row. Also in the tree: the lists pane pins the lists a book is already on above the recently used ones, since which of them it is on is the first question that pane answers; and menu rows and drawer items drop the :active press squeeze, keeping only the fill β a shrinking row reads as the panel moving rather than the row being pressed.
Light-DOM CSS could not tell a saved book from an unsaved one, since the shelf lived only as a property. Reflecting it gives the page `ol-shelf-button[shelf]`, which a carousel uses to keep the saved mark visible while showing the rest on hover.
A page that reveals the bookmark on hover needs to keep it visible while its popover is open, and focus inside a top-layer popover does not register as :focus-within on the host in Chrome. The button now sets an `open` attribute from the popover's open and close events, ignoring a close the panel cancels (Escape stepping back a pane).
The badge over a cover said only "saved"; now it says which shelf, with the same glyphs the popover rows use: the bookmark filled for Want to Read, and book-open, circle-check or circle-pause in blue for the rest. Only the bookmark fills well β the others are stroked shapes that would turn into blobs. The icon map moves to books-api.js next to the label map so both components draw a shelf the same way.
|
I'm going to go ahead and merge this in. It has been reviewed, is low-risk as this is not patron facing, and I'd rather have staff invest time in the follow-up PR which implements this component across the site and does impact patrons. |
β¦ page (#13588) * Route the reading-log JSON writes to FastAPI in dev POST /works/OLβ¦W/ratings.json and POST /people/β¦/lists.json both answered 405 on the web.py port. Ratings had no entry in DEPRECATED_PATHS at all; the lists entries were there but never took effect, because infogami keys its `pages` registry by the literal regex text, so an equivalent pattern spelled differently registers alongside the old GET-only handler instead of replacing it. Collapsing the five per-seed-type patterns into the one string lists.py actually registers covers all of them β works, books, authors and subjects were shadowed the same way people was. The editions read in the request context also has to declare _method="GET". Without it web.py parses multipart POST bodies there, draining wsgi.input before the handler sees it, which surfaces as an empty form β "Invalid bookshelf" on a shelf post that carried one. This is dev-only routing, but it takes the sitewide star-rating form and the list dropper's "create list" with it, not just the components that follow. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg * Add /reading-state.json for a batch of works The book components render whatever shelf and rating they are handed, so a server-rendered page sets them directly. Anything that renders books on the client β a carousel, a search result list β has to ask, and asking once for the whole batch is what keeps the public payload cacheable and user-agnostic. Ratings had no batch query, only per-work reads; Bookshelves had one but its signature disagreed with the numeric work ids every caller actually passes. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg * Add ol-shelf-button and ol-book-actions Two components that let a reader shelve, rate and list a book from wherever the book appears. ol-shelf-button is the control β a split button for a row, a bookmark for a cover β and ol-book-actions is the popover both shapes open: the four reading-log shelves, a star rating, and an "Add to list" pane with a filter and inline list creation. State is optimistic. The UI updates first and an error toast rolls it back, and neither component writes its own shelf: they emit ol-book-state-change and the surface that owns the book applies it, which then flows back down. That is what keeps two cards for the same work in step, and it makes an optimistic update and its rollback the same code path. Signed out, the trigger stands alone and its click goes to login with the intent remembered β without user-key no popover is built at all. ol-icon gains a `filled` attribute along the way, for the on/off states these need: a saved bookmark, a rated star. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg * Give the design gallery a Books section The domain layer gets its own group: everything in it knows what a book is, while the generic pieces it composes β Button, Popover β stay where they are. Both demos write to the signed-in reader's real reading log and lists, so the gallery hands them the reader's key; empty sends the demo to log in instead. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg * Add ol-book-cover The surface the icon-shaped shelf button was built to sit on: a cover at a fixed 2:3 ratio with an `overlay` slot that owns its top-right corner, so a consumer never positions the control itself and clicking it does not follow the cover's link. With no artwork it draws a title/author panel rather than leaving a hole, so a shelf of covers keeps its rhythm. With `tooltip` a pointer gets the title, year and author over the cover, for layouts with no room to print them underneath. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg * Lighten and tighten the blank cover 700 on a generated cover was heavier than any real jacket; 500 reads as a title without shouting. The padding drops a step at both sizes β a step further at small, where a 72px cover was giving up a quarter of its width to inset β and the small title drops to 12px, below where the title scale bottoms out. `size` reflects now, so the small-size styles apply when it is set as a property and not only when it is authored as an attribute. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg * Centre the actions popover under its trigger Both components were overriding ol-popover's own bottom-center default with bottom-end. Right-aligning a 300px panel to a narrow trigger throws almost all of it to one side, so it reads as unattached β centering keeps the panel under the thing that opened it. Nothing is at risk near a viewport edge: the positioner clamps horizontally either way, and transform-origin already tracks the trigger's centre, so the open still radiates from the button. `placement` is still there for a caller that needs an edge alignment. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg * Make bottom-start the popover default A popover panel is nearly always wider than the control that opens it, so centering makes it straddle the trigger and edge-aligning to the far side throws it clear across. Leading edges together is the alignment that reads as attached, and it was already what every menu on the site asked for by hand: ol-menu-popover, ol-select-popover and ol-options-popover each spelled out bottom-start, and those overrides can go. The book components stop naming a placement at all β the pass-through only sets the attribute when a caller supplies one, so an unset `placement` is ol-popover's choice rather than a copy of it. The one surface that genuinely wants centering, the masthead's Browse menu, already says so explicitly, so nothing changes for it. Claude-Session: https://claude.ai/code/session_01CCv12GjjKDZZGeTGQb6GHg * Stop the overlay demo bouncing to login The book-cover overlay example never passed user-key, so its shelf button rendered permanently signed-out and every click went to the login page instead of opening the actions popover. It joins the shelf-button demos on the same work, so the shared listener there keeps all three in step. The example gets an explicit code snippet so the copyable markup shows /people/you rather than the reader's own key. * Drop the shelf-button demo that showed a shelf it did not have "On a shelf" hardcoded shelf 1, 2 and 3 on three placeholder works to illustrate the three states, but the buttons were live: clicking one wrote to the reader's real account while the shelf it displayed stayed whatever the attribute said. A demo that shows one thing and does another is worse than no demo, and the note admitting it was longer than the example earned. The two remaining demos open on the reader's real state for The Secret Garden, so the shelved appearance is already on show β and honestly. Claude-Session: https://claude.ai/code/session_01XJ9QNXNg3UfbtH3dmDxUfV * Fix four things the components got wrong Found while putting these on a real surface; they belong with the components rather than with the first page to use them. bookshelves.json answers a rejected write with 200 and an `error` key, so checking the response status alone let a failed write look like a save β the optimistic update stayed on screen and the reader believed it stuck. Signed out, the button sent the reader to the book's own page after logging in. On a book page that is where they already were, but from a list of results it strands them somewhere they never asked to go. It now resumes on the page they were on, as the reading-log dropper does. Matomo's click trigger matches `data-ol-link-track` attributes, which it cannot see inside a shadow root β events retarget to the host on the way out. So shelf, rating and list actions now push to `_paq` directly, under the same category/action names the dropper's forms use, and the existing dashboards stay continuous across a migration. Adds `hide-rating`, which drops the popover's stars for surfaces that already show a rating control for the same book. The reading log is the case that needs it: its `macros.StarRatings` is a real input, not the aggregate byline that search results carries. Claude-Session: https://claude.ai/code/session_01XJ9QNXNg3UfbtH3dmDxUfV * Ask when the reader finished the book Marking a book Already Read now slides in a pane asking for a finish date, the way "Add to list" already slides in. Answering was previously a separate prompt rendered outside the popover, which opened a modal β two overlay paradigms for one question. The check-ins schema stores partial dates: a year alone, or a year and month, are both valid. That is what makes "In 2026" offerable as one tap rather than a date picker. "Other date" swaps in year/month/day selects, each enabling the next, in the same pane rather than a fourth one β the lists pane swaps in its create form the same way. Month names come from Intl in the page's language, so they cost no translated strings. Only an explicit shelf choice opens it. Rating a book moves it to Already Read server-side too, and interrupting that would turn one tap into two. Nor does it open when the reader is taking a book *off* the shelf. The pane track was hardcoded to two panes β 200% wide, halves, a class that translated by -50%. Width and slide now both derive from the PANES list, so a third pane is an entry there plus a `_render*`. Escape in any sub-pane goes back one step instead of only in the lists pane. Not yet handled: showing or editing a date that already exists. The finish date is write-once from here until we decide how the row surfaces it β relabelling the main button "Read in 2026" is one idea. Claude-Session: https://claude.ai/code/session_01XJ9QNXNg3UfbtH3dmDxUfV * Make the cover hover card always on and trim the design notes The tooltip opt-in on ol-book-cover is gone β every cover renders the hover card, with ol-tooltip's pointer media query still deciding when it arms. Also drops the implementation-detail notes from the shelf-button and book-actions gallery pages. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD * Give the split shelf button the raised secondary treatment * Bring the check-in editing and popover polish over from #13400 Remove from shelf row, read-date display and amending (readDate/eventId through to setCheckIn's event_id), quickYears, the Other-date disclosure, the ol-list-created bridge and ol-book-check-in event, press feedback, fixed row heights, 16px list checkboxes, per-option select seeding, and the accent-color token fix β taken from the search-results branch so this PR carries the component work. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD * Route book-component API calls through /_fast on testing testing.openlibrary.org only routes already-launched paths to its FastAPI container, so the new /reading-state.json (and the bookshelves route its nginx is missing) 500 with the web.py deprecation error there. The /_fast/ prefix exposes the whole FastAPI app on testing, so prefix every request with it on that host until the olsystem nginx routes land. Production has no /_fast and is unaffected. Claude-Session: https://claude.ai/code/session_01D5yRkR7WnUVDXxjc9Gv6Tz * Drop /reading-state.json and server-render the demo state Only the design-system demos called the endpoint: search results pass the state as server-rendered attributes, and the client-rendered surfaces that genuinely need a batch endpoint (carousels) are not in this PR β it can land with them instead, alongside its olsystem routing. The demos now get shelf/rating the same way real surfaces do, from design.py at render time. Also reverts the /_fast testing shim; the testing nginx routing is being fixed properly instead. Claude-Session: https://claude.ai/code/session_01D5yRkR7WnUVDXxjc9Gv6Tz * Keep the shelf button's hover blue when it is selected Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD * Reuse the webpack-side helpers and build popover panes on demand The Lit bundle can import the webpack modules directly, so the copied analytics and pending-action-cookie helpers go away, the lists partial URL regains its lang handling via buildPartialsUrl, and the shelf label/event vocabulary now lives once in books-api. The popover's three panes render on first click instead of at page load, which was building thousands of nodes on book-heavy pages for popovers never opened. Also drops the fictional legacy-dropper list-created protocol, a fake fmt compatibility re-export, and shares the lists route regex between lists.py and deprecated_handler.py instead of a keep-in-sync comment. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD * Show the check-in date on the Already Read row The row rendered the date but nothing ever supplied one: the popover did not keep its own copy after a save, and the design demo never server-rendered it. Now the component updates readDate/eventId optimistically like shelf and rating β which also makes a re-save amend the event instead of recording a second finish β and the demo passes the latest finish event down. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD * Move date helpers to a shared module and centralize lists state Review feedback (#13399): the date helpers belonged beside the other shared utils, and the lists cache lived in three places (a module-level promise, per-instance copies, and event-based syncing) that could drift. lit/utils/dates.js takes MONTHS, formatReadDate, quickYears and partialDate unchanged. lit/utils/lists-store.js holds the one canonical lists object; components read getLists() and re-render on subscribe, and the store owns each optimistic update and its rollback. That retires the per-instance copies, the document-level ol-list-created listener, and the promise-swap after a create. The event still fires outward as documented API. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD * Roll optimistic writes back from a snapshot A rating moves two properties, so restoring them by hand is a place to forget one. _mutate() captures whatever it is about to change and puts all of it back on failure, and refuses to start while another write is in flight β the disabled rows only take effect on the next render, so two fast clicks could otherwise both go out. The shelf button owns no state to snapshot, but it had the same race: guard its main click too. Claude-Session: https://claude.ai/code/session_011AqgrMKn29JVncut49JNtf * Key the filtered list rows with repeat() The Add-to-list pane is the one list in ol-book-actions whose membership changes between renders, so key its rows by list key instead of letting Lit reconcile by index. The other mapped lists (shelves, quick years, date options) are static and stay as maps. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD * Rename ol-book-actions to ol-shelf-actions * Clear the check-in date when a book leaves its shelf Removing a book from a shelf deletes its check-in events server-side, but the popover kept its copy of the date and event id, so the next check-in posted an event id the server no longer had and came back 404. * Add a Clear date action to the check-in pane The pane could set a finish date and replace it, but never take it back. DELETE /check-ins/:id closes that gap; `send` splits out of `request` because that endpoint answers 200 with an empty body. The three take-back actions now read alike. Clear rating, Clear date and Remove from shelf are quiet text links rather than icon rows, leaving the rows that give an answer as the only ones carrying weight. Remove from shelf is offered on Already Read alone, since clicking any other active shelf row toggles it off β Already Read's row leads to the date pane instead, so it was the one shelf with no way out. The selected date row takes the active shelf's blue for the same reason both mark an answer. The date also rides the Already Read row only while the book is on that shelf. Moving a book keeps its check-ins and only coming off the shelves entirely deletes them, so the date outlives the shelf it was given on and would otherwise show against a shelf the book has left. The date pane still opens on it. * Unify menu row height and fix mobile input zoom in popovers Menu and popover rows each sized themselves, so a shelf row, a select option and a menu item all landed on slightly different heights. Promote the shelf-actions local row height to a shared --menu-row-height token and apply it as a min-height across the menu, select and options popovers, letting described or wrapping rows grow past it. The link, text and date fields sit below 16px, which makes iOS zoom the page on focus; bump them to the large body size on mobile. On mobile the lists pane now focuses its back button rather than the filter input, so the soft keyboard doesn't cover the lists the user came to see. * Remember the lists a reader is working in Curation runs in bursts β many books into one list in a sitting β but every book is a fresh popover, so the working list had to be hunted for each time. The server already orders lists by last_modified and a seeds write bumps it, so the recency was there; the client just froze the order it loaded with and, on a listing surface, that page is the whole session. The store now moves a toggled list to the front, matching what a reload would show, and utils/recent-lists.js carries the last three lists per user across page loads in localStorage. The most recent one gets a one-tap row on the main pane, so the common add never opens the lists pane at all β which matters most on mobile, where the filter costs a tap and a soft keyboard over the names and ordering is the only lever left. From five lists up the recent ones are also pinned above the rest, and Enter in the filter toggles the first row for readers on a keyboard. Order and recents are snapshotted when the popover opens and held until it closes, so a row never moves under the reader mid-session. The filter itself now answers to a shared FILTER_THRESHOLD, the count ol-select-popover was already using, so the two pickers offer their field on the same terms instead of by coincidence. Below it the pane is rows and nothing else, and with no lists at all the pane becomes the create form rather than a filter above an empty-state line. * fix(shelf-actions): don't optimistically move a rated book off its shelf #13490 tightened Ratings.add so rating a book only auto-shelves it as Already Read when it's unshelved or on Want to Read β Currently Reading and Stopped Reading are explicit choices the server won't overwrite. The legacy dropper got the matching client-side guard there; <ol-shelf-actions> was written against the old rule, so rating a Currently Reading book showed Already Read in the panel and in ol-book-state-change while the server kept it where it was, with no failure to roll the optimistic update back. * fix(shelf-actions): keep focus and speak changes for screen readers Picking a shelf or star disabled the focused control for the request, which dropped focus to the document with nothing announced. Rows now stay enabled behind an aria-busy dim, and a live region reads out shelf, rating and date changes. Shelf rows become aria-pressed toggle buttons (the menuitemradio role had no menu and failed axe), stars keep stable names with one tab stop and arrow keys, the check-in question labels its group, and the split button's main half names the book and its pressed state. Labels can be plural sets picked by Intl.PluralRules, so list counts no longer read "1 items". ol-popover drops the aria-controls that could never resolve across the shadow boundary and marks the mobile tray aria-modal. * test(ol-popover): stop asserting the aria-controls that could not resolve * feat(shelf-actions): one way off Already Read, in the check-in pane The panel offered two ways to take back an answer β a "Remove from shelf" link on the main pane and a "Clear date" link on the check-in pane β and neither said what the other did. Both are gone, replaced by a single "I didn't read this" row at the foot of the check-in pane. It takes the book off the shelf, and the server deletes the book's check-ins with it, so one request clears the status and the date together. Adds lucide's ban icon for the row. Also in the tree: the lists pane pins the lists a book is already on above the recently used ones, since which of them it is on is the first question that pane answers; and menu rows and drawer items drop the :active press squeeze, keeping only the fill β a shrinking row reads as the panel moving rather than the row being pressed. * Reflect the shelf on ol-shelf-button's host Light-DOM CSS could not tell a saved book from an unsaved one, since the shelf lived only as a property. Reflecting it gives the page `ol-shelf-button[shelf]`, which a carousel uses to keep the saved mark visible while showing the rest on hover. * Carry the popover's open state on ol-shelf-button's host A page that reveals the bookmark on hover needs to keep it visible while its popover is open, and focus inside a top-layer popover does not register as :focus-within on the host in Chrome. The button now sets an `open` attribute from the popover's open and close events, ignoring a close the panel cancels (Escape stepping back a pane). * Draw the shelf's own glyph on the icon shape once a book is shelved The badge over a cover said only "saved"; now it says which shelf, with the same glyphs the popover rows use: the bookmark filled for Want to Read, and book-open, circle-check or circle-pause in blue for the rest. Only the bookmark fills well β the others are stroked shapes that would turn into blobs. The icon map moves to books-api.js next to the label map so both components draw a shelf the same way. * Put the shelf button on carousels, rows and the book page The ol-shelf-button / ol-shelf-actions components were gallery-only. This wires them across the site and retires the legacy My Books dropper everywhere a work is available to shelve. Carousel cards get the icon shape over the cover, rendered without any reader state because that HTML is cached across readers or fetched lazily. Search results, search inside, trending, author works, list pages, the reading log and the book page get the split shape, with the reader's state rendered in from one batched lookup per page (reading_state_for). A new page-level controller, book-state.js, is the surface the stateless button needs: it fans every change out to each button for the same work, hands out the translated labels and the reader's key from the page shell, and hydrates carousel buttons from the new GET /partials/ReadingState.json in batches, re-running for lazy carousels and load-more. The book page's "lists this book is on" strip is now fed by lists/active-showcase.js from the popover's ol-list-created and new ol-list-change events. The legacy dropper survives only where there is no work to shelve: author pages and orphaned editions. * Show the carousel bookmark on hover on desktop, always once shelved Six white circles across a carousel is a lot of chrome for an action most readers won't take on most covers. On a fine pointer the bookmark now appears on hover or focus, which also keeps it while its popover is open, and stays visible once the book is on a shelf, where the filled mark reads as status. Touch devices see it always; there is nothing to hover. * Keep the carousel bookmark while its popover is open Focus inside a top-layer popover does not register as :focus-within on the host in Chrome, so the bookmark faded out under its own menu. The button now carries an `open` attribute while the popover is open; the carousel keys on it. * Reveal the carousel bookmark instantly No fade in or out: the bookmark appears the moment the cover is hovered, which also removes the reduced-motion override it needed. * Put the shelf button in the search modal Each result row gets an "outline" ol-shelf-button: the icon variant's glyph in a bordered square, with the shelf glyph drawn solid once the book is shelved and a short crossfade when it changes. Rows start pending until the reading state arrives so the popover can't act on a guess, and ol-popover grows a block-outside-clicks guard so the click that dismisses the menu doesn't also follow the result link beneath it. The check-in pane only offers a way off the shelf when amending an existing entry. Alongside, the row's "Readable" status moves from a trailing column to a badge beside the title, with a single muted "Readable in <language>" badge when the readable copy isn't in the patron's site language. * Show the shelf badge on the cover in grid views In the 145px grid column the split's label had exactly the width of "Want to Read", so subpixel rounding turned it into "Want to Reβ¦", and "Currently Reading" or a longer translation never fit at all. Grid cards are the same shape as carousel cards, so they now carry the same cover-corner badge; the list keeps the labelled split. The card macro renders both slots and list_books.js moves the button between them when the layout flips. The split's chevron half also shrinks to 32px so the label has room in the list at narrow widths. Also lets a click on a button inside a shadow root (the shelf button's own controls) through the ILE selection manager, which only saw the host, and trims comments across the shelf-button files. * Add space between the shelf button and star rating in reading log rows * Open the menu from the main half once a book is on a reading shelf Want to Read stays a one-tap bookmark. Currently, Already and Stopped Reading carry dates, ratings and goal progress, so leaving one goes through the popover, which already takes Already Read via its date pane. Before this, one tap on the main half posted the current shelf, which toggled it off and deleted the book's check-ins. * Tell the shelf buttons when the star widget rates a book The widget still looked for a legacy dropper, which no longer exists on a work page or an Already Read row, so the server's auto-shelve to Already Read never reached the button beside it. * Keep the popover's stars in grid, where the row's star form is hidden * Fetch an unknown book whole when it changes while the search modal wants state A patch alone would let the popover act on the shelf without knowing the date, so the modal keeps refusing partial pictures and refetches instead. * Put the shelf badge on the My Books mobile showcase The showcase already worked out the work key for each cover and then dropped it; the desktop carousel beside it has carried the badge since the rollout. * Keep a shelf page honest when a book leaves it A reading log shelf page lists one shelf, and until now a row stayed after its book moved, saying the new shelf as if it belonged there, while the sidebar counts and the heading held the old numbers. The row still stays: removing it would take the undo with it and jump the layout under the reader. It now carries a note, "Moved to Already Read" or "Removed from shelf", with an Undo that puts the book back the way the button would. Every count on the page that names a shelf moves with the change: the sidebar, the page heading, and the My Books carousel titles. The next load drops the row. * Let the popover decide for itself when a star form already covers the book Three templates each had their own formula for hide-rating, and the layout toggle updated none of them: a row rendered in list with a star form kept hide-rating in grid, where CSS hides that form, leaving no rating control at all. The popover now checks at each open for a visible star form with the same work key and drops its stars only then. hide-rating stays as an explicit override. * Make one server helper the only producer of the shelf button Four templates wrote the tag out by hand, each with its own copy of the work-and-edition sniffing: the row macro, the book page widget, the desktop carousel card and the mobile showcase. The mobile one computed a work key and then dropped it, which is the class of bug this invites. shelf_button_for(doc, variant, reading_state, cached) resolves the work, edition and title for a Solr or Infogami work or edition, and renders the Jinja partial. Rows pass the page's reading_state; a cached carousel card asks for no reader and no state, and book-state.js fills both in. The partial now takes hydrated explicitly for that case. * Retire the legacy My Books dropper It survived for two surfaces, the author sidebar and orphaned-edition rows, and on both there is no work to shelve, so all it rendered was an "Add to List" button. Its reading-log forms, check-in prompt and check-in components had no page left to render on. Those two surfaces now get the same ol-shelf-button as everything else, with a lists-only mode: the split shape becomes one "Add to list" trigger, the icon shape a list-plus glyph, and the popover opens on its lists pane with the seed's name as heading. shelf_button_for() emits it for an author or an edition without a work. Gone with it: the generic dropper shell, which had no other renderer (the access button's caret menu is a native details element and keeps its rules in generic-dropper.css); the dropper's lists HTML from the MyBooksDropperLists partial; three server helpers only its templates called; the showcase store, which ShowcaseItem now keeps for itself. * Stack My Books grid cards from the top so covers and buttons line up * Keep the popover's list pane current after a list is created The lists partial asked for the user's lists with the one call signature that goes through the memcache memo, and that memo is only evicted by infobase. When the eviction does not land, as it currently does not on testing, a list created moments ago is missing from every popover until the entry is cleared by hand. An explicit limit runs the key query fresh, at the same cap My Books already uses. * Render the facets and subject partials through their Jinja templates * Swap the shelf badge's glyph in place instead of animating the change * Drop the orange focus border on carousel items Slick gives every slide tabindex="-1", so a mouse click on a card's padding focused the slide div and the 5px border ate into the fixed-width card, shifting the cover and button. Keyboard users never reach the slide itself, and nothing sets the .selected class the rule was written for. * Keep the soft keyboard down when the list create form opens on mobile * Add the edition to lists, and flag other copies already there The popover wrote the work as a list's seed and matched only that key. Most entries on existing lists are editions, so a list that already held the book read as empty and ticking it filed the book a second time, while the book page's strip beside it checks both keys and said the opposite. It now records the edition the reader was looking at, which is what the dropper did by default, and a new /partials/WorkEditions.json gives the work's editions, fetched once per book when a popover opens so carousels and other rows pay nothing for it. A list holding another edition, or the book with no edition named, says so under its name instead of reading as empty; ticking such a row adds this edition alongside, because some lists collect editions on purpose, and unticking takes only this one. * Port shelf-button JS tests from Jest to Vitest globals * Unify list marking across shelf popover panes and link to My lists List rows in the lists pane now wear the same plus/check marking as the main pane's recent-list shortcuts (up to two now), with the checkbox kept for assistive tech and the keyboard. A "My lists" link sits below the rows once the reader has a list, so there is a way from filing books to seeing them. Back becomes a ghost ol-button, and "Add to list" states its count as a pluralised "On N lists". * Rename the lists pane footer link to Manage lists * Keep check-in analytics continuous across the move into the popover The date pane reported CheckInPrompt events named by how precise the saved date was, where the old prompt and form reported by which control was used. Each answer now sends the event its predecessor did (SetDateToday, SetDateCurrentYear, SetDateCustom, SubmitCheckIn, EditDate, DeleteCheckIn), so the dashboards built on those names keep counting. Skip stays as the one new event. trackEvent also now sends the athena event ping alongside the Matomo push, which is what a data-ol-link-track click always did; JS-reported events from the shelf components and the search modal were only reaching Matomo. * Drop the divider between recent and other lists in the shelf popover The lists pane now reads as one column; the recent and other groups keep their own accessible labels but no longer draw a rule between them. Also carried in the working tree: the popover reports every open labelled by button shape, and the check-in prompt reports itself shown when it follows a shelf change, so the saves and skips those dashboards count have a denominator. * Skip a work's lazy editions backreference when picking the shelf edition On a work Thing, doc.get("editions") is infogami's backreference LazyObject, which can't be indexed, so every work page 500'd. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Say what the shelf button's server helpers are for reading_state_for() took the reader from the request; it now takes one, as get_reading_states(user, docs), so a caller can see whose state it is asking for. `cached` becomes `async_load`, which is what the flag means to the template: the HTML carries no reader because book-state.js will fill one in, whether it was cached or fetched lazily. The preset shelf ids were spelled out in three templates; shelf_ids() is now the one source. Their count elements move to data-shelf-count-for, and the " (27)" after a carousel title gets its own macro rather than being built twice. The button's state attributes now test `is not none`, so a rating of 0 or event id 0 still renders. * Hold a shelf button until the reader's state arrives hydrate() gave every button the reader's key straight away but awaited ReadingState.json for the shelf, so a carousel bookmark was live while it still read as unshelved. The POST is a toggle: a tap in that window took a book off Want to Read and filled the bookmark to say it had been saved, and the in-flight response, computed before the write, corrected nothing. The buttons now go pending until their state lands, which is the gate the search modal already used, and pending finally looks like something: the trigger dims and drops its clicks, and reports aria-busy. While in here: pool insertions for 100ms instead of setTimeout 0. Each lazy carousel fetches and inserts in its own macrotask, so a zero wait coalesced nothing and a page of carousels asked once per carousel. The row notice and shelf counts follow the names the templates now use. * Let a reader remove a read date and keep the shelf The check-in pane's only destructive row was "I didn't read this", which takes the book off Already Read, so a mis-tapped date could not be cleared on its own β the legacy form's Delete Event could, and deleteCheckIn() has been sitting in books-api.js unused ever since. The amending pane now offers Remove date above it, and CheckInForm / DeleteCheckIn goes back to naming that, rather than being fired from _removeFromShelf beside ReadingLog / RemoveFromShelf, where it counted the opposite of what the old dashboards recorded. * Read the reader's lists from memcache again get_user_lists() passed limit=1000 to get past the memo, because only infobase evicts it and a list made in the popover had to show up. That took every logged-in book page through an uncached infobase things() query, get_many over the lists and get_seeds on each, where master was served from memcache at the default limit. The default limit is back, and the two write paths that the popover reaches β creating a list and deleting one β evict the entry themselves. * Draw the book's list strip from the page's lists store active-showcase.js fetched MyBooksDropperLists.json for itself, so a logged-in book page asked for the reader's lists twice and then kept a second copy of them, updated from the popover's events. It now renders from the shared lists store and re-renders on every change, which makes the strip and the popover the same answer by construction, and removal from the strip goes through the store too. Renamed list-showcase.js, since there is only one. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>


new-dropper.mp4
ol-shelf-buttonshelves a book in a click and opensol-shelf-actionsfor the rest: the four shelves, a star rating, and an "Add to list" pane with a filter, inline list creation, and the lists the patron used most recently pinned to the top (the last one is also a one-tap row on the main pane). Two shapes β a split button for a row, a bookmark for the corner of cover art β so the same component drops into a book row, a search result or a carousel card instead of being rebuilt for each.ol-book-coverprovides that corner. Signed out, the trigger goes to login and the patron lands back where they were.Gallery-only for now, in a new Books group at
/developers/design?section=components. Carousels and search results come next.Technical
The components call only endpoints FastAPI already serves:
bookshelves.json,ratings.json,check-ins, theMyBooksDropperListspartial, and the list seed/create writes. No new endpoints in this PR.The editions read in
request_context.pyneeded_method="GET", without which web.py drains a multipart POST body before the handler sees it β that also fixes the sitewide rating form and the list dropper. Inlists.py, the seed-lists regex the two handlers must share is now one constant, since infogami keys itspagesregistry by the literal regex text. (Earlier versions of this branch also carriedDEPRECATED_PATHSrouting entries; master has since removed that mechanism, so they are gone from the diff.)Accessibility
The popover is a named dialog (
Actions for <title>) and the split button's main half is a toggle button that names the book and its pressed state, so twenty of them in a list still tell a screen reader which is which. Choosing a shelf or a star keeps focus on the control through the request β the rows dim witharia-busyrather than disabling, since disabling the focused button drops focus to the document β and a live region announces every change ("Added to Currently Reading", "Rated 4 of 5. Added to Already Read", "Finished Sep 3, 2026"). The shelf and date rows arearia-pressedtoggle buttons in named groups, the check-in question labels its group, and the stars are a real radio group: one tab stop on the current rating, arrows move and preview like hover, Enter or Space commits, 24px targets at 3:1. Escape steps back a pane before closing, and leaving a sub-pane returns focus to the row that opened it. Labels can be plural sets picked byIntl.PluralRules, so list counts read "1 item". Axe reports no violations on any pane. Two small changes reach everyol-popover: triggers no longer carry anaria-controlsthat could never resolve across the shadow boundary, and the mobile tray declaresaria-modal.Why this is low risk
Ratings.get_users_ratings_of_works, twin of the existing bookshelves one), a type-hint widening, and the design-page demos.request_context.pyis one line, scoped to the solr-editions query read. It stops web.py from parsing multipart POST bodies during that read; nothing passeseditionsin a body./developers/designrenders differently. The new components ship in the site-wide bundle but nothing instantiates them elsewhere yet. The touched shared components are behavior-preserving: thebottom-startpopover default matches what every existing consumer already set explicitly (each one checked), and theol-iconfilledprop is additive.Testing
Sign in and open
/developers/design?section=componentsβ Books. The demos write to your real reading log: shelve the book from either shape, rate it, add it to a list, reload. Both demos are the same work, so one updates the other. Signed out, a trigger goes to login. With a keyboard: open the chevron, Tab through the rows, pick a shelf with Enter and confirm focus stays put, arrow across the stars and rate with Space, Escape back out of the lists pane. With VoiceOver or NVDA, each change should be spoken.Screenshot
See above
Stakeholders
@cdrini @mekarpeles