20 KiB
Tasks: Node List Layout
Input: Design documents from /specs/002-node-list-layout/
Prerequisites: plan.md (required), spec.md (required), research.md, data-model.md, m3-accessibility-audit.md
Tests: Included — spec explicitly defines test scenarios per user story and plan references Phase 7 testing.
Organization: Tasks grouped by user story (US1–US4) with shared infrastructure in Setup/Foundational phases.
Format: [NL-TXXX] [P?] [Story?] Description
- [P]: Can run in parallel (different files, no dependencies)
- [Story]: Which user story this task belongs to (e.g., US1, US2, US3, US4)
- Include exact file paths in descriptions
Path Conventions
- KMP commonMain:
feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/ - Core prefs:
core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ - Core UI:
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ - Settings:
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/ - Resources:
core/resources/src/commonMain/composeResources/values/strings.xml
Phase 1: Setup (Shared Infrastructure)
Purpose: Design gate, density enum, DataStore preference keys, and string resources required by all user stories.
- NL-T001
[UI-GATE]Review.skills/design-standards/SKILL.mdand upstream Meshtastic design standards; record constraints forNodeItemCompact,NodeLayoutSettings, density picker, andNodeListHelpsheet styling. This phase blocks all UI work. - NL-T002 [P] Create
feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/NodeListDensity.ktwithenum class NodeListDensity { COMPLETE, COMPACT }(FR-001, FR-002). - NL-T003 [P] Create
NodeListLayoutPreferencesenum incore/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/NodeListLayoutPreferences.ktdefining all 10 DataStore keys (nodeListDensity+ 9 compact toggles) with their defaults per data-model.md (NFR-003). - NL-T004 Add DataStore preference accessors for all 10 keys in
core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt— density asStateFlow<NodeListDensity>(fallback toCOMPLETEfor invalid values), 9 toggles asStateFlow<Boolean>with eager seeding viaSharingStarted.Eagerly(FR-002, FR-004, FR-005). - NL-T005 Add string resources for all toggle labels, density option labels ("Complete", "Compact"), settings section header ("Node Layout"), help sheet text, signal quality labels, and complete-mode descriptive text to
core/resources/src/commonMain/composeResources/values/strings.xml. Runpython3 scripts/sort-strings.pyafter.
Dependencies: NL-T001 blocks all UI phases (2+). NL-T002 and NL-T003 are independent. NL-T004 depends on NL-T002 + NL-T003. Checkpoint: Preference infrastructure ready — all user stories can now begin.
Phase 2: Foundational (Blocking Prerequisites)
Purpose: Accessibility fix for existing NodeItem and ViewModel wiring that MUST complete before any user story can ship.
⚠️ CRITICAL: The existing NodeItem has zero row-level semantics — TalkBack reads 8–12 separate focus stops per node row. This is a HIGH priority fix (audit §2.1).
- NL-T006 [HIGH] Add
Modifier.semantics(mergeDescendants = true)with a composedcontentDescription(aggregating name, connection status, favorite, last heard, online/offline, role, hops, battery, distance, heading, signal strength) androle = Role.Buttonon the outerCardinfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeItem.kt(FR-025, audit §2.1, §2.3). Extract abuildNodeDescription()helper for reuse byNodeItemCompact. - NL-T007 Ensure
NodeItemusestitleMediumEmphasizedfor node names (already at line 423 — verify no regressions) and confirm Complete rows have 3.dp top/bottom padding (FR-027). AdjustColumnpadding from 12.dp if needed to meet the 3.dp outer spec. - NL-T008 Ensure
NodeItemusesLoraSignalIndicator/NodeSignalQualitycomposables for signal display in Complete mode — quality icon + SNR/RSSI text with quality color, not just a colored icon (FR-022). Verify existingNodeSignalRowat line 250 matches spec. - NL-T009 Modify
NodeListViewModelinfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.ktto exposenodeListDensity: StateFlow<NodeListDensity>and all 9 compact toggleStateFlow<Boolean>values fromUiPrefsImpl(FR-002, FR-004).
Dependencies: Phase 1 (NL-T002–NL-T004) must complete first. Checkpoint: Foundation ready — existing NodeItem is accessible, ViewModel exposes density state.
Phase 3: User Story 1 — Switch Between Complete and Compact Density (Priority: P1) 🎯 MVP
Goal: Users can switch between Complete and Compact density modes via Settings and see the node list re-render with the correct row style.
Independent Test: Open Settings > Node Layout, toggle between Complete and Compact, navigate to the Nodes tab, verify the list renders with the correct row style. Relaunch app and verify density persists.
Implementation for User Story 1
- NL-T010 [P] [US1] Create
feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeItemCompact.ktwith the two-columnRowlayout scaffold: Column 1 (fixed width:NodeChip+ optional battery), Column 2 (Modifier.weight(1f):Column(verticalArrangement = spacedBy(2.dp))) (FR-009, FR-026, FR-027). - NL-T011 [US1] Implement Row 1 (always visible) in
NodeItemCompact.kt:NodeKeyStatusIcon(PKC/key status), long name usingtitleMediumEmphasized(M3 Expressive), and favorite star icon. Row is non-toggleable (FR-012, FR-025). - NL-T012 [US1] Add
Modifier.semantics(mergeDescendants = true)on the outerCardinNodeItemCompact.ktwith composedcontentDescription(reusebuildNodeDescription()from NL-T006) androle = Role.Buttonfor TalkBack (FR-025, audit §2.1, §2.3). - NL-T013 [US1] Add compact row padding: 2.dp top/bottom on outer
Column(FR-027 — intentional M3 deviation for density). - NL-T014 [P] [US1] Create
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/NodeLayoutSettings.ktwithSingleChoiceSegmentedButtonRow+SegmentedButtonfor Complete/Compact density selection. Write selected density to DataStore (FR-001, FR-002). - NL-T015 [US1] Add descriptive text in
NodeLayoutSettings.kt: "The Complete layout displays all available node data. Fields with no data are automatically hidden." — shown when Complete is selected (FR-007). - NL-T016 [US1] Integrate
NodeLayoutSettingsinto the existing App Settings screen infeature/settings/(R-005 — embedded section, no new navigation route). - NL-T017 [US1] Modify
NodeListScreeninfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.ktto collectnodeListDensityfrom ViewModel and delegate toNodeItem(Complete) orNodeItemCompact(Compact) per row (FR-009). - NL-T018 [US1] Ensure
LazyColumninNodeListScreen.ktuses stablekey = { it.num }for both layout variants (already present at line 187 — verify no regression) (NFR-004).
Checkpoint: User Story 1 complete — density switching works end-to-end. Compact shows name-only rows, Complete shows existing full layout. Both persist across app restarts.
Phase 4: User Story 2 — Configure Compact Layout Fields (Priority: P1)
Goal: Users can toggle individual data fields in the compact layout via Settings, and the live preview + node list update in real time.
Independent Test: Switch to Compact, disable all toggles one by one, verify each field disappears from both the preview and the node list. Re-enable them and verify they reappear.
Implementation for User Story 2
- NL-T019 [US2] Implement Row 2 (toggle:
shouldShowLastHeard) inNodeItemCompact.kt: online/offline icon (green checkmark / orange moon) + timestamp viaLastHeardInfo, with relative time support vialastHeardIsRelative. Guard with future date filter (> 1 year) (FR-013, FR-028). - NL-T020 [US2] Implement Row 3 combined icons in
NodeItemCompact.ktasRow(horizontalArrangement = spacedBy(6.dp), modifier = Modifier.height(IntrinsicSize.Min))withVerticalDivider(modifier = Modifier.fillMaxHeight())separators (FR-014, audit §1.4). Render in order: Distance+Bearing, Hops Away, Signal, Channel, Device Role, Log Icons — each gated by its toggle AND data conditions. - NL-T021 [US2] Implement Distance+Bearing rendering in Row 3: gate on
shouldShowLocationtoggle + node has positions + node is not connected node + valid location data for both user and node. UseNumberFormatter.format()for float values (FR-015, FR-028). - NL-T022 [US2] Implement Hops Away rendering in Row 3: gate on
shouldShowHopstoggle +node.hopsAway > 0(FR-016). - NL-T023 [US2] Implement Signal rendering in Row 3: gate on
shouldShowSignaltoggle +node.hopsAway == 0+node.snr != 0+!node.viaMqtt. Icon color viadetermineSignalQuality(snr, rssi). MUST includecontentDescription = stringResource(quality.nameRes)(e.g., "Signal: Good") for WCAG 1.4.1 — no color-only information (FR-017, audit §2.6). - NL-T024 [US2] Implement Channel rendering in Row 3: gate on
shouldShowChanneltoggle +node.channel > 0(FR-018). - NL-T025 [US2] Implement Device Role rendering in Row 3: gate on
shouldShowRoletoggle. Show role'sMeshtasticIconsicon + conditional unmessagable, store-and-forward, and MQTT icons (FR-019). - NL-T026 [US2] Implement Log Icons rendering in Row 3: gate on
shouldShowTelemetrytoggle + node has at least one of: positions, environment metrics, detection sensor metrics, or trace routes. Show device metrics, positions (mappin), environment, detection sensor, trace routes (signpost) icons fromMeshtasticIcons(FR-020). - NL-T027 [US2] Implement conditional battery rendering below
NodeChipin Column 1: gate onshouldShowPowertoggle +node.batteryLevel != null(FR-003 toggle order, spec §Toggle Reference). - NL-T028 [US2] Add 9
SwitchPreferencetoggles (fromcore:ui, NOT rawSwitch) inNodeLayoutSettings.kt, ordered by layout position: Power, Last Heard Time, Relative Last Heard Time, Distance and Bearing, Hops Away, Signal (Direct Only), Channel, Device Role, Log Icons. Show only when Compact is selected (FR-003, audit §1.1). - NL-T029 [US2] Implement "Relative Last Heard Time" toggle disabled state (
enabled = false) when "Last Heard Time" is toggled off inNodeLayoutSettings.kt(FR-006). - NL-T030 [US2] Implement live preview composable in
NodeLayoutSettings.ktbelow toggles: query first node from Room KMP sorted bylastHearddescending, render viaNodeItemorNodeItemCompactbased on current density + toggle state usingcollectAsState(). Show placeholder text when database is empty (FR-008).
Checkpoint: User Story 2 complete — all 9 toggles control compact field visibility, live preview updates in real time, toggle states persist across app launches.
Phase 5: User Story 3 — Adaptive Chip Sizing (Priority: P2)
Goal: The NodeChip in compact mode scales proportionally based on the number of active row groups.
Independent Test: Disable all optional rows (last heard + combined row), verify the chip shrinks to 36.dp minimum. Enable all rows, verify it grows to 70.dp maximum.
Implementation for User Story 3
- NL-T031 [US3] Implement
lineCountcomputed property inNodeItemCompact.kt: count active row groups (1 base + 1 ifshouldShowLastHeard+ 1 if any combined-row toggle is enabled). Derive from toggle state, NOT actual data presence (R-003, data-model.md §Adaptive Chip Sizing). - NL-T032 [US3] Implement adaptive chip sizing in
NodeItemCompact.kt:max(36.dp, min(70.dp, 24.dp × lineCount)). UseModifier.defaultMinSize()(not hardModifier.size()) to allow growth with system font scaling > 100% (FR-011, audit §2.8). - NL-T033 [US3] Ensure
NodeChipalways renders as aNodeChipcomposable at all sizes — maintaining consistent M3Cardstyling (FR-010).
Checkpoint: User Story 3 complete — chip scales smoothly across 36.dp/48.dp/70.dp sizes based on toggle configuration.
Phase 6: User Story 4 — Signal Strength Help Documentation (Priority: P3)
Goal: Users can tap a help button on the node list to see a documented legend of signal quality colors and the LoraSignalIndicator composable.
Independent Test: Open node list, tap help icon, scroll to "Node Details" section, verify 4 signal quality entries (Good/Fair/Bad/None) + LoraSignalIndicator entry are present with correct colors and descriptions.
Implementation for User Story 4
- NL-T034 [P] [US4] Create
feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeListHelp.ktas aModalBottomSheetwithrememberModalBottomSheetState(skipPartiallyExpanded = true)(FR-023, audit §1.1). - NL-T035 [US4] Add "Node Details" section with 4 signal quality entries: Good (green, SNR > −7 dB, RSSI > −115 dBm), Fair (yellow, SNR > −12 dB, RSSI > −120 dBm), Bad (orange, SNR > −18 dB, RSSI > −125 dBm), None (red, below all thresholds). Use
Qualityenum drawables fromLoraSignalIndicator.kt(FR-023). - NL-T036 [US4] Add
LoraSignalIndicatorcomposable documentation entry inNodeListHelp.ktshowing the quality icon + description explaining how SNR and RSSI combine into a quality level (Complete layout only) (FR-024). - NL-T037 [US4] Add help
IconButton(NOT rawIcon+clickable) trigger toNodeListScreen.ktthat opens the help sheet via state. Use M3IconButtonfor built-in 48dp minimum touch target (audit §2.5).
Checkpoint: User Story 4 complete — signal help is discoverable and documents all quality levels.
Phase 7: Polish & Cross-Cutting Concerns
Purpose: Performance validation, edge case hardening, and verification across all stories.
- NL-T038 [P] Verify smooth scrolling at 60fps with 200+ nodes in Compact mode. Use
derivedStateOffor computed states to avoid unnecessary recompositions (NFR-002). - NL-T039 [P] Ensure all float values in both layouts use
NumberFormatter.format()before display — distance, SNR, voltage, etc. (FR-028, Constitution §III). - NL-T040 Validate edge cases in
NodeItemCompact.kt: all-toggles-disabled state (name row + 36.dp chip only, battery hidden), missing data (field absent, no placeholder), signal/hops mutual exclusivity, channel 0 hiding, connected node distance exclusion, MQTT signal exclusion, future date guard (spec §Edge Cases). - NL-T041 [P] Write unit tests for
NodeListDensityenum,lineCountcalculation logic (1/2/3 row cases), and invalid density string fallback toCOMPLETEinfeature/node/src/commonTest/. - NL-T042 [P] Write unit tests for DataStore preference defaults (all
trueexceptlastHeardIsRelative=false) incore/prefs/src/commonTest/. - NL-T043 [P] Write unit tests for edge cases: future date filtering (> 1 year), channel 0 hiding, signal/hops mutual exclusivity (
hopsAway == 0vshopsAway > 0), connected node distance exclusion, MQTT signal exclusion (viaMqtt == true) infeature/node/src/commonTest/. - NL-T044 [P] Write Compose UI tests for
NodeItemCompactwith various toggle combinations (all on, all off, partial) infeature/node/src/commonTest/. - NL-T045 [P] Write Compose UI tests for density switching in
NodeListScreen(Complete → Compact → Complete round-trip) infeature/node/src/commonTest/. - NL-T046 Run
./gradlew :feature:node:allTests :feature:settings:allTests :core:prefs:allTeststo validate module tests. - NL-T047 Run
./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTestsfor full verification (Constitution §II, §VI).
Dependencies & Execution Order
Phase Dependencies
- Phase 1 (Setup): No dependencies — NL-T001 blocks all UI. NL-T002 ∥ NL-T003, then NL-T004.
- Phase 2 (Foundational): Depends on Phase 1 (NL-T002–NL-T004). BLOCKS all user stories.
- Phase 3 (US1 — Density Switching): Depends on Phase 2. NL-T010 ∥ NL-T014 (different files).
- Phase 4 (US2 — Field Toggles): Depends on US1 (
NodeItemCompactscaffold + Settings UI). - Phase 5 (US3 — Adaptive Sizing): Depends on US2 (requires toggle logic to compute
lineCount). - Phase 6 (US4 — Help Sheet): Can start after Phase 1 — independent of Phases 3–5. NL-T034 can run in parallel with any UI phase.
- Phase 7 (Polish): Depends on Phases 3–6.
User Story Dependencies
- US1 (P1): Can start after Foundational (Phase 2) — no dependencies on other stories
- US2 (P1): Depends on US1 (
NodeItemCompactscaffold andNodeLayoutSettingsmust exist) - US3 (P2): Depends on US2 (toggle logic needed for
lineCountderivation) - US4 (P3): Independent — can start after Phase 1, no dependencies on US1–US3
Critical Path
Phase 1 → Phase 2 → Phase 3 (US1) → Phase 4 (US2) → Phase 5 (US3) → Phase 7
Parallel Opportunities
Phase 6 (US4) runs in parallel with Phases 3–5
NL-T002 ∥ NL-T003 (Setup)
NL-T010 ∥ NL-T014 (US1 — compact scaffold ∥ settings scaffold)
NL-T034 ∥ any Phase 3–5 task (US4 — help sheet)
NL-T041 ∥ NL-T042 ∥ NL-T043 ∥ NL-T044 ∥ NL-T045 (Phase 7 tests)
Parallel Example: User Story 1
# Launch independent scaffolds together:
NL-T010: "Create NodeItemCompact.kt scaffold with two-column Row layout"
NL-T014: "Create NodeLayoutSettings.kt with SegmentedButton density picker"
# After scaffolds complete, sequential within US1:
NL-T011 → NL-T012 → NL-T013 (compact row details)
NL-T015 → NL-T016 (settings integration)
NL-T017 → NL-T018 (node list wiring)
Dependency Graph
Phase 1 (Setup)
├──→ Phase 2 (Foundational: NodeItem a11y + ViewModel) ──→ Phase 3 (US1: Density Switch)
│ └──→ Phase 4 (US2: Field Toggles)
│ └──→ Phase 5 (US3: Adaptive Sizing)
│ └──→ Phase 7 (Polish)
└──→ Phase 6 (US4: Help Sheet) ──────────────────────────────────────────────→ Phase 7 (Polish)
Implementation Strategy
MVP First (User Story 1 Only)
- Complete Phase 1: Setup (design gate + preferences + strings)
- Complete Phase 2: Foundational (NodeItem TalkBack fix + ViewModel density exposure)
- Complete Phase 3: User Story 1 (density switching end-to-end)
- STOP and VALIDATE: Toggle density, verify list renders correctly, verify persistence
- Ship as MVP — users can switch to Compact (name-only rows for now)
Incremental Delivery
- Phase 1 + Phase 2 → Foundation ready
- Phase 3: US1 → Density switching works → MVP shippable
- Phase 4: US2 → All 9 field toggles work → Full compact experience
- Phase 5: US3 → Adaptive chip sizing → Visual polish
- Phase 6: US4 → Help documentation → Feature complete
- Phase 7 → Tests + verification → Merge-ready
Parallel Team Strategy
With multiple developers:
- All complete Phase 1 + Phase 2 together
- Once Foundational is done:
- Developer A: US1 (Phase 3) → US2 (Phase 4) → US3 (Phase 5) (critical path)
- Developer B: US4 (Phase 6) (independent, can start after Phase 1)
- Both converge at Phase 7 (Testing)