Skip to content

Combined Layer Controls

Decisions

Decision Choice Why
Menu structure One layer-control trigger and one popover containing separate cards Gives users one entry point while preserving each control's visual and behavioral boundary
Card categories Base layers, Images, Features, and conditional Avesmaps Covers the four existing map-view controls without merging unrelated checkbox state
Feature navigation Keep Points, Lines, and Areas as tabs inside the Features card Preserves the existing feature organization and limits panel height
Avesmaps navigation Keep Locations, Paths, and Crossings as tabs inside the Avesmaps card Preserves the dataset-specific grouping, counts, and nested subtype controls
Conditional cards Omit Images without configured overlays and Avesmaps without a manifest Avoids empty controls and matches the current route availability checks
Visibility state Preserve existing per-map visibility behavior and storage contracts The task changes composition, not map rendering or layer semantics
Tab state Reset existing layer-control and active-tab localStorage state for the new unified-menu model Explicitly chosen product behavior; no migration is required
Server scope No loader, action, route-table, project-data, or Directus changes All inputs and state are already client-side in project-map.tsx; no Directus field is read or written
Control container Keep MapControlContainer as the Leaflet event-isolation boundary It is layout-agnostic and already supports one or many child controls

Prerequisites

  • Keep the protected route declaration in app/routes.ts unchanged: project/:projectId/map/:mapId continues to render app/routes/project-map.tsx.
  • Preserve the public state APIs from base-layer-control.tsx, image-layer-control.tsx, map-object-visibility-control.tsx, and avesmaps-layer-control.tsx unless a small prop-only adjustment is needed to render them inside the combined panel.
  • Keep filesystem access and project data on the server boundary; this feature must not add a loader or action.
  • Keep Leaflet client-only behavior unchanged. The combined panel is regular React UI and must not import Leaflet directly.
  • Confirmed schema status: no directus/schema/*.json files exist in this workspace, and this task intentionally reads and writes no Directus field.
  • Use the existing Popover, Card, Tabs, ScrollArea, Button, and Checkbox primitives; do not add a new UI dependency or shared primitive unless an existing component cannot support the required layout.

Step 1 - Define the combined control composition

Goal: Establish one map-layer control component that owns the shared trigger/popover and accepts the already-available layer-control content as separate cards.

Scope

  • In: Add the smallest feature-local component under app/features/map-controls/ for the unified trigger, popover, scrollable content region, and card ordering.
  • In: Give the trigger a layer-oriented label and icon consistent with the existing map controls, and give the popover a stable width and viewport-constrained maximum width.
  • In: Define the composition contract so Base layers and Features are always rendered, while Images and Avesmaps are rendered only when their entry data is available.
  • In: Keep each category visually isolated in its own Card with a heading; the shared component should compose content rather than duplicate checkbox or tab logic.
  • Out: Changes to Leaflet layers, localStorage keys, loader data, route actions, or the editor sidebar.

Acceptance criteria

  • A single component owns the unified trigger and popover.
  • The component can render ordered card content for Base layers, Images, Features, and Avesmaps.
  • The component's props describe availability and content without introducing server-only types or browser-global access during SSR.
  • The popover has a bounded width and a scrollable content area suitable for multiple cards.
  • pnpm typecheck passes.

Step 2 - Adapt the existing controls into card content

Goal: Reuse the four current control implementations inside the unified popover while removing their independent trigger/popover shells.

Scope

  • In: app/features/map-controls/base-layer-control.tsx, expose or extract the Base layers card content while preserving BaseLayerEntry, useBaseLayerSelection, checked-state semantics, and toggle callbacks.
  • In: app/features/map-controls/image-layer-control.tsx, expose or extract the Images card content while preserving ImageOverlayLayer labels, useImageLayerSelection, and toggle callbacks.
  • In: app/features/map-controls/map-object-visibility-control.tsx, expose or extract the Features card content while preserving useMapObjectVisibilitySelection, useFeatureTabSelection, MapObjectTypeSection, grouped checkboxes, and Points/Lines/Areas tabs.
  • In: app/features/map-controls/avesmaps-layer-control.tsx, expose or extract the Avesmaps card content while preserving manifest grouping, useAvesmapsLayerSelection, useAvesmapsTabSelection, counts, group toggles, subtype toggles, and Locations/Paths/Crossings tabs.
  • In: Keep existing storage keys and visibility-selection behavior unless the explicitly chosen reset requires a deliberate key/version change; do not silently migrate old state.
  • Out: Rewriting checkbox logic, changing map-object or Avesmaps data shapes, changing tab labels, or changing default layer visibility.

Acceptance criteria

  • Each category renders as one headed card inside the shared popover and no longer renders its own independent trigger.
  • Base-layer checkboxes retain their current enabled/disabled semantics.
  • Image-overlay checkboxes retain their current enabled/disabled semantics.
  • Features retains Points, Lines, and Areas tabs with all grouped and ungrouped type controls.
  • Avesmaps retains Locations, Paths, and Crossings tabs with group/subtype controls and counts.
  • Checked, unchecked, and indeterminate states behave exactly as before within each card.
  • pnpm typecheck passes.

Step 3 - Compose the unified control in the map route

Goal: Replace the four sibling controls in the map view with one unified control while preserving route-owned data and callbacks.

Scope

  • In: app/routes/project-map.tsx, replace the current BaseLayerControl, conditional ImageLayerControl, MapObjectVisibilityControl, and conditional AvesmapsLayerControl siblings with the unified control composition.
  • In: Pass the existing baseLayers, image overlays, datasetManifest, map-object type definitions, visibility predicates, and toggle callbacks into their corresponding cards.
  • In: Keep the current availability rules: always include Base layers and Features; include Images only when imageOverlays.length > 0; include Avesmaps only when datasetManifest exists.
  • In: Keep the unified control inside the existing MapControlContainer so Leaflet click and scroll propagation remain disabled for the whole panel.
  • Out: Changes to loader, action, routes.ts, map layer ordering, map rendering props, or project persistence.

Acceptance criteria

  • The map view exposes one layer-control trigger instead of four separate triggers.
  • The unified popover receives all existing visibility callbacks without adding a server request.
  • Maps with image overlays show an Images card; maps without them do not.
  • Avesmaps maps show an Avesmaps card; maps without a manifest do not.
  • Base layer and Features cards remain available according to current route data.
  • MapControlContainer remains the only map-control event-isolation boundary.
  • pnpm typecheck passes.

Step 4 - Reset and validate the unified-menu UI state

Goal: Apply the selected state-reset behavior intentionally and ensure the new multi-card layout does not inherit stale active-tab or visibility state.

Scope

  • In: Establish the new unified-menu state namespace or versioning boundary required to prevent old layer-control and tab preferences from being treated as migrated state.
  • In: Make the default state explicit: base layers and image overlays enabled, custom feature types visible, Avesmaps subtypes visible, Features on Points, and Avesmaps on its default available group.
  • In: Ignore malformed, unknown, or unavailable entries without breaking the menu; preserve per-map isolation for any new state that remains persisted.
  • In: Keep popover open/closed state transient unless the implementation already has a dedicated state mechanism required by the combined component.
  • Out: Migrating old preference values, changing map rendering defaults, or persisting the menu to project data.

Acceptance criteria

  • Existing control-specific visibility and active-tab values are not migrated into the new unified-menu state model.
  • A newly opened unified menu starts from the documented defaults.
  • State is scoped to the current map id wherever the new component persists state.
  • Invalid or unavailable stored values fall back without a user-facing error.
  • Toggling a checkbox and switching tabs does not reset another card's in-memory state during the same open session.
  • pnpm typecheck passes.

Step 5 - Tune responsive layout and scrolling

Goal: Make the combined multi-card popover usable on narrow and wide map viewports with long labels and long layer lists.

Scope

  • In: The unified control component and, only where necessary, the four card-content modules, adjust local classes for card spacing, tab sizing, wrapping/truncation, and scroll boundaries.
  • In: Use one clear vertical scroll region for the combined content or clearly bounded nested regions; avoid nested scrolling unless required by the existing tab controls.
  • In: Preserve the existing tab primitives and checkbox label structure so long names remain associated with their controls.
  • In: Keep the trigger aligned with the current top-right map-control placement.
  • Out: Global CSS changes, shared primitive redesign, changes to map dimensions, and unrelated control styling.

Acceptance criteria

  • The popover stays within the viewport on narrow screens.
  • Cards, headings, tabs, checkboxes, labels, counts, and neighboring rows do not overlap.
  • Long feature and Avesmaps lists remain vertically scrollable.
  • Long labels do not push checkboxes or counts outside the card.
  • On wide screens, the panel remains bounded and aligned with the map controls.
  • Manual responsive checks show no console errors or layout warnings relevant to the change.

Step 6 - Verify the complete feature

Goal: Confirm the unified menu preserves all four control behaviors while changing only their presentation and intentionally resetting old UI state.

Acceptance criteria

  • pnpm typecheck passes.
  • pnpm format completes successfully.
  • pnpm build passes.
  • Manual check: a tile-set map opens one layer menu with Base layers and Features cards.
  • Manual check: a map with image overlays also shows the Images card, and each overlay can be toggled independently.
  • Manual check: an Avesmaps map shows the Avesmaps card with all available tabs, nested controls, and counts.
  • Manual check: a map without overlays or Avesmaps data omits those cards rather than showing empty states.
  • Manual check: feature group toggles, indeterminate states, subtype toggles, and tab switching retain their expected behavior.
  • Manual check: the old control-specific localStorage values do not populate the new unified-menu defaults.
  • Manual check: opening and using the menu does not change the map center/zoom, project data, or server state.
  • Confirm no changes were made to app/routes.ts, route loaders/actions, data/projects.json, or Directus schema/collections.

Risks / open questions

  • There is no directus/schema/*.json snapshot in the workspace. This does not block implementation because the plan adds no Directus field or server persistence.
  • Removing the existing trigger/popover shells while preserving the four hooks may expose coupling between UI wrappers and state hooks; keep extraction prop-focused and validate after each control is moved.
  • Resetting old localStorage state can be implemented with a new key namespace/version or by intentionally ignoring the old keys. The chosen mechanism must be explicit and must not accidentally preserve stale preferences.
  • The combined panel can become taller than any individual existing popover. The scroll boundary and responsive checks in Step 5 are required before considering the work complete.