Tabbed map feature visibility control — implementation plan¶
Source: refined user request to structure the growing map and feature controls with tabs, following the existing edit-sidebar and project-settings tab patterns.
Today, map-object-visibility-control.tsx renders the Points, Lines, and Areas sections as one vertically scrolling Features popover. The map route mounts this control separately from Base layers, Images, and the conditional Avesmaps control. The existing Tabs primitives in tabs.tsx already provide the project’s established tab styling.
This plan organizes the custom feature sections into tabs, keeps all other controls separate, and persists the selected feature tab per map in localStorage alongside the existing visibility state.
Decisions¶
| Decision | Choice | Why |
|---|---|---|
| Control structure | Keep separate controls for Base layers, Images, Features, and Avesmaps | Confirmed with user; the task is a focused refactor of the custom feature control rather than a new consolidated layer panel |
| Feature tabs | Point, Line, and Area tabs inside the Features popover | Matches the existing three sections and reduces the height of the large control without changing its content |
| Avesmaps control | Keep the standalone Avesmaps trigger and popover | Confirmed with user; Avesmaps remains a separate dataset-specific control |
| Visibility behavior | Preserve individual checkboxes, group checkboxes, indeterminate states, and per-map disabled-id persistence | The task changes organization only; existing map rendering behavior must remain unchanged |
| Active tab state | Controlled tab state persisted under a feature-tab-specific, map-specific localStorage key |
Confirmed with user; the selected tab should survive reopening/revisiting the map without adding server or project-data overhead |
| Initial tab | Point when no saved tab exists or the saved value is invalid/unavailable | Provides a deterministic default and handles renamed/removed tabs gracefully |
| Empty sections | Keep tabs available but render the existing empty-state behavior for a kind with no types | Stable navigation is preferable to conditionally changing the tab bar as project types change |
| Responsive behavior | Keep the popover width bounded and make the tab list/content usable on narrow viewports without clipping or overlap | Confirmed with user; the current control can become very large and must remain usable across viewport sizes |
| Data/server scope | No loader, action, route-table, project model, Directus, or schema changes | All required type definitions and visibility state are already available in the client route; no directus/schema/ snapshot exists in this workspace |
Prerequisites¶
- Preserve the existing
useMapObjectVisibilitySelectionAPI and itsfeatureVisibility.<mapId>storage key. - Use the existing
Tabs,TabsList,TabsTrigger, andTabsContentcomponents from~/components/ui/tabsrather than adding another tab implementation. - Keep
MapObjectTypeSectionas the owner of grouped and ungrouped type rendering; tabs should only determine which section is visible. - Confirm from project-map.tsx that the Features control remains independently mounted from Base layers, Images, and Avesmaps after the refactor.
- No Directus field is read or written by this feature, so no schema change or authorization work is required.
Step 1 — Add persisted active-tab state¶
Goal: The Features control has a small client-only state hook or equivalent local state that restores and persists the selected Point/Line/Area tab per map.
Scope
- In: map-object-visibility-control.tsx, add a feature-tab-specific storage key derived from
mapIdand a typed tab value for Point, Line, and Area. - In: Read the saved value after hydration, accept only known tab values, and fall back to Point for missing, malformed, blocked, or stale storage.
- In: Persist changes only after hydration, following the existing storage error-handling conventions in this module.
- Out: Changes to the existing feature visibility storage key, checkbox state, map rendering, or server persistence.
Acceptance criteria
- A map with no saved active tab starts on Point.
- A valid selected tab is restored for the same map after reload/revisit.
- A different map does not inherit the previous map’s active tab.
- Invalid JSON, an unknown tab value, or unavailable storage falls back without breaking the control.
- Visibility preferences continue using their existing storage key and behavior.
-
pnpm typecheckpasses.
Step 2 — Replace the stacked feature sections with tabs¶
Goal: The Features popover presents Point, Line, and Area content through the existing tab components while retaining each section’s current controls.
Scope
- In: map-object-visibility-control.tsx, render a tab list and one tab panel for each map-object kind inside the existing card and scrollable content area.
- In: Move each existing
MapObjectTypeSectioninvocation into its matching tab panel, preserving all type arrays, enable predicates, type toggles, group toggles, labels, counts, and separators. - In: Bind the tab root to the active-tab state from Step 1 and update that state when a tab is selected.
- In: Keep the Features trigger label/icon, popover alignment, card title, width, and independent control boundary unless responsive adjustments require a narrowly scoped class change.
- Out: Any changes to BaseLayerControl, ImageLayerControl, AvesmapsLayerControl, map rendering, or project data.
Acceptance criteria
- The Features popover contains Point, Line, and Area tabs using the existing tab primitives and styling conventions.
- Only the selected kind’s section is visible at a time.
- Point retains POI group/type checkboxes; Line retains line group/type checkboxes; Area retains area group/type checkboxes.
- Individual toggles, group toggles, indeterminate states, and newly introduced types default-to-visible exactly as before.
- Switching tabs does not reset visibility selections or trigger a server request.
- The standalone Avesmaps button and popover remain unchanged and independently usable.
-
pnpm typecheckpasses.
Step 3 — Tune the tabbed popover for responsive use¶
Goal: The reorganized control remains legible and operable at narrow and wide viewport sizes, including projects with long type/group names.
Scope
- In: map-object-visibility-control.tsx, adjust only the local tab list, panel, scroll-area, and popover classes needed to prevent clipping, horizontal overflow, or overlap.
- In: Use stable content dimensions compatible with the existing
ScrollArea; preserve vertical scrolling for long type lists. - In: Ensure tab labels remain readable and selectable on narrow widths, using the existing tab component behavior rather than introducing a new responsive abstraction.
- Out: Global CSS, changes to the shared tab component, redesign of unrelated map controls, and changes to feature labels/data.
Acceptance criteria
- On a narrow viewport, all three tab triggers remain visible or are reachable without horizontal clipping.
- Long group/type labels remain inside the popover and do not overlap checkboxes, counts, or neighboring content.
- Long lists remain vertically scrollable within the popover.
- On a wide viewport, the control remains aligned with the map controls and does not grow into an oversized panel.
- Manual responsive checks show no console errors or layout overlap.
-
pnpm typecheckpasses.
Step 4 — Verify behavior and regression boundaries¶
Goal: Confirm the tab refactor changes organization only and does not regress map visibility or the neighboring controls.
Acceptance criteria
-
pnpm typecheckpasses. -
pnpm formatcompletes successfully. -
pnpm buildpasses. - Manual check: open a map with POI, line, and area types; each tab displays its correct content and only the selected panel is shown.
- Manual check: toggle individual and grouped feature types, including a partially enabled group, then switch tabs and confirm state is retained.
- Manual check: reload/revisit the map and confirm the selected feature tab and visibility preferences are restored independently.
- Manual check: verify Base layers, Images, and the conditional Avesmaps control still open and function independently.
- Manual check: repeat the feature-control check at narrow and wide viewport sizes with a long list of types.
- No route, loader, action,
data/projects.json, or Directus data changes are introduced by the feature.
Risks / open questions¶
- There is no
directus/schema/directory or JSON snapshot in this workspace. The planned change reads and writes no Directus field, so this does not block implementation. - The existing tab primitive uses horizontal triggers by default; responsive verification should catch whether the fixed popover width needs a local adjustment without changing the shared component.