Skip to content

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

Merged
jamesarich merged 64 commits into
mainfrom
feat/node-list
May 21, 2026
Merged

feat: node list density switching with compact layout and field toggles#5444
jamesarich merged 64 commits into
mainfrom
feat/node-list

Conversation

@jamesarich

@jamesarich jamesarich commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Node List Density Switching with Compact Layout and Field Toggles

Summary

Complete redesign of the node list UI with density switching (Compact/Complete), configurable field visibility, device hardware images from CDN, M3 Expressive card theming with node color tint, and a combined device+node details card on the detail screen.

Key Features

  • Density Switching: Complete (full card) and Compact (single-row) modes via settings
  • Field Toggles: 8 independently toggleable fields in Compact mode, per-field visibility in Complete mode
  • Device Hardware Images: CDN-loaded hardware model images with fallback icon
  • Node Color Tint: Subtle 8% card background tint using the node's assigned color
  • Active Border: Color-coded border (0.65α active, 0.3α inactive) for node status indication
  • Glow Animation: M3 Expressive motion bloom/decay glow on packet receipt
  • Settings UX: Live preview above toggles with divider-separated sections and animated height transitions
  • Combined Detail Card: Merged device info and node details on the detail screen

Screenshots

Complete Mode

Light Dark
Complete Light Complete Dark

Complete Mode — Active (Online)

Light Dark
Complete Active Light Complete Active Dark

Compact Mode — All Fields

Light Dark
Compact All Light Compact All Dark

Compact Mode — Active

Light Dark
Compact Active Light Compact Active Dark

Compact Mode — Minimal Fields

Light Dark
Compact Minimal Light Compact Minimal Dark

Settings — Complete Layout

Light Dark
Settings Complete Light Settings Complete Dark

Settings — Compact Layout

Light Dark
Settings Compact Light Settings Compact Dark

Node Detail Content

Light Dark
Detail Light Detail Dark

Technical Details

  • KMP-compatible implementation in core/ui and feature/node
  • Settings stored via DataStore preferences in core/data
  • Screenshot test coverage: 234 reference images across light/dark themes
  • M3 Expressive theming using lerp() for card tint and motionScheme for glow animation
  • animateContentSize() for smooth settings preview transitions
  • Divider-separated settings sections for clear visual hierarchy

@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
@codecov

codecov Bot commented May 18, 2026

Copy link
Copy Markdown

⚠️ 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 jamesarich force-pushed the feat/node-list branch 3 times, most recently from ca65619 to 7bbcc05 Compare May 19, 2026 00:56
@jamesarich

Copy link
Copy Markdown
Collaborator Author

Node List - Compact (Light)

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Node List - Compact (Dark)

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Node List - Complete (Light)

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Node List - Complete (Dark)

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Node Detail - Local (Light)

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Node Detail - Local (Dark)

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Node Detail - Remote (Light)

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Node Detail - Remote (Dark)

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Node List - Compact All Fields (Light)

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Node Details Section (Light)

@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

📄 Docs staleness check — advisory

This PR modifies user-facing UI source files but does not update any page under docs/en/user/ or docs/en/developer/.

⚠️ Doc changes propagate to 3 consumers: in-app docs browser, Jekyll site (GitHub Pages), and meshtastic.org (Docusaurus sync). Updating a page in docs/en/ automatically flows to all three.

Changed source files:

core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BuildNodeDescription.kt
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LastHeardInfo.kt
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MaterialBatteryInfo.kt
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeCardGlow.kt
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItem.kt
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItemCompact.kt
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeStatusIcons.kt
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TelemetryInfo.kt
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TransportIcon.kt
feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt
feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt
feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeListHelp.kt
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettings.kt

What to check:

Changed area Likely doc page
feature/messaging/ docs/en/user/messages-and-channels.md
feature/node/ docs/en/user/nodes.md or docs/en/user/node-metrics.md
feature/map/ docs/en/user/map-and-waypoints.md
feature/connections/ docs/en/user/connections.md
feature/settings/ docs/en/user/settings-radio-user.md or docs/en/user/settings-module-admin.md
feature/firmware/ docs/en/user/firmware.md
feature/intro/ docs/en/user/onboarding.md
feature/discovery/ docs/en/user/discovery.md
feature/docs/ Internal docs infrastructure
core/ui/ docs/en/developer/codebase.md or component-specific user pages

New page checklist (if adding a new doc page):

  1. Create the .md file in docs/en/user/ or docs/en/developer/ with last_updated frontmatter
  2. Register in DocBundleLoader.kt with string resources (in-app browser)
  3. Jekyll and Docusaurus sync pick up new pages automatically — no config change needed

If this PR does not require a doc update (e.g., internal refactor, bug fix, test change), add the skip-docs-check label to dismiss this check.

