Skip to content

feat(render_map_tool): results carousel instead of the side list - #292

Merged
mattpodwysocki merged 12 commits into
mainfrom
feat/render-map-carousel
Nov 5, 2026
Merged

mattpodwysocki merged 12 commits into
mainfrom
feat/render-map-carousel

Conversation

@mattpodwysocki

@mattpodwysocki mattpodwysocki commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Description

Moves render_map_tool's numbered side list to a results carousel along the bottom of the map, the pattern ChatGPT uses for its built-in place results. Closes #283.

  • Place pins: white pins with a Maki icon for the place's category (Search Box's maki name, else matched from the category, else a plain marker), turning black when selected, like ChatGPT's place results and OpenAI's layout guidance. About 20 common icons are inlined (~12 KB, CC0). Numbered badges stay where the number means something: a caller's numbered label (visit order), optimized stops, start/end.
  • Cards: name, capitalized category, price level ($ to $$$$ from Places attributes.price_level) and distance, plus the photo once it loads. A second line shows the hours. No star rating, since Places doesn't have one. The phone number waits for a place detail card.
  • Selected pin: the selected place's pin turns black (a numbered badge turns white), comes to the front and shows the place name under it. The matching card gets a border. The first card starts selected.
  • Sync both ways: swiping selects the card nearest the middle, clicking a card selects it, and clicking a pin scrolls to its card. The map only pans when the selected pin is off screen or hidden behind the cards.
  • No popups for carousel pins. Pins without a card (route ends, the search center) keep theirs.
  • Layout: the carousel sits above the Mapbox logo and attribution, the legend moves up above it, and fit-to-bounds padding accounts for both.
  • Today's hours: parsed from OSM opening_hours for simple rules (Mo-Fr 07:00-19:00; Sa,Su 08:00-17:00, Su off, 24/7, wrap-around day ranges). Anything else shows no hours rather than wrong ones. When Places gives the place's time zone (telemetry.tags.timezone) the card says "Open until 8:30 PM" in green or "Closed · opens 7:30 AM" in red; otherwise it shows today's hours ("Open 7:30–15:00"). Times use the viewer's locale.
  • OpenAI's UI guidelines (fourth commit): every card has a picture (the place's icon as a placeholder when there's no photo), labeled for screen readers; numbered badges pick dark or white text by contrast; and the tool description asks the model not to list the places again after rendering. Still open from those guidelines: one action per card (render_map_tool: click a place to ask the model about it #284), and an Expand-to-fullscreen button (render_map_tool: fullscreen button #286).
  • Light and dark: cards, summary chip, legend and messages follow the host's theme (hostContext.theme at ui/initialize and on host-context-changed), falling back to prefers-color-scheme, and use the host's style variables (--color-background-primary, --color-text-primary, …) when sent. The map and pins look the same in both.
  • Tool and schema descriptions say "carousel" instead of "panel".

Notes

Design feedback (PDES-471)

Nikita's review on PDES-471 and the Figma mockup, addressed in the latest commit:

  • Category names are capitalized.
  • "% popularity" is gone. Places has no star rating, so the card shows the price level instead.
  • Hours read "Open until …" / "Closed · opens …" (Dash style) when we know the place's time zone.
  • The phone number is off the card until there's a place detail card (not designed yet).
  • Every carousel pin shows its name with a white halo and no box, not just the selected one. These are HTML markers, so labels don't collide-hide like map labels. Dense results can overlap.

Testing

  • npm test: 1117 pass. New tests cover card/pin selection, pin click scrolling to its card, swipe selection, pan-only-when-hidden, fit padding with the carousel, legend position, and 11 hours-parsing cases.
  • npm run lint, tsc, npm run build clean.
  • Rendered in headless Chrome at 800x560 and 400x600, in light and dark, from a live category_search_tool result (coffee near SF City Hall): cards, photos, hours and phone all fill in, and card click, pin click and selection sync work. That pass found a pin just above the cards whose name label was hidden behind them, fixed in the second commit (pan when the label is hidden, and only as far as needed). It also showed the summary chip covering the zoom-in button (already the case on main); the third commit moves the zoom buttons below it, copying feat(render_map_tool): live traffic and Satellite/Traffic buttons on the map #278's placeTopLeftControls as-is so the two PRs merge cleanly.
  • Not yet checked inside Claude Desktop or ChatGPT.
pr292-carousel-phone-dark pr292-carousel-wide-dark

Checklist

  • I've added/updated tests as needed
  • I've updated documentation if necessary (CHANGELOG, tool descriptions)
  • Checked in Claude Desktop and ChatGPT

🤖 Generated with Claude Code

