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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user