Skip to content

feat: node list density switching with compact layout and field toggles#5444

Draft
jamesarich wants to merge 50 commits into
mainfrom
feat/node-list
Draft

feat: node list density switching with compact layout and field toggles#5444
jamesarich wants to merge 50 commits into
mainfrom
feat/node-list

Conversation

@jamesarich
Copy link
Copy Markdown
Collaborator

@jamesarich jamesarich commented May 13, 2026

Node List Item Layout Redesign

Redesigned compact and complete density node items with M3 Expressive patterns.

Changes

  • M3 Expressive card treatment: node-color border (always visible) + spring-animated glow on packet received
  • Neutral card backgrounds (compliant with meshtastic/design Standards v1.4 §1)
  • M3 color roles replace alpha-based text emphasis
  • Online status indicated by green-tinted LastHeardInfo (StatusGreen) — no separate icon
  • Battery fill corrected: fills from flat end toward terminal
  • 4-row compact layout: name row (with chip inline), health row, metrics row, footer row
  • All rows use SpaceBetween — items flush left/right with even gaps
  • Battery below node chip (Column 1) — not duplicated in health row
  • Compact footer: icon + value (no labels, no separators)
  • Bearing direction shown as rotated compass icon
  • Environment metrics row in compact (temp, humidity, pressure — icon + value)
  • MetricsGrid 3-column alignment in complete view
  • showTelemetry toggle gates metrics in both views
  • HorizontalDivider before Complete footer section

Screenshots

Compact — All Fields (Light / Dark)

Compact All Fields Light
Compact All Fields Dark

Compact — Active Node (Light / Dark)

Compact Active Light
Compact Active Dark

Complete — Full (Light / Dark)

Complete Light
Complete Dark

Complete — Active Node (Light / Dark)

Complete Active Light
Complete Active Dark

Toggle Matrix — Compact (Light / Dark)

Compact Toggle Matrix Light
Compact Toggle Matrix Dark

Toggle Matrix — Complete (Light / Dark)

Complete Toggle Matrix Light
Complete Toggle Matrix Dark

Settings Panel

Settings Compact
Settings Complete

@github-actions github-actions Bot added the enhancement New feature or request label May 13, 2026
@jamesarich jamesarich added this to the 2.8.0 milestone May 14, 2026
jamesarich and others added 15 commits May 18, 2026 08:09
…toggles

Implement the Node List Layout feature (Phases 1-6):

- Add NodeListDensity enum (COMPLETE/COMPACT) in core:model
- Add 10 DataStore preferences for density and field toggles
- Create NodeItemCompact with two-column layout, adaptive chip sizing,
  and toggle-driven fields (power, last heard, location, hops, signal,
  channel, role, telemetry)
- Add accessibility semantics (mergeDescendants, contentDescription,
  Role.Button) to both NodeItem and NodeItemCompact
- Create NodeLayoutSettings with SegmentedButton density picker and
  9 SwitchPreference toggles for compact mode
- Integrate settings into Android and Desktop settings screens
- Wire NodeListScreen to delegate between layouts based on density
- Create NodeListHelp ModalBottomSheet with signal quality legend
- Add help IconButton to NodeListScreen app bar
- Add ~23 new string resources for layout settings and help text
- Update FakeUiPrefs for test compatibility

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…emetry, tests

- Remove @file:Suppress("detekt:ALL") from NodeListScreen.kt
- Fix 3 detekt violations: remove unused onNavigateToChannels param,
  add modifier param to NodeListScreen, remove blank line
- Plumb lastHeardIsRelative through LastHeardInfo → NodeItemCompact →
  NodeListScreen for relative/absolute time toggle
- Add hasPowerMetrics icon to CompactTelemetryIcons (3 of 3 model-supported)
- Update buildNodeDescription() a11y helper for relative time flag
- Add BuildNodeDescriptionTest (14 tests) covering signal visibility,
  battery range, hops, distance, favorite, and online/offline
- Add NodeListDensityTest (6 tests) covering density string fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rsing

- H1: Wrap buildNodeDescription in remember(thatNode) in NodeItem and
  remember(thatNode, lastHeardIsRelative) in NodeItemCompact to avoid
  runBlocking formatAgo calls on every recomposition during scroll
- H2/M2: Extract NodeListDensity.fromName() companion method, replace
  3 duplicated entries.firstOrNull fallback sites (ViewModel + 2 Settings)
- H3: Remove NODE_LIST_DENSITY from boolean enum, use companion const
  DENSITY_KEY + DEFAULT_DENSITY for string pref type-safety