Cross-platform note: This check is advisory while doc coverage matures. Both Android and Apple repos use the same skip-docs-check label and advisory severity. See meshtastic/design standards for shared conventions.

@jamesarich jamesarich force-pushed the feat/node-list branch 2 times, most recently from 9d736a7 to 15502a6 Compare May 21, 2026 18:02
jamesarich and others added 9 commits May 21, 2026 14:47
…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>
jamesarich and others added 20 commits May 21, 2026 14:49
…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>
Replace the generic ic_memory chip icon in the node list footer with
the actual device SVG from flasher.meshtastic.org. The NodeListViewModel
resolves device images via DeviceHardwareRepository (DB-cached) and
passes the URL to NodeItemCompact and NodeItem.

- Add Coil dependency to core:ui for AsyncImage support
- Add deviceImageUrl parameter to NodeItemCompact, NodeItem, HardwareInfo
- NodeListViewModel resolves unique hw_model values → image URLs lazily
- Falls back to ic_memory vector icon when image unavailable

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merge the DeviceDetailsSection and NodeDetailsSection into one unified
card. The device hardware image is displayed as a centered "hero" at
the top with the model name and support badge, followed by a horizontal
divider, then all node identity info.

Key changes:
- NodeDetailsSection now accepts optional DeviceHardware and
  reportedTarget params to render the hero section
- DeviceHeroSection composable with avatar, name, and support badge
- DeviceHardwareImage extracted as internal for reuse
- Removed standalone DeviceDetailsSection from NodeDetailList
- Uses animateContentSize() for smooth expansion when hardware data
  loads asynchronously
- Added preview for the combined card with device hero

Shared element transitions researched but deferred: the API is
experimental (@ExperimentalSharedTransitionApi), has limited CMP
support on iOS/Desktop, and the adaptive ListDetailSceneStrategy
already handles the list→detail transition well.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch from centered vertical Column to a horizontal Row with 64dp
avatar on the left and device name + support badge on the right.
Saves ~50% vertical space in the hero section.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove explicit surface color override from node list cards. The M3
default cardColors() uses surfaceContainerLow which is the standard
filled card treatment for list items.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Update layout structure from 3-row to 4-row compact (health, metrics, footer)
- Document device hardware images from CDN (DeviceHardwareRepository)
- Fix glow animation values (0→1f bloom, not 0→0.6f)
- Update card colors to M3 default (surfaceContainerLow)
- Replace adaptive chip sizing with actual defaultMinSize behavior
- Update toggle DataStore keys to actual kebab-case values
- Add NFRs for image dedup and Coil caching
- Add FR-033 through FR-035 for CDN images, node status, transport icon
- Exclude node detail combined card changes (separate scope)

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

- Move showTelemetry toggle above mode-specific section (shared)
- Add 'Compact Fields' section header for compact-only toggles
- Rename 'Device Role' to 'Device & Role' for clarity
- Complete mode now shows shared toggle + description text
- Compact mode shows shared toggle + compact-specific toggles

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jamesarich and others added 4 commits May 21, 2026 15:54
…aram order

- Add missing @PreviewTest/@PreviewLightDark/@composable annotations lost
  during rebase conflict resolution in NodeScreenshotTests.kt
- Move modifier param before optional params in NodeListScreen (ComposableParamOrder)
- Remove unused onNavigateToChannels param from NodeListScreen (UnusedParameter)
- Remove corresponding call site in AdaptiveNodeListScreen

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Blend 5% of the node's identity color into the card container color
using lerp(). Transparent nodeColor (nodes with no assigned color)
keeps the default M3 surfaceContainerLow background unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Card tint: 0.05 → 0.08 (lerp fraction)
- Active border: 0.5 → 0.65 alpha
- Inactive border: 0.2 → 0.3 alpha

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Document shared Environment Metrics toggle behavior
- Update preview placement (below density picker)
- Correct DataStore density key to node-list-density
- Align card tint/border requirements with implemented values
- Update toggle labels and requirement wording for Device & Role

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jamesarich jamesarich marked this pull request as ready for review May 21, 2026 22:01
jamesarich and others added 2 commits May 21, 2026 17:11
Add HorizontalDivider separators between the preview section, shared
toggle, and compact-specific field toggles to reduce visual density
and improve section readability.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The formatDateTime path requires Android Application context via
ContextServices.app which is unavailable in the layoutlib screenshot
renderer. Switch to lastHeardIsRelative=true to use the context-free
DateUtils.getRelativeTimeSpanString path instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jamesarich jamesarich merged commit 5d9e71d into main May 21, 2026
18 checks passed
@jamesarich jamesarich deleted the feat/node-list branch May 21, 2026 22:44
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