Skip to content

Label visibility controls, compact grouped edit forms, and selection persistence — implementation plan

Source: user request ("improved marker/line/area edit form: better group options with headings/sections: basics, marker/display, label; allow to hide label or set min/max zoom for label; make it more compact, as we will add more options in the future; when item is selected in view-mode, selection should be kept when entering edit mode").

Today, PointOfInterest/LineFeature/AreaFeature and their type definitions (poi.ts, line.ts, area.ts) only carry a marker/line/area-level minZoom/maxZoom, resolved instance-over-type by feature-zoom.ts's resolveZoomRange. FeatureLabelLayer in map.tsx reuses that exact same resolved range to decide whether to draw the name label — there is no way to hide a label independently of its marker, or to give it its own zoom window. PoiForm/LineForm/AreaForm in project-map.tsx render every field in one flat column with no grouping. Switching from view mode to edit mode (or back) always clears the current selection (handleModeChange/handleSelectRequest etc. in project-map.tsx).

This plan adds labelHidden (instance-only) and labelMinZoom/ labelMaxZoom (type + instance, resolved the same way as marker zoom) to all three feature kinds, wires them through the server, makes FeatureLabelLayer honor them, reorganizes the three instance edit forms into headed "Basics / Display / Label" sections, adds the two new label zoom fields to the three type-definition editors in project.tsx, and carries the current selection across view↔edit mode switches.


Decisions

