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:
@@ -0,0 +1,23 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The leading icon a list row renders when its item has one — the symbol name straight from
|
||||
/// frontmatter, tinted through the palette with `.secondary` standing in for any name the
|
||||
/// palette does not know. Decorative: the row's text is the accessible content.
|
||||
struct ItemIconView: View {
|
||||
let icon: String?
|
||||
let iconColor: String?
|
||||
|
||||
var body: some View {
|
||||
if let icon, !icon.isEmpty {
|
||||
Image(systemName: icon)
|
||||
.foregroundStyle(tint)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// An unrecognized colour name falls back to `.secondary` rather than guessing — the same
|
||||
/// leniency `CardPalette.color(named:in:)` documents for itself.
|
||||
private var tint: Color {
|
||||
iconColor.flatMap { CardPalette.color(named: $0, in: CardPalette.foregrounds) } ?? .secondary
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import Foundation
|
||||
|
||||
/// How the lane and card lists order their rows. Raw values are persisted (`AppStorage`), so
|
||||
/// they are API. `.manual` is the board's own arrangement — the rank order the loader already
|
||||
/// delivered — and sorting here is display-only: it never rewrites ranks on disk.
|
||||
enum ItemSortOrder: String, CaseIterable, Sendable {
|
||||
case manual
|
||||
case name
|
||||
case recent
|
||||
}
|
||||
|
||||
extension ItemSortOrder {
|
||||
/// Orders `items` for display without touching the manual arrangement they arrived in.
|
||||
///
|
||||
/// **`.manual` is a no-op** — `items` is already in the loader's rank order, so this returns it
|
||||
/// unchanged.
|
||||
///
|
||||
/// **`.name` sorts by `title`**, case/diacritic-insensitive (`localizedStandardCompare`). Callers
|
||||
/// pass the row's own display title — the "Untitled Lane"/"Untitled Card" fallback already
|
||||
/// applied — so there is no separate nil case to sort here.
|
||||
///
|
||||
/// **`.recent` sorts by `modified` descending, with `nil` last.** Both sorts tie-break the same
|
||||
/// way: Swift's `sort` is not a stable sort, so ties — equal titles, equal or absent dates — keep
|
||||
/// the incoming order via each item's original offset, exactly as `BoardSortOrder.sorted` does.
|
||||
nonisolated func sorted<Item>(
|
||||
_ items: [Item],
|
||||
title: (Item) -> String,
|
||||
modified: (Item) -> Date?
|
||||
) -> [Item] {
|
||||
switch self {
|
||||
case .manual:
|
||||
return items
|
||||
|
||||
case .name:
|
||||
return items.enumerated().sorted { lhs, rhs in
|
||||
let comparison = title(lhs.element).localizedStandardCompare(title(rhs.element))
|
||||
if comparison != .orderedSame { return comparison == .orderedAscending }
|
||||
return lhs.offset < rhs.offset
|
||||
}.map(\.element)
|
||||
|
||||
case .recent:
|
||||
return items.enumerated().sorted { lhs, rhs in
|
||||
switch (modified(lhs.element), modified(rhs.element)) {
|
||||
case let (l?, r?) where l != r:
|
||||
return l > r
|
||||
case (nil, .some):
|
||||
return false
|
||||
case (.some, nil):
|
||||
return true
|
||||
default:
|
||||
return lhs.offset < rhs.offset
|
||||
}
|
||||
}.map(\.element)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user