Skip to content

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 useMapObjectVisibilitySelection API and its featureVisibility.<mapId> storage key.
  • Use the existing Tabs, TabsList, TabsTrigger, and TabsContent components from ~/components/ui/tabs rather than adding another tab implementation.
  • Keep MapObjectTypeSection as 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 mapId and 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 typecheck passes.

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 MapObjectTypeSection invocation 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 typecheck passes.

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 typecheck passes.

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 typecheck passes.
  • pnpm format completes successfully.
  • pnpm build passes.
  • 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.