Decision Choice Why
labelHidden New boolean, instance-only (not on type definitions), independent of any zoom range — when true the label never renders regardless of zoom Confirmed with user
Label zoom range New labelMinZoom/labelMaxZoom on both the type definitions and the instances, resolved with the existing resolveZoomRange (instance → type → unlimited) Confirmed with user; reuses the exact fallback rule already used for marker zoom, no new resolution logic
FeatureLabelLayer Computes a second resolved range per candidate (the label range) instead of reusing the marker-visibility range; also skips any candidate with labelHidden The label's visible zoom window is now decoupled from the marker/line/area's own visibility window
Type-editor scope PoiTypeForm/LineTypeForm/AreaTypeForm in project.tsx gain labelMinZoom/labelMaxZoom inputs added to their existing flat layout Confirmed with user; no labelHidden at type level (it's instance-only) and no section headings added here — the grouped/compact layout is scoped to the instance forms only
Instance-form reorg scope Only PoiForm/LineForm/AreaForm in project-map.tsx get restructured into "Basics" / "Marker" (or "Line"/"Area") display / "Label" sections Confirmed with user
Compactness Fields that can reasonably share a row (e.g. the two zoom-number pairs) move to two-column grids; section headings use the same small/muted style already used for FieldSet-style groups elsewhere in the app Leaves room for future fields per the request, without inventing a new visual language
Selection persistence Both directions: view → edit opens the currently-selected item for editing; edit → view re-selects the item that was being edited Confirmed with user
Dataset (avesmaps) feature selection Left as-is (still cleared on any mode change) Dataset features aren't editable, so there is nothing to carry into edit mode
Data migration None — all new fields are optional; missing values mean "not hidden, no override" Matches how minZoom/maxZoom already default today

Out of scope

  • Any visual treatment that distinguishes a hidden/zoom-limited label from a normal one on the map (no fade, no indicator) — it's a binary render/don't-render.
  • Bulk or multi-select editing.
  • Managing/editing poiconfig.json, PoiTypeDefinition/etc. beyond the two new fields.
  • Dataset (avesmaps) feature selection carrying across mode switches.
  • Any change to data/projects.json migration/backfill logic beyond the existing optional-field defaulting already in readProjectsFile/normalizeProject.

Step 1 — Add label fields to the three data models

Goal: PointOfInterest, LineFeature, AreaFeature and their type definitions each gain labelHidden?: boolean (instance types only) and labelMinZoom?: number / labelMaxZoom?: number (both instance and type definition types). Type-only change, no logic yet.

  • poi.ts: add labelMinZoom?/labelMaxZoom? to PoiTypeDefinition; add labelHidden?/labelMinZoom?/labelMaxZoom? to PointOfInterest.
  • line.ts: same additions to LineTypeDefinition/LineFeature.
  • area.ts: same additions to AreaTypeDefinition/AreaFeature.

Acceptance criteria - [ ] All six types compile with the new optional fields. - [ ] pnpm typecheck passes.

Step 2 — Thread label fields through the POI server functions

Goal: addPointOfInterest, updatePointOfInterest, addPoiType, and updatePoiType in projects.server.ts accept and persist labelHidden/labelMinZoom/labelMaxZoom (the type functions only take the zoom pair, not labelHidden), following the exact pattern already used for minZoom/maxZoom in each function.

Acceptance criteria - [ ] addPointOfInterest/updatePointOfInterest accept and store labelHidden, labelMinZoom, labelMaxZoom on the PointOfInterest. - [ ] addPoiType/updatePoiType accept and store labelMinZoom, labelMaxZoom on the PoiTypeDefinition. - [ ] pnpm typecheck passes.

Step 3 — Thread label fields through the line server functions

Goal: Same change as Step 2, applied to addLine, updateLine, addLineType, updateLineType.

Acceptance criteria - [ ] Same two criteria as Step 2, adapted to lines. - [ ] pnpm typecheck passes.

Step 4 — Thread label fields through the area server functions

Goal: Same change as Step 2, applied to addArea, updateArea, addAreaType, updateAreaType.

Acceptance criteria - [ ] Same two criteria as Step 2, adapted to areas. - [ ] pnpm typecheck passes.

Step 5 — FeatureLabelLayer honors labelHidden and the label zoom range

Goal: In map.tsx, FeatureLabelLayer's candidate-building loop (POI/line/area) skips any item with labelHidden, and computes each candidate's zoom gate from a resolveZoomRange call over labelMinZoom/labelMaxZoom (instance and type) instead of reusing the marker/line/area's own resolved range. Candidate priority (used for collision thinning) is based on this new resolved label range's minZoom, same as today.

Acceptance criteria - [ ] A POI/line/area with labelHidden: true never appears in visibleLabels, at any zoom. - [ ] A POI/line/area with a labelMinZoom/labelMaxZoom narrower than its own marker visibility range shows its marker but hides its label outside that narrower range. - [ ] An item with no label zoom override still falls back to its type's label zoom range, then to always-visible — matching resolveZoomRange's existing fallback order. - [ ] pnpm typecheck passes.

Step 6 — Parse label fields in the project-map.tsx action

Goal: The add/update/add-line/update-line/add-area/update-area intents in project-map.tsx's action parse labelHidden (checkbox-style boolean) and labelMinZoom/ labelMaxZoom (via the existing parseOptionalNumber helper) from formData, and pass them through to the corresponding projects.server.ts calls added in Steps 2–4.

Acceptance criteria - [ ] Submitting any of the six intents with label fields present in the form persists them via the Step 2–4 server functions. - [ ] Omitting the label fields (e.g. checkbox unchecked, zoom inputs blank) persists labelHidden: false/undefined and labelMinZoom/labelMaxZoom: undefined, matching how blank minZoom/maxZoom behave today. - [ ] pnpm typecheck passes.

Step 7 — Reorganize PoiForm into Basics / Marker / Label sections

Goal: PoiForm in project-map.tsx is restructured into three headed, compact sections: Basics (name, description, type), Marker (existing minZoom/maxZoom, laid out as a two-column row), Label (a hide toggle plus a labelMinZoom/labelMaxZoom two-column row). Default values and hidden-field wiring follow the existing pattern.

Acceptance criteria - [ ] The form renders three visually distinct sections with headings, in the order Basics → Marker → Label. - [ ] The Label section includes a control to set labelHidden and two number inputs for labelMinZoom/labelMaxZoom, all pre-filled from defaultValues when editing. - [ ] The form's overall vertical footprint is smaller than today's for the same field set (e.g. the zoom pairs share rows instead of stacking). - [ ] Submitting still posts labelHidden/labelMinZoom/labelMaxZoom alongside the existing fields. - [ ] pnpm typecheck passes.

Step 8 — Reorganize LineForm into Basics / Line / Label sections

Goal: Same restructuring as Step 7, applied to LineForm (section label "Line" instead of "Marker").

Acceptance criteria - [ ] Same criteria as Step 7, adapted to lines. - [ ] pnpm typecheck passes.

Step 9 — Reorganize AreaForm into Basics / Area / Label sections

Goal: Same restructuring as Step 7, applied to AreaForm (section label "Area" instead of "Marker").

Acceptance criteria - [ ] Same criteria as Step 7, adapted to areas. - [ ] pnpm typecheck passes.

Step 10 — Add label zoom fields to the type-definition editors

Goal: In project.tsx, the add-poi-type/update-poi-type, add-line-type/update-line-type, and add-area-type/update-area-type action branches parse labelMinZoom/ labelMaxZoom (via the existing local parseOptionalNumber) and pass them to the Step 2–4 server functions; PoiTypeForm, LineTypeForm, and AreaTypeForm each gain two number inputs for labelMinZoom/labelMaxZoom alongside their existing minZoom/maxZoom inputs, with no section headings added (these forms stay in their current flat layout).

Acceptance criteria - [ ] All three type-definition forms have labelMinZoom/labelMaxZoom inputs, pre-filled from defaultValues when editing a type. - [ ] Saving any of the six type intents persists the new fields on the corresponding type definition. - [ ] pnpm typecheck passes.

Step 11 — Persist selection across view↔edit mode switches

Goal: In project-map.tsx, switching modes carries the current selection over instead of clearing it: handleModeChange("edit") sets editingPoiId/editingLineId/editingAreaId from whichever of selectedPoiId/selectedLineId/selectedAreaId is currently set (if any), and handleModeChange("view") sets the corresponding selected*Id from whichever editing*Id is currently set. Dataset feature selection (selectedDatasetFeatureId) is still cleared on any mode change, as today.

Acceptance criteria - [ ] Selecting a POI in view mode, then switching to edit mode, opens that POI in the edit sidebar (no extra click). Same for a selected line or area. - [ ] Editing a POI, then switching to view mode, shows that POI's view-mode detail panel (no extra click). Same for an edited line or area. - [ ] Switching modes with a dataset feature selected (and nothing else) behaves as today — no dataset-feature carry-over. - [ ] pnpm typecheck passes.

Step 12 — Verification pass

Goal: Confirm all three feature kinds behave consistently and nothing regressed.

  • Manually exercise, per kind (POI/line/area): set labelHidden, confirm the label disappears on the map at every zoom; set a narrow labelMinZoom/labelMaxZoom and confirm the marker stays visible while the label toggles in/out at the boundary zooms; leave label fields blank and confirm the type-level label zoom (if set) still applies.
  • Manually exercise the two mode-switch directions for each kind.
  • Confirm the type-definition editors in project.tsx still work for the existing fields (name/color/icon/group/minZoom/maxZoom) alongside the two new ones.

Acceptance criteria - [ ] pnpm typecheck passes. - [ ] pnpm format passes. - [ ] pnpm build passes. - [ ] Manual checks above pass with no console errors.