- L2: Update NodeListDensityTest to test fromName() directly instead
  of duplicating the parsing logic

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- M7: Extract buildNodeDescription() from NodeItem.kt into its own
  BuildNodeDescription.kt file with internal visibility, eliminating
  orphaned public API in a component package
- M4: Replace inline .collectAsStateWithLifecycle().value with by
  delegation in both SettingsScreen and DesktopSettingsScreen for
  consistent state observation pattern
- Extract magic numbers (SNR_UNSET_THRESHOLD, MAX_BATTERY_PERCENT)
  to named constants to satisfy detekt MagicNumber rule

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- FR-013: Add online/offline icon (green checkmark / orange moon) before
  timestamp in Row 2
- FR-012: Row 1 now shows only PKC icon, name, and favorite star per spec
  (removed NodeStatusIcons from compact name row)
- FR-019: Device Role section now renders conditional unmessageable and
  MQTT icons alongside the role icon
- Remove unused connectionState/deviceType params from NodeItemCompact

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add onClickLabel/onLongClickLabel to combinedClickable in both
  NodeItem and NodeItemCompact for proper TalkBack action announcements
- Add semantics heading() to section titles in NodeListHelp bottom sheet
  for screen reader navigation between sections
- Add string resources: node_list_click_label, node_list_long_click_label

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
M4: Extract NodeDescriptionStrings data class with rememberNodeDescriptionStrings()
    composable resolver. buildNodeDescription now takes localized strings param
    instead of hardcoded English. Added 9 a11y_node_* string resources.

M1: Replace mutableListOf<@composable> with keyed Pair<String, @composable>
    list in CompactCombinedRow. Each item gets a stable key() wrapper for
    correct Compose identity across recompositions.

M2: Bump compact icon size from 14dp to 16dp (M3 minimum for dense UI).
    Extract COMPACT_ICON_SIZE_DP constant for consistency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add @PreviewLightDark composables for:
- NodeItem (complete density): remote node + active/this-node
- NodeItemCompact: all fields, minimal, active/this-node
- NodeLayoutSettings: compact toggles + complete description

Add corresponding @previewTest entries in NodeScreenshotTests and
SettingsScreenshotTests with reference screenshots (14 images).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace adaptive chip height formula (36-70dp based on row count) with
a fixed 48dp square. The adaptive sizing caused the avatar to tower
over the content when multiple rows were visible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove custom chip sizing — let NodeChip use its own defaultMinSize
(minWidth=64dp, minHeight=28dp) for consistent dimensions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use StatusYellow for favorite star in Compact layout to match Complete
layout's NodeStatusIcons behavior. Remove unused contentColor param
from CompactNameRow. Regenerate affected screenshots.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add OnlineStatusIcon (green checkmark/gray sleep) next to LastHeardInfo
in the Complete layout header, matching Compact's behavior. Icon only
shows for remote nodes (not thisNode, which already has ConnectionsNavIcon).
Regenerate affected Complete screenshots.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TransportIcon was hardcoded to Color.White, causing inconsistent tinting
in the node list. Default to LocalContentColor.current so the icon
inherits the correct content color from its container. Preserve explicit
Color.White for MessageItem where the chat bubble requires it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 20 unit tests verifying:
- nodeListDensity defaults to COMPLETE and round-trips COMPACT
- All 9 boolean toggles default to true (except lastHeardIsRelative = false)
- All setters persist their values correctly

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jamesarich and others added 4 commits May 18, 2026 08:47
These composables are pure UI components with no feature-level logic.
Moving them to core:ui makes them reusable across feature modules
(e.g., feature:settings for live preview) without cross-feature deps.

Also moves BuildNodeDescription and NodeStatusIcons (helpers used by
NodeItem) and fixes detekt parameter ordering in NodeItem.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Renders a comprehensive sample node directly in the settings screen
so users can see how toggling density and field visibility affects
the node list appearance in real-time. Fulfills FR-008.

The preview shows a realistic node with all fields populated:
- Battery, signal, position, hops, channel, role
- Environment metrics (temperature, humidity, pressure)
- PKC key, favorite status

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add NodeLayoutSettingsCompactMinimalPreview showing toggles-off state
- Add ScreenshotNodeLayoutSettingsCompactMinimal screenshot test
- Move BuildNodeDescriptionTest to core:ui (follows the code it tests)
- Update screenshot references to reflect live preview in settings
- Update import in SettingsScreenshotTests for new preview

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Status: Not Started → Implemented
- FR-008: Updated to reflect hardcoded sample node (not Room query)
- Component table: NodeItem/NodeItemCompact now in core:ui
- Assumptions: Updated live preview description

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

