The phone's rows learn their faces and two more orders — icons everywhere, lanes and cards sortable

Boards, lanes, and cards all show their frontmatter icon leading the
row, tinted through the palette with a secondary fallback; the board
scanner now reads icon and iconColor from the same one-file parse as
the title. Lane and card lists gain the segmented Manual/Name/Recent
control — display-only sorting layered over the rank order, never
rewriting what's on disk.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-08-08 12:34:10 -04:00
parent 445d035a83
commit e8791b239d
7 changed files with 209 additions and 44 deletions
+39 -14
View File
@@ -5,7 +5,9 @@ import SwiftUI
/// Holds `boardRoot` and `laneID`, never a `Lane` value: the lane is re-read from
/// `session.snapshot` on every body evaluation, so a write this screen makes or one relayed
/// from elsewhere through the metadata query reaches the list the moment the session's reload
/// lands, and a lane deleted on another device is noticed rather than shown stale.
/// lands, and a lane deleted on another device is noticed rather than shown stale. A segmented
/// control above the card rows lets the list be sorted by name or by most recent change, on top
/// of the lane's own manual arrangement, remembered across launches.
struct LaneScreen: View {
let boardRoot: URL
let laneID: ItemID
@@ -16,8 +18,21 @@ struct LaneScreen: View {
@State private var isPresentingNewCard = false
@State private var cardPendingMove: ItemID?
/// Persisted as its raw value, not the enum itself same reasoning as `BoardsTabView`'s
/// `sortOrderRaw`.
@AppStorage("cardListSortOrder") private var sortOrderRaw = ItemSortOrder.manual.rawValue
private var session: BoardSession { index.session(forBoardAt: boardRoot) }
/// The stored raw value as the enum, defaulting to `.manual` on anything the store didn't
/// write itself an unset key or a stale raw value from a build that no longer has this case.
private var sortOrder: Binding<ItemSortOrder> {
Binding(
get: { ItemSortOrder(rawValue: sortOrderRaw) ?? .manual },
set: { sortOrderRaw = $0.rawValue }
)
}
private var lane: Lane? {
session.snapshot?.lanes.first { $0.id == laneID }
}
@@ -66,6 +81,13 @@ struct LaneScreen: View {
return title
}
/// The row's display title, "Untitled Card" fallback included fed to `ItemSortOrder.sorted`
/// so `.name` sorts on exactly what the row shows.
private func displayTitle(of card: Card) -> String {
guard let title = card.title.value, !title.isEmpty else { return "Untitled Card" }
return title
}
@ViewBuilder
private var content: some View {
if let lane {
@@ -84,9 +106,20 @@ struct LaneScreen: View {
description: Text("Add a card to this lane.")
)
} else {
// Already in display order (`Lane.cards`'s own contract) trusted rather than
// re-sorted here, exactly as the lane list trusts `BoardModel.lanes`.
ForEach(lane.cards) { card in
Picker("Sort", selection: sortOrder) {
Text("Manual").tag(ItemSortOrder.manual)
Text("Name").tag(ItemSortOrder.name)
Text("Recent").tag(ItemSortOrder.recent)
}
.pickerStyle(.segmented)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
// The trusted order is `.manual`'s base `Lane.cards` is already in display
// order, exactly as the lane list trusts `BoardModel.lanes` and `.name`/
// `.recent` layer a display-only re-sort on top, never touching that base or
// what's on disk.
ForEach(sortOrder.wrappedValue.sorted(lane.cards, title: displayTitle(of:), modified: { $0.modified.value })) { card in
NavigationLink(value: BoardRoute.card(boardRoot: boardRoot, laneID: laneID, cardID: card.id)) {
CardSummaryRow(card: card)
}
@@ -168,7 +201,7 @@ struct LaneScreen: View {
}
}
/// One card row: title, an attachment-count hint, and the card's own icon when it has one.
/// One card row: its icon when it has one, title, and an attachment-count hint.
///
/// **No label chips.** `labels` is not a field `BoardModel`/`FrontmatterFields` expose see
/// `CardAttributesSection`'s doc comment for why so `attachments` (a field the model already
@@ -178,6 +211,7 @@ private struct CardSummaryRow: View {
var body: some View {
HStack {
ItemIconView(icon: card.icon.value, iconColor: card.iconColor.value)
VStack(alignment: .leading, spacing: 2) {
Text(displayTitle)
if !card.attachments.isEmpty {
@@ -186,11 +220,6 @@ private struct CardSummaryRow: View {
.foregroundStyle(.secondary)
}
}
Spacer()
if let icon = card.icon.value, !icon.isEmpty {
Image(systemName: icon)
.foregroundStyle(iconTint)
}
}
}
@@ -198,8 +227,4 @@ private struct CardSummaryRow: View {
guard let title = card.title.value, !title.isEmpty else { return "Untitled Card" }
return title
}
private var iconTint: Color {
card.iconColor.value.flatMap { CardPalette.color(named: $0, in: CardPalette.foregrounds) } ?? .secondary
}
}