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:
@@ -80,6 +80,8 @@ struct MailThread: Identifiable, Hashable, Codable {
|
||||
var isUnread: Bool
|
||||
var isStarred: Bool
|
||||
var tags: [String]
|
||||
var lane: Lane
|
||||
var summary: [String]?
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
@@ -90,7 +92,9 @@ struct MailThread: Identifiable, Hashable, Codable {
|
||||
messages: [MailMessage],
|
||||
isUnread: Bool,
|
||||
isStarred: Bool,
|
||||
tags: [String] = []
|
||||
tags: [String] = [],
|
||||
lane: Lane = .people,
|
||||
summary: [String]? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.routeID = routeID
|
||||
@@ -101,6 +105,29 @@ struct MailThread: Identifiable, Hashable, Codable {
|
||||
self.isUnread = isUnread
|
||||
self.isStarred = isStarred
|
||||
self.tags = tags
|
||||
self.lane = lane
|
||||
self.summary = summary
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, routeID, mailbox, subject, participants, messages
|
||||
case isUnread, isStarred, tags, lane, summary
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.id = try c.decode(UUID.self, forKey: .id)
|
||||
self.routeID = try c.decode(String.self, forKey: .routeID)
|
||||
self.mailbox = try c.decode(Mailbox.self, forKey: .mailbox)
|
||||
self.subject = try c.decode(String.self, forKey: .subject)
|
||||
self.participants = try c.decode([MailPerson].self, forKey: .participants)
|
||||
let rawMessages = try c.decode([MailMessage].self, forKey: .messages)
|
||||
self.messages = rawMessages.sorted { $0.sentAt < $1.sentAt }
|
||||
self.isUnread = try c.decode(Bool.self, forKey: .isUnread)
|
||||
self.isStarred = try c.decode(Bool.self, forKey: .isStarred)
|
||||
self.tags = try c.decodeIfPresent([String].self, forKey: .tags) ?? []
|
||||
self.lane = try c.decodeIfPresent(Lane.self, forKey: .lane) ?? .people
|
||||
self.summary = try c.decodeIfPresent([String].self, forKey: .summary)
|
||||
}
|
||||
|
||||
var latestMessage: MailMessage? {
|
||||
|
||||
Reference in New Issue
Block a user