⚠️ JUnit XML file not found

The CLI was unable to find any JUnit XML files to upload.
For more help, visit our troubleshooting guide.

jamesarich and others added 5 commits May 18, 2026 15:47
Add 6 isolated screenshot test variants for the preview sample node:
- Complete density (Celsius/metric)
- Complete density (Fahrenheit/imperial)
- Compact with all fields enabled
- Compact with signal + last heard only
- Compact with name only (no optional fields)

Each variant captured in both light and dark themes (12 new images).
Makes previewSampleNode() internal for cross-module preview access.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Split the single CompactCombinedRow into two logical rows that mirror
the Complete density layout:

Row 3 (Position + Signal): distance | elevation | hops | signal | channel | sats
Row 4 (Device + Telemetry): hardware | role+unmessageable+mqtt | node ID | telemetry icons

Previously, all fields were jammed into one row making it hard to
visually correlate field positions between Complete and Compact.

Added fields previously missing from Compact:
- Elevation (controlled by showLocation toggle)
- Satellite count (controlled by showLocation toggle)
- Hardware model (controlled by showRole toggle)
- Node ID (controlled by showRole toggle)

No new toggles needed — fields are grouped logically under existing
toggles (showLocation = GPS data, showRole = device identity).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Distance was never visible in screenshots because thisNode was null,
making thisNode?.distance(thatNode) return null.

Added previewLocalNode() ~1.6km away from the sample node so distance
renders in both Complete and Compact previews when showLocation is on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When all fields are enabled, the single Row overflows on narrower
screens. Switch to FlowRow so items wrap to the next line instead
of being clipped off the right edge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace FlowRow-based compact layout with structured 3-row design:
- Row 1 (Identity): Name + PKC icon + favorite star
- Row 2 (Health): Online dot + last heard + battery + distance + signal
- Row 3 (Footer): Hardware · role · hops · channel + telemetry icons

Use middot separators instead of VerticalDivider for cleaner wrapping.
Battery shows icon+percentage, signal shows qualitative label.
Footer uses labelSmall typography with reduced emphasis.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace SpaceBetween with spacedBy(12.dp) + weight/Spacer for more
consistent visual spacing across battery, signal, and footer rows.
MetricsGrid uses spacedBy(12.dp) instead of SpaceBetween.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove unmessageable/mqtt text symbols (✗/⌁) from compact footer —
they were rendering as unrecognizable glyphs. Remove fillMaxWidth from
footer row so telemetry icons sit adjacent to text instead of being
pushed to far right.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jamesarich and others added 2 commits May 18, 2026 20:00
Adjusted drawBehind insets to align with ic_battery_horiz_000 viewport:
- insetLeft: 0.11 → 0.25 (inner body starts at x=240/960)
- insetRight: 0.22 → 0.167 (inner body ends at x=800/960)
- insetVertical: 0.28 → 0.375 (inner body y=360→600 in 960h)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace Row with FlowRow in CompactHealthRow so long absolute dates
  wrap instead of pushing signal off-screen
- Increase footer spacedBy from 4dp to 6dp for even spacing between
  text segments and status icons

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jamesarich and others added 22 commits May 18, 2026 20:21
- Add hopsAway parameter to previewSampleNode() for direct-heard variant
- Use relative time in signal-only preview (absolute date formatter
  crashes in headless screenshot test JVM)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Matches Complete view placement — unmessageable icon now appears in the
top row next to the favorite star instead of buried in the footer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In compact mode, distance now uses IconInfo directly without the
'Distance' label text — just the icon and the value (e.g. '2.2 km').
This matches the compact convention of icon+value for all fields.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The pin-drop, temperature, and power icons in compact mode were
confusing — they indicated data availability but without context.
Complete view already shows actual telemetry values directly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- showTelemetry now toggles the environment sensors grid (temp, humidity,
  baro, soil, voltage, current, IAQ) in Complete view
