The tab bar retires — boards are the app, settings behind the gear, recency the default order
The boards stack becomes the root screen: Settings moves from its own tab to a sheet behind a leading gearshape button, the sort pickers trade their text segments for symbols (hand.draw, textformat.abc, clock) with the words kept for accessibility, and every list now opens sorted by most recent change. UI tests re-aimed at the gear button and Done-button scoping fixed for the sheet-over-sheet nav bars — 4/4 green in the simulator. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The iPhone app's entry point. Two tabs, per the mobile MVP charter: Boards (the whole
|
||||
/// board → lane → card navigation stack) and Settings.
|
||||
/// The iPhone app's entry point. The boards navigation stack (board → lane → card) is the whole
|
||||
/// app; Settings lives behind the gear button on the board list rather than a tab of its own.
|
||||
@main
|
||||
struct MobileApp: App {
|
||||
|
||||
@@ -13,21 +13,8 @@ struct MobileApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
RootTabView()
|
||||
BoardsTabView()
|
||||
.environment(boardIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RootTabView: View {
|
||||
var body: some View {
|
||||
TabView {
|
||||
Tab("Boards", systemImage: "rectangle.stack") {
|
||||
BoardsTabView()
|
||||
}
|
||||
Tab("Settings", systemImage: "gearshape") {
|
||||
SettingsTabView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
**August 2026**
|
||||
|
||||
Settings now lives behind the gear button on the board list instead of a separate tab, so boards fill the whole screen.
|
||||
|
||||
Lists now open sorted by most recent change.
|
||||
|
||||
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.
|
||||
|
||||
@@ -16,15 +16,15 @@ struct BoardScreen: View {
|
||||
|
||||
/// Persisted as its raw value, not the enum itself — same reasoning as `BoardsTabView`'s
|
||||
/// `sortOrderRaw`.
|
||||
@AppStorage("laneListSortOrder") private var sortOrderRaw = ItemSortOrder.manual.rawValue
|
||||
@AppStorage("laneListSortOrder") private var sortOrderRaw = ItemSortOrder.recent.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
|
||||
/// The stored raw value as the enum, defaulting to `.recent` 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 },
|
||||
get: { ItemSortOrder(rawValue: sortOrderRaw) ?? .recent },
|
||||
set: { sortOrderRaw = $0.rawValue }
|
||||
)
|
||||
}
|
||||
@@ -83,9 +83,9 @@ struct BoardScreen: View {
|
||||
if case .ready = session.phase, let snapshot = session.snapshot, !snapshot.lanes.isEmpty {
|
||||
ToolbarItem(placement: .principal) {
|
||||
Picker("Sort", selection: sortOrder) {
|
||||
Text("Manual").tag(ItemSortOrder.manual)
|
||||
Text("Name").tag(ItemSortOrder.name)
|
||||
Text("Recent").tag(ItemSortOrder.recent)
|
||||
Label("Manual", systemImage: "hand.draw").tag(ItemSortOrder.manual)
|
||||
Label("Name", systemImage: "textformat.abc").tag(ItemSortOrder.name)
|
||||
Label("Recent", systemImage: "clock").tag(ItemSortOrder.recent)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(maxWidth: 260)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The root of the boards navigation stack: board list → lanes → cards → card detail
|
||||
/// (`BoardRoute`'s destinations).
|
||||
/// (`BoardRoute`'s destinations) — and, now that the tab bar is gone, the app's root screen.
|
||||
/// Settings is presented from the leading gear button rather than a tab of its own.
|
||||
///
|
||||
/// 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
|
||||
@@ -12,10 +13,11 @@ struct BoardsTabView: View {
|
||||
@Environment(BoardIndexStore.self) private var index
|
||||
|
||||
@State private var isPresentingNewBoard = false
|
||||
@State private var isPresentingSettings = false
|
||||
|
||||
/// Persisted as its raw value, not the enum itself — `AppStorage` needs a property-list type, and
|
||||
/// the raw `String` is exactly what `BoardSortOrder` promises to keep stable.
|
||||
@AppStorage("boardListSortOrder") private var sortOrderRaw = BoardSortOrder.name.rawValue
|
||||
@AppStorage("boardListSortOrder") private var sortOrderRaw = BoardSortOrder.recent.rawValue
|
||||
|
||||
/// The board whose settings sheet is up. A value, not a URL: the sheet renders the row's own
|
||||
/// summary, and a successful move dismisses it before the stale copy could matter.
|
||||
@@ -25,6 +27,15 @@ struct BoardsTabView: View {
|
||||
NavigationStack {
|
||||
content
|
||||
.navigationTitle("Boards")
|
||||
.toolbar {
|
||||
// Unconditional — settings (About, license, changelog) must stay reachable
|
||||
// even while the index is still loading or the board list is empty.
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button("Settings", systemImage: "gearshape") {
|
||||
isPresentingSettings = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
if case .ready = index.phase {
|
||||
Button("New Board", systemImage: "plus") {
|
||||
@@ -61,13 +72,16 @@ struct BoardsTabView: View {
|
||||
.sheet(item: $boardInSettings) { board in
|
||||
BoardSettingsSheet(board: board)
|
||||
}
|
||||
.sheet(isPresented: $isPresentingSettings) {
|
||||
SettingsScreen()
|
||||
}
|
||||
}
|
||||
|
||||
/// The stored raw value as the enum, defaulting to `.name` on anything the store didn't write
|
||||
/// The stored raw value as the enum, defaulting to `.recent` 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<BoardSortOrder> {
|
||||
Binding(
|
||||
get: { BoardSortOrder(rawValue: sortOrderRaw) ?? .name },
|
||||
get: { BoardSortOrder(rawValue: sortOrderRaw) ?? .recent },
|
||||
set: { sortOrderRaw = $0.rawValue }
|
||||
)
|
||||
}
|
||||
@@ -83,8 +97,8 @@ struct BoardsTabView: View {
|
||||
if case .ready = index.phase, !index.boards.isEmpty {
|
||||
ToolbarItem(placement: .principal) {
|
||||
Picker("Sort", selection: sortOrder) {
|
||||
Text("Name").tag(BoardSortOrder.name)
|
||||
Text("Recent").tag(BoardSortOrder.recent)
|
||||
Label("Name", systemImage: "textformat.abc").tag(BoardSortOrder.name)
|
||||
Label("Recent", systemImage: "clock").tag(BoardSortOrder.recent)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(maxWidth: 260)
|
||||
|
||||
@@ -20,15 +20,15 @@ struct LaneScreen: View {
|
||||
|
||||
/// Persisted as its raw value, not the enum itself — same reasoning as `BoardsTabView`'s
|
||||
/// `sortOrderRaw`.
|
||||
@AppStorage("cardListSortOrder") private var sortOrderRaw = ItemSortOrder.manual.rawValue
|
||||
@AppStorage("cardListSortOrder") private var sortOrderRaw = ItemSortOrder.recent.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
|
||||
/// The stored raw value as the enum, defaulting to `.recent` 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 },
|
||||
get: { ItemSortOrder(rawValue: sortOrderRaw) ?? .recent },
|
||||
set: { sortOrderRaw = $0.rawValue }
|
||||
)
|
||||
}
|
||||
@@ -98,9 +98,9 @@ struct LaneScreen: View {
|
||||
if let lane, !lane.cards.isEmpty {
|
||||
ToolbarItem(placement: .principal) {
|
||||
Picker("Sort", selection: sortOrder) {
|
||||
Text("Manual").tag(ItemSortOrder.manual)
|
||||
Text("Name").tag(ItemSortOrder.name)
|
||||
Text("Recent").tag(ItemSortOrder.recent)
|
||||
Label("Manual", systemImage: "hand.draw").tag(ItemSortOrder.manual)
|
||||
Label("Name", systemImage: "textformat.abc").tag(ItemSortOrder.name)
|
||||
Label("Recent", systemImage: "clock").tag(ItemSortOrder.recent)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(maxWidth: 260)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import SwiftUI
|
||||
import IndieAbout
|
||||
|
||||
/// The About section (indie-about skill).
|
||||
struct SettingsTabView: View {
|
||||
/// The About section (indie-about skill). Presented as a sheet from `BoardsTabView`'s gear
|
||||
/// button, so it carries its own navigation stack and a Done button to dismiss itself.
|
||||
struct SettingsScreen: View {
|
||||
@Environment(BoardIndexStore.self) private var index
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -19,9 +21,14 @@ struct SettingsTabView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Done") { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Idempotent (BoardIndexStore.start()), and harmless if BoardsTabView already called it —
|
||||
// this tab can be the first one the user opens.
|
||||
// defensive only, since the boards screen is always what starts the index now.
|
||||
.task { index.start() }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user