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.tsunchanged:project/:projectId/map/:mapIdcontinues to renderapp/routes/project-map.tsx. - Preserve the public state APIs from
base-layer-control.tsx,image-layer-control.tsx,map-object-visibility-control.tsx, andavesmaps-layer-control.tsxunless 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/*.jsonfiles exist in this workspace, and this task intentionally reads and writes no Directus field. - Use the existing
Popover,Card,Tabs,ScrollArea,Button, andCheckboxprimitives; 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
Cardwith 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 typecheckpasses.
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 preservingBaseLayerEntry,useBaseLayerSelection, checked-state semantics, and toggle callbacks. - In:
app/features/map-controls/image-layer-control.tsx, expose or extract the Images card content while preservingImageOverlayLayerlabels,useImageLayerSelection, and toggle callbacks. - In:
app/features/map-controls/map-object-visibility-control.tsx, expose or extract the Features card content while preservinguseMapObjectVisibilitySelection,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 typecheckpasses.
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 currentBaseLayerControl, conditionalImageLayerControl,MapObjectVisibilityControl, and conditionalAvesmapsLayerControlsiblings 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 whendatasetManifestexists. - In: Keep the unified control inside the existing
MapControlContainerso 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.
-
MapControlContainerremains the only map-control event-isolation boundary. -
pnpm typecheckpasses.
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 typecheckpasses.
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 typecheckpasses. -
pnpm formatcompletes successfully. -
pnpm buildpasses. - 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/*.jsonsnapshot 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.