- Renamed toggle label from 'Log Icons' to 'Environment Metrics'
- Toggle was previously a no-op after removing telemetry presence icons
  from compact mode

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Shows temp, humidity, and barometric pressure as icon + value only
(no labels). Controlled by the 'Environment Metrics' toggle.
Uses FlowRow for wrapping on narrow screens.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace ROUTER/CLIENT/etc text with the role-specific icon
(mountain flag for router, person for client, etc). Keeps the
footer more compact and visually scannable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rewrite CompactFooterRow with SegmentedRow helper (middot-separated)
- Hardware: icon + model name (no label)
- Role: icon only (icon IS the information)
- Hops: icon + count number
- Channel: numbered counter icon only
- MQTT: icon only when via MQTT
- Extract channelIcon() helper (Counter0–Counter8 mapping)
- Remove stale @Suppress("UnusedParameter") now that showTelemetry is used

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Matrix screenshots show node items in various toggle combinations:
- Compact: all fields, health only, no metrics, no footer, metrics+footer, minimal
- Complete: with/without metrics toggle

Each row labeled with active state for quick visual reference.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move the avatar chip from a separate leading column into the name row.
This allows health, metrics, and footer rows to span the full card width
instead of being constrained by the chip's horizontal space.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rows

All compact rows (health, metrics, footer) now use Arrangement.SpaceEvenly
to distribute items across the full card width instead of middot-separated
clusters. Removes FlowRow dependency since wrapping is no longer needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Items start flush left and end flush right with even gaps between.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Computes bearing between local and remote node, converts to 8-point
compass direction (N, NE, E, SE, S, SW, W, NW), and appends to distance
text (e.g. '2.3 km NW'). Shown in both compact and complete views.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace text compass direction (NW, SE, etc.) with a rotated MapCompass
icon. Bearing is now a separate visual field — the compass arrow points
in the direction of the remote node. Shown in both compact and complete.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add FR-029 to FR-034: neutral card background, node-color border,
  packet-received glow animation with spring physics, M3 color roles
  for text hierarchy, SpaceBetween layout, bearing as rotated compass
- Add NFR-005: glow animation performance constraint
- Add Phase 8 tasks (NL-T048 to NL-T055) with dependency graph
- Update architecture section with M3 Expressive card treatment docs
- Add risks for glow perf and dark node color visibility
- Aligns with meshtastic/design standards v1.4 §1 (Circle Standard)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Resolve FR-014 vs FR-033 conflict: FR-014 now defers to FR-033
  (FlowRow + SpaceBetween, no VerticalDivider)
- Fix NL-T020 task description to match FR-033
- Clarify NL-T052 as regression fix (chip-inline was iterative drift)
- Replace duplicate NL-T053 (adaptive sizing) with FR-034 (bearing icon)
- Fix critical path in tasks.md to include Phase 8
- Add Constitution V cross-platform spec exemption justification
- Add NodeCardGlow to Key Components table

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove node-color background tinting, use neutral surface (FR-029)
- Add node-color BorderStroke modulated by online state (FR-030)
- Implement packet-received glow animation with spring physics (FR-031)
  - New NodeCardGlow.kt: Animatable + fastSpatialSpec bloom + slowSpatialSpec decay
  - Zero overhead when not animating (shadow only applied when alpha > 0)
- Replace alpha-based text emphasis with M3 color roles (FR-032)
  - contentColor.copy(alpha=0.7f) → MaterialTheme.colorScheme.outline
- Restore two-column layout in compact mode (FR-009, FR-052)
  - Column 1: NodeChip + battery, Column 2: content rows
- Add HorizontalDivider before Complete footer (FR-054)
- Compliant with Design Standards v1.4 §1 (neutral card background)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The LaunchedEffect(lastHeard) was triggering on first composition when
lastHeard > 0 (which is always true for nodes heard in the past). This
caused every card to glow when scrolling into view in a LazyColumn.

Fix: track previous lastHeard value to distinguish initial composition
from actual changes. Only animate when the value genuinely changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Battery is already displayed below the node chip in Column 1. Having it
again in the health row was redundant and caused signal quality text to
ellipsize ('G...' instead of 'Good') due to overcrowding.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eardInfo

Remove separate Success/DeviceSleep icons from compact health row and
complete header. Instead, tint the LastHeardInfo antenna icon directly
with tertiary (online) or outline (offline) color based on 2-hour
lastHeard threshold.

Cleaner visual — one icon conveys both 'last heard time' and 'online
status' through color alone.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Battery fill now grows from flat end (right) toward terminal (left),
matching standard UI convention. Previously filled in reverse.

Also switch online status tint from tertiary (blue) to StatusGreen,
matching the established connected/good color pattern used throughout
the app (ConnectionsNavIcon, LoraSignalIndicator, SecurityIcon).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request needs-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant