Skip to content

Explorer mockup v1: in-map search, sidebar mirror, bottom legend, permanent table (#200)#217

Merged
rdhyee merged 11 commits into
isamplesorg:mainfrom
rdhyee:explorer-mockup-v1
May 13, 2026
Merged

Explorer mockup v1: in-map search, sidebar mirror, bottom legend, permanent table (#200)#217
rdhyee merged 11 commits into
isamplesorg:mainfrom
rdhyee:explorer-mockup-v1

Conversation

@rdhyee
Copy link
Copy Markdown
Contributor

@rdhyee rdhyee commented May 13, 2026

Closes most of #200 mockup-alignment scope. Sequenced into independently revertable commits.

Summary

  • M-1A — Relocate search input + two scope buttons + help/results count from a top-of-page block into an absolutely-positioned .map-search-overlay over #cesiumContainer. All element IDs preserved.
  • M-1B — New sidebar #sampleSearchSidebar input mirrored to #sampleSearch via two-way input-event handlers. Enter on sidebar always submits world-scope (Option B).
  • M-2 — Display-only bottom-center .map-color-legend with the four source-palette swatches. aria-hidden, pointer-events: none — does not duplicate the filter source-of-truth in #sourceFilter.
  • M-5 — Samples table is now permanent below the globe. Globe/Table toggle (#globeViewBtn, #tableViewBtn, body.table-view-active, ?view= URL param) removed. Table row click reuses the sample-mode globe-click selection ceremony — sets viewer._globeState.selectedPid, updates the sidebar, flies the camera, writes #pid to the hash directly so the URL is canonical regardless of zoomWatcher init order.
  • DocsEXPLORER_STATE.md updated with a new §6 "Mockup-v1 addendum" plus targeted strikes for the removed state surfaces. Also picks up a pre-existing doc gap (missing camera.moveEnd listener in §5).

Codex review trail

Each segment was iterated via codex exec until Codex returned LGTM, ready to merge:

Segment Rounds Notable catches
M-1A + M-1B 3 mobile overlay overlapping Cesium toolbar; base-layer dropdown z-index; 320px button overflow; real elementFromPoint click-through test
M-2 1 (clean first pass; Codex ran quarto render)
M-5 2 Playwright spec still referenced removed toggle; ?view= not canonicalized; #pid hash race before zoomWatcher wires
EXPLORER_STATE.md 4 stale view-mode refs; ceremony writeup imprecise; Share is hash-only not query-state; missing camera.moveEnd listener

State-contract delta (full writeup in EXPLORER_STATE.md §6 mockup-v1 addendum)

  • Removed: ?view= URL param, body.table-view-active class, isTableViewActive(), setView(), #globeViewBtn / #tableViewBtn. writeQueryState() now does params.delete('view') to canonicalize legacy bookmarks (next writeQueryState() call only — hash-only writes preserve location.search).
  • Added: #sampleSearchSidebar input mirrored to #sampleSearch. .map-search-overlay, .map-color-legend, .map-wrap DOM surfaces. tr.selected visual marker on .samples-table tbody tr[data-pid]. rowsByPid: Map cached at refreshTable() time for O(1) per-PID lookup.
  • Asymmetric selection sync — table-row click updates globe + sidebar + URL; globe-point / search-row clicks do NOT live-update the table's .selected class. The table only repaints .selected on next renderTable(). Bidirectional highlight is a follow-up.

Test plan

  • Playwright explorer-layout-stability.spec.js updated for new map height clamp (400-540 desktop, 360-480 mobile) and the now-permanent table
  • New explorer-map-overlay.spec.js: overlay vs Cesium toolbar (desktop, 390px, 320px); base-layer dropdown click-through via elementFromPoint; sidebar↔in-map input mirror; table-row click → .selected class + viewer._globeState.selectedPid + #pid hash
  • url-roundtrip.spec.js doc-comment updated; no spec deletions needed
  • Manual smoke before merge: load explorer, type in either search input, scope to area + world, click a table row, verify sidebar + URL update

Deferred (non-blocking)

  • a11y: aria-label on #sampleSearch, aria-describedby linking the help/hint text
  • applyQueryToSearch() does not clear inputs when URL drops ?search= (back/forward navigation edge case)
  • Globe→table selection sync (live .selected class repaint when sample selected from globe or search)

🤖 Generated with Claude Code

rdhyee and others added 11 commits May 13, 2026 14:26
Relocate the search input, scope buttons (Search Selected Areas / Search
Entire World), help text, and results count from the top-of-page
.explorer-controls block into an absolutely-positioned .map-search-overlay
inside a new .map-wrap div around #cesiumContainer.

All element IDs preserved (#sampleSearch, #searchAreaBtn, #searchWorldBtn,
#searchResults) so existing handlers in the zoomWatcher OJS cell and the
URL state contract continue to work unchanged.

Part of isamplesorg#200 mockup-alignment work. The Globe/Table toggle and
#tableControls stay in the top-level .explorer-controls block for now;
they will be removed in a later commit when the table becomes permanent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add #sampleSearchSidebar input to the top .panel-section of .side-panel.
Two-way input-event mirror with the in-map #sampleSearch keeps the two
input chrome in sync as a single logical query term. Enter on the sidebar
input always submits world scope per the Option B decision in the isamplesorg#200
mockup discussion — a typed-text query from the sidebar implies "find
anywhere" rather than "limit to current map view."

applyQueryToSearch() now hydrates both inputs from the ?search= URL
param. writeQueryState() still reads from #sampleSearch only since the
mirror guarantees value parity.

The mirror guards against feedback loops by comparing values before
setting; programmatic .value assignment does not fire 'input', so a
strict-equality guard is sufficient (no debounce or recursion flag).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex flagged three blockers on the cumulative M-1A + M-1B diff:

1. Mobile overlay collided with the Cesium toolbar. At max-width: 900px
   the overlay was switching to left: 8px, covering the toolbar column
   at left: 5px. Removed the mobile left-shift; keep the overlay offset
   at left: 50px so the vertical toolbar remains hit-targetable. Width
   adjusts to calc(100% - 58px) instead.

2. Base-layer picker dropdown could be occluded by the overlay
   (dropdown opens at left: 36px; overlay starts at left: 50px). Bump
   .cesium-baseLayerPicker-dropDown z-index to 1100 so it wins the
   z-stack against the overlay's 1000.

3. No automated coverage for overlay-vs-toolbar collision. Add
   tests/playwright/explorer-map-overlay.spec.js with four specs:
   desktop overlap, mobile overlap, dropdown z-index ordering, and a
   smoke test that the sidebar↔in-map input mirror is bidirectional.

Also picked up the small IME concern from Codex ask #1: gate both Enter
handlers (in-map and sidebar) on `!e.isComposing && e.keyCode !== 229`
so IMEs that emit Enter to commit a candidate don't trigger a search on
the pre-commit value.

Non-blocker items (URL-clears-search edge case, aria-label on
#sampleSearch, aria-describedby for hints) deferred — to be picked up
in the EXPLORER_STATE.md / a11y commit at the end of the PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex round-2 review flagged three remaining items on the M-1A/M-1B
overlay work:

1. At 320px (iPhone SE) the two nowrap scope buttons would overflow
   .search-actions. Add a @media (max-width: 400px) rule that switches
   .search-actions to flex-direction: column so they stack.

2. The z-index Playwright check was only computed-style math; it would
   miss an ancestor stacking-context trap or pointer interception. The
   dropdown spec now adds a real document.elementFromPoint() hit-test
   at the geometric intersection of the dropdown and overlay rects.

3. Added a dedicated 320px viewport test that asserts both (a) the
   overlay clears the toolbar and (b) no button overflows its parent
   row at that width.

Also picked up the spec hygiene notes:
- test.describe.configure({ timeout: 90000 }) — Cesium/OJS boot is
  slow on CI; default 30s was too tight.
- Mirror-input spec now uses a waitForBootReady() helper that waits
  for window._ojs before calling value('zoomWatcher'), mirroring the
  pattern used elsewhere in the suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a horizontal swatch row centered above Cesium's bottom credits row,
showing the four source colors with their display names. Mockup
requirement is "users can identify what each color means without
hunting the sidebar"; functional toggle/show-hide stays in
#sourceFilter as the single source of truth.

The legend is marked aria-hidden="true" so screen readers don't read
the palette twice (the sidebar source filter exposes the same
information via labeled checkboxes). It also has pointer-events: none
so it never intercepts globe interaction.

Hardcoded colors mirror assets/js/source-palette.js. We could fetch
SOURCE_COLORS dynamically at render-time, but that pulls a static
overlay into the OJS reactive graph for no real benefit — the palette
is stable and audited via issue isamplesorg#113.

Wraps onto multiple rows below 600px viewport width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructure the page so the samples table is always visible below the
globe rather than hidden behind a Globe/Table toggle button.

Removed:
- #globeViewBtn / #tableViewBtn buttons and their .view-toolbar /
  .view-toggle CSS
- body.table-view-active class (toggle-mode marker; no longer needed)
- isTableViewActive() helper
- ?view=table URL param read in writeQueryState() and hydration in
  the tableView OJS cell
- setView() function inside tableView; the unconditional permanent
  layout replaces it

Restructured:
- Map height shrinks to clamp(400px, 50vh, 540px) from 500/65/680 so
  the table fits below without a full-page-height app feel
- #tableControls (Max samples input) moved from a top-of-page toolbar
  into the table section header, next to the new <h3>Samples table</h3>
- #tableContainer defaults to display: block (no display: none toggle)

Added:
- Table row click handler that mirrors the search-row click ceremony
  (isamplesorg#207 item 8): bump freshness token → set selectedPid → update sample
  card → flyTo → lazy-load description from wide_url. Same code path
  as a sample-mode globe-point click, so the URL pid hash, sidebar
  detail, and selection state stay in sync regardless of which surface
  the user clicks on.
- rowsByPid Map built at refreshTable() time for O(1) row lookup on
  click — avoids re-scanning a 25K-row array per click.
- .samples-table tr.selected styling so the table reflects the
  currently-selected sample. Selection is repainted in-place on click
  rather than triggering a full table refresh.

The "Max samples" change handler now always refreshes (previously gated
on isTableViewActive()).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex flagged three blockers + one cleanup on commit d51e0c5:

1. Playwright layout-stability spec still targeted the removed
   #tableViewBtn / #globeViewBtn and asserted the pre-M-5 desktop map
   height range (>=500). Rewrote the desktop assertion path: globe and
   table now coexist; assert tableContainer.top sits below the globe;
   assert the toggle buttons have count 0. Updated the desktop clamp
   bounds (400-540) and the mobile clamp bounds (360-480) to match the
   new --explorer-map-height values.

2. ?view= was no longer being WRITTEN but was not being canonicalized
   AWAY either. A bookmarked `?view=table&...` URL would stick through
   filter/share flows. Added params.delete('view') to writeQueryState()
   so the legacy param is stripped on the next URL update.

3. Boot-time race: a very-early table-row click could fire before
   zoomWatcher registered its URL hash listeners, leaving the #pid
   hash out of sync with viewer._globeState.selectedPid. The table-row
   click handler now writes the hash directly via
   history.replaceState(null, '', buildHash(viewer)) so URL canonicality
   is independent of zoomWatcher init order.

Cleanup:
- Stale comment "Binary Globe/Table view" → "Samples table (permanent
  below globe; M-5)".
- url-roundtrip.spec.js comment block: removed the stale `view=table`
  coverage reference; added a one-line note that the param was removed.

Plus new test coverage from Codex round-1 ask isamplesorg#7:
- tests/playwright/explorer-map-overlay.spec.js: new spec that clicks
  the first table row, asserts the .selected class repaint, asserts
  viewer._globeState.selectedPid, and asserts the #pid hash carries
  the row's pid. Closes the "table-row click as primary selection
  surface" coverage gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflect the state-contract changes that landed in this PR:

- §1 URL params: ~~view~~ struck (removed in M-5). New row documents
  search_scope (was already in code, missing from the inventory).
- §3 DOM-as-state: ~~body.table-view-active~~ struck. New rows added
  for the permanent samples table's data-pid attribute on
  <tr data-pid> and the tr.selected visual marker.
- §3 input-elements paragraph: #sampleSearchSidebar listed alongside
  #sampleSearch with a note that the two are mirrored via
  input-event handlers.
- §5 OJS cell graph: tableView no longer writes ?view to URL; it
  now writes the #pid hash via buildHash from the table-row click
  handler (mirrors sample-mode globe click).
- §7 facet-count contract: "view mode" row struck — moot since the
  Globe/Table toggle is gone.
- §6 new "Mockup-v1 addendum": the canonical writeup of M-1A, M-1B,
  M-2, M-5 — what changed, why, what stayed (data contract is
  unchanged from option C + the isamplesorg#178 light path).
- Top-of-doc note flagging the inventory has had targeted edits since
  the original 94e7674 baseline; line citations may not match exactly.

This is documentation only — no code changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex flagged four sets of doc errors in c552351:

1. Stale view-mode references remained:
   - §1 search row: "called from doSearch() and from globe/table
     toggle" → removed the toggle reference (and the toggle is gone).
   - §4 hydration paragraph: "tableView ⇒ view hydration" → updated
     to note the hydration step was removed in mockup-v1.
   - §5 cell graph: tableView "reads view from URL once" → updated
     to "calls refreshTable() unconditionally on boot — no URL
     hydration".
   - §5 "two cells register change listeners" note: removed the
     "marks dirty and refreshes only if visible" half — tableView
     now refreshes unconditionally.

2. Table-row click ceremony writeup tightened:
   - Added explicit preconditions (anchor-element bail; missing
     sample/lat-lng bail; typeof viewer === 'undefined' bail).
   - Moved the .selected repaint into the ceremony list as step 6,
     synchronous, before the async detail query.
   - Step 7 now mentions the pid SQL-escaping via .replace(/'/g, "''").
   - Reworded "same five-step ceremony" → "same async-selection
     ceremony with one table-specific addition", since the direct
     history.replaceState(buildHash) is unique to table-row click.

3. ?view= canonicalization wording narrowed:
   - Added caveat that only the next writeQueryState() call strips
     the param; hash-only writes via buildHash() preserve
     location.search as-is.

4. §5 zoomWatcher event-listener cell: added the sidebar
   #sampleSearchSidebar input/keydown listeners to the list, plus
   the in-map keydown that was already noted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex round-2 caught a contradiction: the zoomWatcher row claimed
Share writes through writeQueryState(), but explorer.qmd:2477 shows
Share actually uses history.replaceState(null, '', buildHash(viewer)),
which is a hash-only write that preserves location.search. The new
mockup-v1 addendum caveat already lists Share correctly as a buildHash
caller; this commit aligns §5 with that reality.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gap)

Codex round-3 spotted a pre-existing doc gap: zoomWatcher registers
both viewer.camera.changed AND viewer.camera.moveEnd (added in isamplesorg#205
for sub-threshold pan settling), but §5 listed only camera.changed.
Adding moveEnd to both the event-listeners and the URL-writes cells.

Not strictly mockup-v1 scope but landing while we're here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rdhyee rdhyee merged commit 7e160ff into isamplesorg:main May 13, 2026
1 check 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.

1 participant