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.jsonmigration/backfill logic beyond the existing optional-field defaulting already inreadProjectsFile/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?toPoiTypeDefinition; addlabelHidden?/labelMinZoom?/labelMaxZoom?toPointOfInterest. - 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 narrowlabelMinZoom/labelMaxZoomand 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.tsxstill 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.