feat(shopping-list) Shopping list overview with item previews (spec 002) #56

Merged
araemer merged 11 commits from 002-shopping-list-overview-previews into stage 2026-10-04 06:04:26 +00:00
Owner

Implements spec 002: the shopping list overview as a grid of cards with item previews.

UI verification

Verified with /verify-ui on 2026-10-03 against the dev backend running recipe-backend PR #25 (spec 002), as user, with one list owned by admin and shared read-write. All nine checks of plan step 9 passed. The test lists are deleted again.

# Check Result
1 Cards Name, "Standard" badge on the default only, "von " on the shared list only, first 5 open items with quantities ("1,5 l", "10", "Würfel"), "+ 7 weitere" for 12 open items, "Keine offenen Einträge" on both the empty and the all-ticked list. A long item name is cut with an ellipsis on one line; its quantity stays fully visible.
2 Tap target and actions Clicking an item line, the empty card body and the area beside the actions opens the list. Star, pencil and bin do not navigate. Set default, rename and delete (with confirmation) show their result without a reload. The shared list offers only the star.
3 Order Ticking an item moves that list's card to the front; a list created in the UI is first after returning; a rename keeps the position.
4 Refresh One GET /shopping-list/all on return to the app, and the changed card moves to the front. No requests during 16 s of idle. Pull to refresh works with real touch events (CDP); a short pull does nothing.
5 Columns 400×890 → 1; 800×1280 and 890×400 → 2; 1280×800 and 1920×1080 → 3. Card widths 336 / 360 / 405 / 352 / 352 px, equal height per row, actions at the bottom, buttons 44×44 px, no horizontal scroll.
6 Empty states The "choose a default" prompt with lists and no default; the "no lists" state with the create button.
7 Order elsewhere Switcher and move dialog list by name while the overview order differed.
8 Opened list still refreshes An outside change showed up in under a second; no requests while hidden; immediate refresh on return.
9 Console No errors on the overview or the list page.

Also checked: with a long list name at 400 px the name truncates and the badge stays on one line; the keyboard focus ring surrounds the whole card, Tab goes from the name link to the card's actions and then to the next card, Enter opens the list.

Limits of this verification:

  • "Return to the app" was simulated by dispatching visibilitychange with an overridden visibilityState; a real tab or app switch is not possible headless.
  • Pull to refresh starts only below the page header, because PullToRefresh wraps the body only, as on the list page.

What was implemented, per plan step

  1. Models and mappers: CompactShoppingListItemModel, CompactShoppingListItemMapper; CompactShoppingListModel gains openItemsPreview and openItemCount; stale "sorted by name" JSDoc fixed. The mapper keeps the server order, with a regression test.
  2. Display helpers in shoppingListDisplayUtils.ts: sortShoppingListsByName (German collation, tie-break by id), countHiddenOpenItems, describeHiddenOpenItems, with tests.
  3. Name order elsewhere: ShoppingListHeader (switcher) and ShoppingListMoveItemDialog sort by name themselves. fetchShoppingListOverview has exactly the four callers the plan names.
  4. useVisibilityChange hook, used by ShoppingListProvider (behaviour unchanged) and the overview.
  5. ContentBody width (ContentBodyWidth.Narrow / Wide) and CARD_GRID_CLASS in layout.ts.
  6. ShoppingListOverviewRow → ShoppingListOverviewCard: stretched link as tap target, preview lines, "more" line, actions in their own row.
  7. ShoppingListOverviewPage: wide body, grid, pull to refresh, refresh on return to the app, no timer; epoch counter and in-flight count for the discard / skip / latest-wins rules.

docs/architecture.md is updated in the commits of steps 4 and 5–7.

Deviations from the plan

Four, recorded in the plan's Deviations section:

  • Step 5.1, widthClasses keyed by the enum constants: the plan wrote the literal keys narrow / wide; the code uses [ContentBodyWidth.Narrow] / [ContentBodyWidth.Wide], as CLAUDE.md (Enums) asks. Decided by review-implementation.
  • D6, actions at the top of the card: the actions sit at the top right, next to the name and badge, instead of in their own row at the bottom. The name truncates when they leave too little room. This departs from the spec's UI/UX note "On narrow cards they need their own place"; the spec update is still open. Decided by the user.
  • D6, quantity directly after the item name: amount and unit follow the name instead of sitting at the right edge. Decided by the user.
  • D6, tooltip on cut names: a list or item name cut with an ellipsis shows in full on hover (src/utils/truncationUtils.ts, with tests). To make the texts hoverable, the link stretched over the card is replaced by a click handler on the card; the name stays a real link. Ctrl-click and middle-click now open a new tab only on the name. Decided by the user (tooltip) and implementation (click handler).

These three card changes were verified in the browser on 2026-10-04 as user: columns and 44 px buttons unchanged at the five viewports; the item, the card body and the name open the list with one history entry; star, pencil, bin and the rename dialog do not; the keyboard focus ring surrounds the card and Enter opens the list; tooltips appear only on cut names. Where the description above says the actions are at the bottom or the quantity on the right, this section replaces it.