Place results now show as swipeable cards along the bottom of the map,
like ChatGPT's place results. The selected card and its pin stay in
sync: swiping or clicking a card highlights its pin (white, on top,
with the name under it) and pans only when the pin is hidden; clicking
a pin scrolls to its card. Pins with a card no longer open popups.
Cards show today's hours parsed from simple OSM opening_hours.

Closes #283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mattpodwysocki
mattpodwysocki requested a review from a team as a code owner October 19, 2026 20:31
mattpodwysocki and others added 8 commits October 19, 2026 16:31
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in a headless render: a pin just above the carousel kept its name
label hidden behind the cards. Pan when the label (not just the pin)
would be hidden, do it for pin clicks too, and pan only as far as
needed instead of recentering, so nearby pins stay in view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "6 results for ..." chip sat on top of the zoom-in button. Same
placeTopLeftControls fix as #278, copied as-is so the two merge cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Every card has a picture: Assembly's marker icon on a muted tile until
  a photo loads, or when there is none, labeled for screen readers.
- Pin numbers pick dark or white text by WCAG contrast; white on the
  default orange was 2.8:1.
- The tool description asks the model not to repeat the places the
  cards already show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ange numbers

Matches ChatGPT's place results and OpenAI's UI guidance: search results
and other places in the carousel are white pins with a Maki icon for
their category, black when selected, and their cards drop the number
badge. The icon comes from Search Box's maki name, else the category,
else a plain marker; about 20 common Maki icons (CC0) are inlined.
Numbered badges stay where the number means something: a caller's
numbered label (visit order), optimized stops, start/end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cards, summary chip, legend and messages follow the host's theme
(MCP Apps hostContext.theme, at initialize and on host-context-changed),
falling back to prefers-color-scheme. Colors use the host's style
variables (--color-background-primary etc.) when sent, with light/dark
fallbacks otherwise. The map and its pins are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Capitalize the category on cards.
- Show the Places price level ($ to $$$$) instead of "% popularity";
  Places has no star rating.
- Hours say "Open until 8:30 PM" / "Closed · opens 7:30 AM" when Places
  gives the place's time zone, otherwise "Open 7:30–15:00" for today.
- Drop the phone number from the card until there's a place detail card.
- Label every carousel pin with a white halo instead of a box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zmofei
zmofei previously approved these changes Nov 5, 2026

@zmofei zmofei left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests, lint, tsc and build pass locally, and the carousel works well on narrow maps. Approving; a few non-blocking comments below.


// Swiping selects whichever card ends up in the middle.
var scrollTimer = null;
function onCardScroll() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On maps wider than about 675px, the first and last cards can't reach the center of the list. So after a scroll, onCardScroll selects card 2 (or the second-to-last) instead, even right after the user clicked card 1 or its pin. Reproduced in Chrome at 800px. Could you let the end cards reach the center (for example side padding of max(32px, 50% - 150px)), or pick the first/last card when the list is scrolled to an end, and add a test with a wide list?

'than sending them alone.';
'than sending them alone. ' +
"After rendering places, don't list them again in your reply: the map's " +
'cards already show each name, category, distance, hours and phone. ' +

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description says the cards show the phone, but c85f169 removed the phone from the cards and the pin popups. So the model will skip the phone, and the user sees it nowhere. Please list only what the cards show, and ask the model to add things like the phone or address. The "popularity score" and "under its pin when selected" texts in the schema and RenderMapTool.ts are also out of date.

/* A place pin (search results and other places in the carousel): a
white disc with the place's Maki icon, black when selected. Numbered
badges stay for markers whose number means something (visit order). */
.marker-badge.place {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#283 suggests the carousel for narrow maps and keeping the side list on wide or fullscreen maps, but this PR removes the side list for all sizes and still closes #283. Is that on purpose? If yes, please say why in the PR and on #283. If not, please add the wide-map case or open a follow-up issue.

# Conflicts:
#	CHANGELOG.md
#	src/resources/ui-apps/mapAppHtml.ts
#	test/resources/ui-apps/mapAppHtml.script.test.ts
@mattpodwysocki

Copy link
Copy Markdown
Contributor Author

Merged main in (8114b2e) after #278 landed. That was a real conflict in mapAppHtml.ts:

I checked it in Chrome with cards, the legend and traffic on together. Full suite: 1205 pass. Your earlier comments go in the follow-up PR, as discussed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
#	src/resources/resourceRegistry.ts
@mattpodwysocki
mattpodwysocki merged commit 05cd8a6 into main Nov 5, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

render_map_tool: bottom carousel for place results on narrow/inline maps

2 participants