M4+: mirror the desktop design language onto mobile (Home/Projects/To-Dos/Sessions)
Design system: Palette (teal accent + the Mac's categorical status colors, status→color refined by turn disposition), StatusStyle glyphs/labels, a card surface modifier, app-wide teal tint. Fixes the reported bug: Sessions now hides archived chats and uses disposition to split NEEDS YOU from DONE (a finished-the-work turn no longer shows as needing you). Rows gain status dots, favorite star, approval-count badge, diffstat; swipe to favorite/archive/delete. New surfaces, full control-scope parity with the Mac: - Home: greeting + day-streak flame, GitHub-style activity grid, stat cards, quick to-dos, and a start-chat composer (project picker + Auto). - Projects: per-project session counts → project detail with a scoped composer. - To-dos: capture, group by project, complete/dispatch/delete. - Session detail: a control menu (rename, favorite, interrupt, integrate merge/squash/rebase, archive, delete). RemoteStore handles the dashboard event + exposes all control intents; claims control scope. A NUCLEIC_DEMO/NUCLEIC_TAB env seam seeds mock state for offline UI preview. iOS app builds for the iOS 27 simulator; Home + Sessions verified rendering via screenshots. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import SwiftUI
|
||||
import NucleicProtocol
|
||||
|
||||
/// Start a new chat in a project, straight from Home (control scope — mirrors the Mac's home
|
||||
/// composer). Picks a project, types a prompt, optionally toggles Auto.
|
||||
struct StartChatComposer: View {
|
||||
@EnvironmentObject var store: RemoteStore
|
||||
@State private var projectID: ProjectID?
|
||||
@State private var draft = ""
|
||||
@State private var auto = false
|
||||
|
||||
private var projects: [WireProject] { store.dashboard.projects }
|
||||
private var selected: WireProject? {
|
||||
projects.first { $0.id == projectID } ?? projects.first
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("New chat").font(.headline)
|
||||
HStack {
|
||||
Menu {
|
||||
ForEach(projects) { project in
|
||||
Button(project.name) { projectID = project.id }
|
||||
}
|
||||
} label: {
|
||||
Label(selected?.name ?? "Select a project", systemImage: "folder")
|
||||
.font(.subheadline)
|
||||
}
|
||||
Spacer()
|
||||
Toggle(isOn: $auto) {
|
||||
Label("Auto", systemImage: auto ? "bolt.fill" : "bolt.slash")
|
||||
}
|
||||
.toggleStyle(.button)
|
||||
.tint(Palette.accent)
|
||||
.font(.caption)
|
||||
}
|
||||
HStack(alignment: .bottom, spacing: 8) {
|
||||
TextField("Describe a task…", text: $draft, axis: .vertical)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.lineLimit(1...5)
|
||||
Button {
|
||||
if let project = selected {
|
||||
store.startChat(in: project.id, message: draft, auto: auto)
|
||||
draft = ""
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.up.circle.fill").font(.title)
|
||||
}
|
||||
.disabled(selected == nil || draft.trimmingCharacters(in: .whitespaces).isEmpty || !store.canControl)
|
||||
}
|
||||
if !store.canControl {
|
||||
Text("This device is view-only.").font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.card()
|
||||
}
|
||||
}
|
||||
|
||||
/// A compact peek at open ideas with inline capture; the full inbox is the To-Dos tab.
|
||||
struct QuickTodos: View {
|
||||
@EnvironmentObject var store: RemoteStore
|
||||
@State private var draft = ""
|
||||
|
||||
private var open: [WireTodo] { store.dashboard.todos.filter { $0.status == .open } }
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Text("To-dos").font(.headline)
|
||||
Spacer()
|
||||
if !open.isEmpty { Text("\(open.count)").foregroundStyle(.secondary) }
|
||||
}
|
||||
HStack {
|
||||
TextField("Capture an idea…", text: $draft)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.onSubmit(capture)
|
||||
Button(action: capture) { Image(systemName: "plus.circle.fill").font(.title3) }
|
||||
.disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty || !store.canControl)
|
||||
}
|
||||
ForEach(open.prefix(3)) { todo in
|
||||
HStack(spacing: 10) {
|
||||
Button { store.completeTodo(todo.id) } label: {
|
||||
Image(systemName: "circle").foregroundStyle(.secondary)
|
||||
}.buttonStyle(.plain)
|
||||
Text(todo.summary?.isEmpty == false ? todo.summary! : todo.text)
|
||||
.font(.subheadline).lineLimit(1)
|
||||
Spacer()
|
||||
if let name = todo.projectName {
|
||||
Text(name).font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.card()
|
||||
}
|
||||
|
||||
private func capture() {
|
||||
store.captureTodo(draft, projectID: nil)
|
||||
draft = ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user