Judgment calls

  • Focus ring colour: the card's focus ring uses ring-accent, not the plan's ring-accent-ring. The plan says to use the app's existing focus style for links/cards if there is one; RecipeListItem uses focus-visible:ring-2 focus-visible:ring-accent.
  • Texts as constants: "+ 1 weiterer" and the suffix "weitere" are module constants in shoppingListDisplayUtils.ts; "Keine offenen Einträge" is a constant in the card file (the plan lists shoppingListMessages.ts as not to be changed).
  • Umlaut test: the "de" collation gives "Äpfel", "Apfelmarkt", "Zoo" (a shorter prefix sorts first); the test asserts exactly that.
  • Spacing of the card is the plan's markup as written; it looked right in the UI verification and was not adjusted.

Verification

Check Result
npm run build passes
npm run lint passes: 0 errors, 2 warnings in MealPlanDayProvider.tsx that are not from this branch
npm run test passes: 39 files, 399 tests
UI verification (/verify-ui) passes: all nine checks of plan step 9, see above

Rollout

The backend with spec 002 must be deployed before this app version: the mapper has no fallback for a missing openItemsPreview / openItemCount (plan D9), so the overview fails to render against an older backend.

🤖 Generated with Claude Code

Implements spec 002: the shopping list overview as a grid of cards with item previews. - Spec: https://git.raemer.net/araemer/recipe-platform/src/branch/main/docs/features/002-shopping-list-overview-previews.md - Plan: [`docs/plans/002-shopping-list-overview-previews.md`](https://git.raemer.net/araemer/recipe-app/src/branch/002-shopping-list-overview-previews/docs/plans/002-shopping-list-overview-previews.md) ## UI verification Verified with `/verify-ui` on 2026-10-03 against the dev backend running recipe-backend PR #25 (spec 002), as `user`, with one list owned by `admin` and shared read-write. All nine checks of plan step 9 passed. The test lists are deleted again. | # | Check | Result | |---|---|---| | 1 | Cards | Name, "Standard" badge on the default only, "von <owner>" on the shared list only, first 5 open items with quantities ("1,5 l", "10", "Würfel"), "+ 7 weitere" for 12 open items, "Keine offenen Einträge" on both the empty and the all-ticked list. A long item name is cut with an ellipsis on one line; its quantity stays fully visible. | | 2 | Tap target and actions | Clicking an item line, the empty card body and the area beside the actions opens the list. Star, pencil and bin do not navigate. Set default, rename and delete (with confirmation) show their result without a reload. The shared list offers only the star. | | 3 | Order | Ticking an item moves that list's card to the front; a list created in the UI is first after returning; a rename keeps the position. | | 4 | Refresh | One `GET /shopping-list/all` on return to the app, and the changed card moves to the front. No requests during 16 s of idle. Pull to refresh works with real touch events (CDP); a short pull does nothing. | | 5 | Columns | 400×890 → 1; 800×1280 and 890×400 → 2; 1280×800 and 1920×1080 → 3. Card widths 336 / 360 / 405 / 352 / 352 px, equal height per row, actions at the bottom, buttons 44×44 px, no horizontal scroll. | | 6 | Empty states | The "choose a default" prompt with lists and no default; the "no lists" state with the create button. | | 7 | Order elsewhere | Switcher and move dialog list by name while the overview order differed. | | 8 | Opened list still refreshes | An outside change showed up in under a second; no requests while hidden; immediate refresh on return. | | 9 | Console | No errors on the overview or the list page. | Also checked: with a long list name at 400 px the name truncates and the badge stays on one line; the keyboard focus ring surrounds the whole card, Tab goes from the name link to the card's actions and then to the next card, Enter opens the list. Limits of this verification: - "Return to the app" was simulated by dispatching `visibilitychange` with an overridden `visibilityState`; a real tab or app switch is not possible headless. - Pull to refresh starts only below the page header, because `PullToRefresh` wraps the body only, as on the list page. ## What was implemented, per plan step 1. **Models and mappers:** `CompactShoppingListItemModel`, `CompactShoppingListItemMapper`; `CompactShoppingListModel` gains `openItemsPreview` and `openItemCount`; stale "sorted by name" JSDoc fixed. The mapper keeps the server order, with a regression test. 2. **Display helpers** in `shoppingListDisplayUtils.ts`: `sortShoppingListsByName` (German collation, tie-break by id), `countHiddenOpenItems`, `describeHiddenOpenItems`, with tests. 3. **Name order elsewhere:** `ShoppingListHeader` (switcher) and `ShoppingListMoveItemDialog` sort by name themselves. `fetchShoppingListOverview` has exactly the four callers the plan names. 4. **`useVisibilityChange` hook**, used by `ShoppingListProvider` (behaviour unchanged) and the overview. 5. **`ContentBody` `width`** (`ContentBodyWidth.Narrow` / `Wide`) and `CARD_GRID_CLASS` in `layout.ts`. 6. **`ShoppingListOverviewRow` → `ShoppingListOverviewCard`:** stretched link as tap target, preview lines, "more" line, actions in their own row. 7. **`ShoppingListOverviewPage`:** wide body, grid, pull to refresh, refresh on return to the app, no timer; epoch counter and in-flight count for the discard / skip / latest-wins rules. `docs/architecture.md` is updated in the commits of steps 4 and 5–7. ## Deviations from the plan Four, recorded in the plan's Deviations section: - **Step 5.1, `widthClasses` keyed by the enum constants:** the plan wrote the literal keys `narrow` / `wide`; the code uses `[ContentBodyWidth.Narrow]` / `[ContentBodyWidth.Wide]`, as `CLAUDE.md` (Enums) asks. Decided by `review-implementation`. - **D6, actions at the top of the card:** the actions sit at the top right, next to the name and badge, instead of in their own row at the bottom. The name truncates when they leave too little room. This departs from the spec's UI/UX note "On narrow cards they need their own place"; the spec update is still open. Decided by the user. - **D6, quantity directly after the item name:** amount and unit follow the name instead of sitting at the right edge. Decided by the user. - **D6, tooltip on cut names:** a list or item name cut with an ellipsis shows in full on hover (`src/utils/truncationUtils.ts`, with tests). To make the texts hoverable, the link stretched over the card is replaced by a click handler on the card; the name stays a real link. Ctrl-click and middle-click now open a new tab only on the name. Decided by the user (tooltip) and implementation (click handler). These three card changes were verified in the browser on 2026-10-04 as `user`: columns and 44 px buttons unchanged at the five viewports; the item, the card body and the name open the list with one history entry; star, pencil, bin and the rename dialog do not; the keyboard focus ring surrounds the card and Enter opens the list; tooltips appear only on cut names. Where the description above says the actions are at the bottom or the quantity on the right, this section replaces it. ## Judgment calls - **Focus ring colour:** the card's focus ring uses `ring-accent`, not the plan's `ring-accent-ring`. The plan says to use the app's existing focus style for links/cards if there is one; `RecipeListItem` uses `focus-visible:ring-2 focus-visible:ring-accent`. - **Texts as constants:** "+ 1 weiterer" and the suffix "weitere" are module constants in `shoppingListDisplayUtils.ts`; "Keine offenen Einträge" is a constant in the card file (the plan lists `shoppingListMessages.ts` as not to be changed). - **Umlaut test:** the `"de"` collation gives "Äpfel", "Apfelmarkt", "Zoo" (a shorter prefix sorts first); the test asserts exactly that. - **Spacing** of the card is the plan's markup as written; it looked right in the UI verification and was not adjusted. ## Verification | Check | Result | |---|---| | `npm run build` | passes | | `npm run lint` | passes: 0 errors, 2 warnings in `MealPlanDayProvider.tsx` that are not from this branch | | `npm run test` | passes: 39 files, 399 tests | | UI verification (`/verify-ui`) | passes: all nine checks of plan step 9, see above | ## Rollout The backend with spec 002 must be deployed before this app version: the mapper has no fallback for a missing `openItemsPreview` / `openItemCount` (plan D9), so the overview fails to render against an older backend. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs(plan) Add shopping list overview previews plan (spec 002)
Some checks failed
/ test (pull_request) Failing after 44s
63541495aa
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chore(contract) Pin the contract for spec 002
Some checks failed
/ test (pull_request) Failing after 46s
/ contract_pin (pull_request) Successful in 3s
f7ba4ccf6e
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(shopping-list) map item previews of the overview
Some checks failed
/ contract_pin (pull_request) Has been cancelled
/ test (pull_request) Has been cancelled
b75b72fa09
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(shopping-list) add name sort and hidden-item helpers
Some checks failed
/ contract_pin (pull_request) Has been cancelled
/ test (pull_request) Has been cancelled
333b69e7f8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(shopping-list) keep lists sorted by name in the switcher and move dialog
Some checks failed
/ contract_pin (pull_request) Has been cancelled
/ test (pull_request) Has been cancelled
cec82205c6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
refactor(shopping-list) extract useVisibilityChange hook
Some checks failed
/ contract_pin (pull_request) Has been cancelled
/ test (pull_request) Has been cancelled
fd30c8f179
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(shopping-list) show the overview as cards with item previews
All checks were successful
/ test (pull_request) Successful in 1m10s
/ contract_pin (pull_request) Successful in 5s
67ba5a7269
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
araemer changed title from WIP: feat(shopping-list) Shopping list overview with item previews (spec 002) to feat(shopping-list) Shopping list overview with item previews (spec 002) 2026-10-03 18:15:29 +00:00
refactor(ui) key ContentBody width classes by the enum constants
All checks were successful
/ test (pull_request) Successful in 1m10s
/ contract_pin (pull_request) Successful in 3s
cea876d6a5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs(plan) shorten the decided-by line of the ContentBody deviation
All checks were successful
/ test (pull_request) Successful in 1m5s
/ contract_pin (pull_request) Successful in 3s
2185663fec
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(shopping-list) move card actions to the top, show cut names as tooltip, put the quantity next to the item
All checks were successful
/ test (pull_request) Successful in 1m4s
/ contract_pin (pull_request) Successful in 2s
4267cde0f3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
araemer merged commit a4546130cf into stage 2026-10-04 06:04:26 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
araemer/recipe-app!56
No description provided.