The sort control leaves the list — three pickers settle into the bottom bar
Same segments, same persistence; the picker now rides each screen's toolbar as a bottom-bar item shown only when there are rows to reorder, instead of masquerading as the first row of the list it sorts. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -4,7 +4,7 @@ Sort lanes and cards by your own arrangement, name, or most recent change.
|
|||||||
|
|
||||||
Boards, lanes, and cards now show their icon and color in the lists.
|
Boards, lanes, and cards now show their icon and color in the lists.
|
||||||
|
|
||||||
Sort your board list by name or by most recently changed with the new control above the list.
|
Sort your board list by name or by most recently changed with the sort control in the toolbar.
|
||||||
|
|
||||||
Version 1.0: Lanework comes to iPhone — browse your boards from iCloud Drive, move cards between lanes, and edit them on the go.
|
Version 1.0: Lanework comes to iPhone — browse your boards from iCloud Drive, move cards between lanes, and edit them on the go.
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import SwiftUI
|
|||||||
/// Holds only `boardRoot`, never a `BoardSummary`/`BoardModel` value: the session and its
|
/// Holds only `boardRoot`, never a `BoardSummary`/`BoardModel` value: the session and its
|
||||||
/// snapshot are pulled from the environment's index store fresh on every body evaluation, so a
|
/// snapshot are pulled from the environment's index store fresh on every body evaluation, so a
|
||||||
/// write from anywhere in the stack (a lane created, a card moved) reaches this screen the moment
|
/// write from anywhere in the stack (a lane created, a card moved) reaches this screen the moment
|
||||||
/// the session's reload lands. A segmented control above the lane rows lets the list be sorted by
|
/// the session's reload lands. A segmented control in the toolbar lets the list be sorted by
|
||||||
/// name or by most recent change, on top of the board's own manual arrangement, remembered across
|
/// name or by most recent change, on top of the board's own manual arrangement, remembered across
|
||||||
/// launches.
|
/// launches.
|
||||||
struct BoardScreen: View {
|
struct BoardScreen: View {
|
||||||
@@ -39,6 +39,9 @@ struct BoardScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.toolbar {
|
||||||
|
sortToolbarItem
|
||||||
|
}
|
||||||
.task { session.open() }
|
.task { session.open() }
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
// Stops the materializing/downloading retry timer. In practice a no-op here: a
|
// Stops the materializing/downloading retry timer. In practice a no-op here: a
|
||||||
@@ -73,6 +76,22 @@ struct BoardScreen: View {
|
|||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shown only once there is something to sort — an empty or not-yet-loaded lane list has no
|
||||||
|
/// rows for the segments to reorder.
|
||||||
|
@ToolbarContentBuilder
|
||||||
|
private var sortToolbarItem: some ToolbarContent {
|
||||||
|
if case .ready = session.phase, let snapshot = session.snapshot, !snapshot.lanes.isEmpty {
|
||||||
|
ToolbarItem(placement: .bottomBar) {
|
||||||
|
Picker("Sort", selection: sortOrder) {
|
||||||
|
Text("Manual").tag(ItemSortOrder.manual)
|
||||||
|
Text("Name").tag(ItemSortOrder.name)
|
||||||
|
Text("Recent").tag(ItemSortOrder.recent)
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var content: some View {
|
private var content: some View {
|
||||||
switch session.phase {
|
switch session.phase {
|
||||||
@@ -123,15 +142,6 @@ struct BoardScreen: View {
|
|||||||
description: Text("Add a lane to start organizing cards.")
|
description: Text("Add a lane to start organizing cards.")
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
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 — `BoardModel.lanes` is already in display
|
// The trusted order is `.manual`'s base — `BoardModel.lanes` is already in display
|
||||||
// order (the loader's `Ranks.sortedForDisplay`) — and `.name`/`.recent` layer a
|
// order (the loader's `Ranks.sortedForDisplay`) — and `.name`/`.recent` layer a
|
||||||
// display-only re-sort on top, never touching that base or what's on disk.
|
// display-only re-sort on top, never touching that base or what's on disk.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import SwiftUI
|
|||||||
/// Renders both of `BoardIndexStore.Phase`'s live states and, above the list, the iCloud notice —
|
/// Renders both of `BoardIndexStore.Phase`'s live states and, above the list, the iCloud notice —
|
||||||
/// which is a row, not a wall (softened 2026-08-08). A phone with no account still has a device home
|
/// which is a row, not a wall (softened 2026-08-08). A phone with no account still has a device home
|
||||||
/// and therefore still has boards, so the missing half of the app is reported next to the half that
|
/// and therefore still has boards, so the missing half of the app is reported next to the half that
|
||||||
/// works rather than in place of it. A segmented control above the rows lets the list be sorted by
|
/// works rather than in place of it. A segmented control in the toolbar lets the list be sorted by
|
||||||
/// name or by most recent change, remembered across launches.
|
/// name or by most recent change, remembered across launches.
|
||||||
struct BoardsTabView: View {
|
struct BoardsTabView: View {
|
||||||
@Environment(BoardIndexStore.self) private var index
|
@Environment(BoardIndexStore.self) private var index
|
||||||
@@ -32,6 +32,9 @@ struct BoardsTabView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.toolbar {
|
||||||
|
sortToolbarItem
|
||||||
|
}
|
||||||
.navigationDestination(for: BoardRoute.self) { route in
|
.navigationDestination(for: BoardRoute.self) { route in
|
||||||
switch route {
|
switch route {
|
||||||
case let .lanes(boardRoot):
|
case let .lanes(boardRoot):
|
||||||
@@ -73,6 +76,21 @@ struct BoardsTabView: View {
|
|||||||
sortOrder.wrappedValue.sorted(index.boards)
|
sortOrder.wrappedValue.sorted(index.boards)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shown only once there is something to sort — an empty or not-yet-loaded list has no rows
|
||||||
|
/// for the segments to reorder.
|
||||||
|
@ToolbarContentBuilder
|
||||||
|
private var sortToolbarItem: some ToolbarContent {
|
||||||
|
if case .ready = index.phase, !index.boards.isEmpty {
|
||||||
|
ToolbarItem(placement: .bottomBar) {
|
||||||
|
Picker("Sort", selection: sortOrder) {
|
||||||
|
Text("Name").tag(BoardSortOrder.name)
|
||||||
|
Text("Recent").tag(BoardSortOrder.recent)
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var content: some View {
|
private var content: some View {
|
||||||
switch index.phase {
|
switch index.phase {
|
||||||
@@ -93,14 +111,6 @@ struct BoardsTabView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Picker("Sort", selection: sortOrder) {
|
|
||||||
Text("Name").tag(BoardSortOrder.name)
|
|
||||||
Text("Recent").tag(BoardSortOrder.recent)
|
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
.listRowBackground(Color.clear)
|
|
||||||
.listRowSeparator(.hidden)
|
|
||||||
|
|
||||||
ForEach(sortedBoards) { board in
|
ForEach(sortedBoards) { board in
|
||||||
NavigationLink(value: BoardRoute.lanes(boardRoot: board.rootURL)) {
|
NavigationLink(value: BoardRoute.lanes(boardRoot: board.rootURL)) {
|
||||||
BoardSummaryRow(board: board)
|
BoardSummaryRow(board: board)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import SwiftUI
|
|||||||
/// `session.snapshot` on every body evaluation, so a write this screen makes — or one relayed
|
/// `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
|
/// 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. A segmented
|
/// 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
|
/// control in the toolbar lets the list be sorted by name or by most recent change, on top
|
||||||
/// of the lane's own manual arrangement, remembered across launches.
|
/// of the lane's own manual arrangement, remembered across launches.
|
||||||
struct LaneScreen: View {
|
struct LaneScreen: View {
|
||||||
let boardRoot: URL
|
let boardRoot: URL
|
||||||
@@ -47,6 +47,9 @@ struct LaneScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.toolbar {
|
||||||
|
sortToolbarItem
|
||||||
|
}
|
||||||
.task { session.open() }
|
.task { session.open() }
|
||||||
.titlePromptAlert("New Card", isPresented: $isPresentingNewCard, placeholder: "Card Title") { title in
|
.titlePromptAlert("New Card", isPresented: $isPresentingNewCard, placeholder: "Card Title") { title in
|
||||||
createCard(titled: title.isEmpty ? nil : title)
|
createCard(titled: title.isEmpty ? nil : title)
|
||||||
@@ -88,6 +91,22 @@ struct LaneScreen: View {
|
|||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shown only once there is something to sort — an empty or not-yet-loaded card list has no
|
||||||
|
/// rows for the segments to reorder.
|
||||||
|
@ToolbarContentBuilder
|
||||||
|
private var sortToolbarItem: some ToolbarContent {
|
||||||
|
if let lane, !lane.cards.isEmpty {
|
||||||
|
ToolbarItem(placement: .bottomBar) {
|
||||||
|
Picker("Sort", selection: sortOrder) {
|
||||||
|
Text("Manual").tag(ItemSortOrder.manual)
|
||||||
|
Text("Name").tag(ItemSortOrder.name)
|
||||||
|
Text("Recent").tag(ItemSortOrder.recent)
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var content: some View {
|
private var content: some View {
|
||||||
if let lane {
|
if let lane {
|
||||||
@@ -106,15 +125,6 @@ struct LaneScreen: View {
|
|||||||
description: Text("Add a card to this lane.")
|
description: Text("Add a card to this lane.")
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
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
|
// The trusted order is `.manual`'s base — `Lane.cards` is already in display
|
||||||
// order, exactly as the lane list trusts `BoardModel.lanes` — and `.name`/
|
// 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
|
// `.recent` layer a display-only re-sort on top, never touching that base or
|
||||||
|
|||||||
Reference in New Issue
Block a user