Align Mail UI with social.io design handoff

Rewrite the Mail feature to match the Apple-native look from the
handoff spec: lane-split inbox, AI summary card, clean ThreadRow,
Cc/From + format toolbar in Compose. Drop the gradient hero
surfaces and blurred canvas backgrounds the spec calls out as
anti-patterns, and introduce a token-backed design layer so the
lane palette and SIO tint live in the asset catalog.

- Add Assets.xcassets with SIOTint, LaneFeed, LanePaper, LanePeople
  (light + dark variants).
- Add Sources/Core/Design/SIODesign.swift: SIO tokens, Lane enum,
  LaneChip, AvatarView, AISummaryCard, KeyboardHint, button styles,
  and a glass-chrome helper with iOS 26 / material fallback.
- Extend MailThread with lane + summary; custom Codable keeps old
  payloads decodable. Seed mock threads with sensible lanes and
  hand-write summaries on launch-copy, investor-update, roadmap-notes.
- Add lane filtering to AppViewModel (selectedLane, selectLane,
  laneUnreadCount, laneThreadCount).
- Rewrite MailRootView end to end: sidebar with Inbox/lane rows,
  lane filter strip, Apple-native ThreadRow (avatar, unread dot,
  lane chip, summary chip), ThreadReadingView with AI summary +
  floating reply pill, ComposeView with To/Cc/From/Subject and a
  format toolbar.
- Wire Assets.xcassets + SIODesign.swift into project.pbxproj.

Accessibility identifiers preserved byte-identical; new ones
(mailbox.lane.*, lane.chip.*) added only where new.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 16:22:10 +02:00
parent 15af566353
commit 549aaa634c
12 changed files with 1129 additions and 874 deletions
+26
View File
@@ -5,6 +5,7 @@ import Observation
@Observable
final class AppViewModel {
var selectedMailbox: Mailbox = .inbox
var selectedLane: Lane?
var selectedThreadID: MailThread.ID?
var focusedMessageRouteID: String?
var searchText = ""
@@ -44,6 +45,10 @@ final class AppViewModel {
.filter { thread in
selectedMailbox == .starred ? thread.isStarred : thread.mailbox == selectedMailbox
}
.filter { thread in
guard let lane = selectedLane else { return true }
return thread.lane == lane
}
.filter { thread in
!showUnreadOnly || thread.isUnread
}
@@ -51,6 +56,27 @@ final class AppViewModel {
.sorted { $0.lastUpdated > $1.lastUpdated }
}
func laneUnreadCount(_ lane: Lane) -> Int {
threads.filter { thread in
let inCurrentMailbox = selectedMailbox == .starred
? thread.isStarred
: thread.mailbox == selectedMailbox
return inCurrentMailbox && thread.lane == lane && thread.isUnread
}.count
}
func laneThreadCount(_ lane: Lane) -> Int {
threads.filter { thread in
thread.mailbox == .inbox && thread.lane == lane
}.count
}
func selectLane(_ lane: Lane?) {
selectedLane = lane
clearThreadSelection()
mailboxNavigationToken = UUID()
}
var totalUnreadCount: Int {
threads.filter(\.isUnread).count
}
+1 -1
View File
@@ -7,7 +7,7 @@ struct SocialIOApp: App {
var body: some Scene {
WindowGroup {
MailRootView(model: model)
.tint(MailTheme.accent)
.tint(SIO.tint)
.onOpenURL { url in
model.apply(url: url)
}