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:
6
swift/Assets.xcassets/Contents.json
Normal file
6
swift/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
38
swift/Assets.xcassets/LaneFeed.colorset/Contents.json
Normal file
38
swift/Assets.xcassets/LaneFeed.colorset/Contents.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "0.420",
|
||||
"red" : "0.184"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "0.561",
|
||||
"red" : "0.369"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
38
swift/Assets.xcassets/LanePaper.colorset/Contents.json
Normal file
38
swift/Assets.xcassets/LanePaper.colorset/Contents.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.039",
|
||||
"green" : "0.624",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.251",
|
||||
"green" : "0.702",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
38
swift/Assets.xcassets/LanePeople.colorset/Contents.json
Normal file
38
swift/Assets.xcassets/LanePeople.colorset/Contents.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.345",
|
||||
"green" : "0.820",
|
||||
"red" : "0.188"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.447",
|
||||
"green" : "0.847",
|
||||
"red" : "0.392"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
38
swift/Assets.xcassets/SIOTint.colorset/Contents.json
Normal file
38
swift/Assets.xcassets/SIOTint.colorset/Contents.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "0.420",
|
||||
"red" : "0.184"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "0.561",
|
||||
"red" : "0.369"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
A10000000000000000000007 /* AppControlService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000008 /* AppControlService.swift */; };
|
||||
A10000000000000000000008 /* AppNavigationCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000009 /* AppNavigationCommandTests.swift */; };
|
||||
A10000000000000000000009 /* AppViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000000000000000000000A /* AppViewModelTests.swift */; };
|
||||
A10000000000000000000010 /* SIODesign.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000020 /* SIODesign.swift */; };
|
||||
A10000000000000000000011 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000021 /* Assets.xcassets */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -40,6 +42,8 @@
|
||||
A20000000000000000000009 /* AppNavigationCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNavigationCommandTests.swift; sourceTree = "<group>"; };
|
||||
A2000000000000000000000A /* AppViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewModelTests.swift; sourceTree = "<group>"; };
|
||||
A2000000000000000000000B /* SocialIOTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SocialIOTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A20000000000000000000020 /* SIODesign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SIODesign.swift; sourceTree = "<group>"; };
|
||||
A20000000000000000000021 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -73,6 +77,7 @@
|
||||
children = (
|
||||
A40000000000000000000003 /* Sources */,
|
||||
A4000000000000000000000B /* Tests */,
|
||||
A20000000000000000000021 /* Assets.xcassets */,
|
||||
);
|
||||
name = SocialIO;
|
||||
sourceTree = "<group>";
|
||||
@@ -103,10 +108,19 @@
|
||||
children = (
|
||||
A40000000000000000000006 /* Models */,
|
||||
A40000000000000000000007 /* Services */,
|
||||
A40000000000000000000020 /* Design */,
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A40000000000000000000020 /* Design */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A20000000000000000000020 /* SIODesign.swift */,
|
||||
);
|
||||
path = Design;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A40000000000000000000006 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -238,6 +252,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A10000000000000000000011 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -262,6 +277,7 @@
|
||||
A10000000000000000000001 /* SocialIOApp.swift in Sources */,
|
||||
A10000000000000000000006 /* AppNavigationCommand.swift in Sources */,
|
||||
A10000000000000000000007 /* AppControlService.swift in Sources */,
|
||||
A10000000000000000000010 /* SIODesign.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
195
swift/Sources/Core/Design/SIODesign.swift
Normal file
195
swift/Sources/Core/Design/SIODesign.swift
Normal file
@@ -0,0 +1,195 @@
|
||||
import SwiftUI
|
||||
|
||||
enum SIO {
|
||||
static let tint = Color("SIOTint")
|
||||
static let laneFeed = Color("LaneFeed")
|
||||
static let lanePaper = Color("LanePaper")
|
||||
static let lanePeople = Color("LanePeople")
|
||||
|
||||
static let cardRadius: CGFloat = 14
|
||||
static let controlRadius: CGFloat = 10
|
||||
static let chipRadius: CGFloat = 6
|
||||
|
||||
static let bodyFontSize: CGFloat = 15.5
|
||||
static let bodyLineSpacing: CGFloat = 15.5 * 0.55
|
||||
}
|
||||
|
||||
enum Lane: String, CaseIterable, Codable, Hashable {
|
||||
case feed
|
||||
case paper
|
||||
case people
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .feed: "Feed"
|
||||
case .paper: "Paper"
|
||||
case .people: "People"
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .feed: SIO.laneFeed
|
||||
case .paper: SIO.lanePaper
|
||||
case .people: SIO.lanePeople
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
@ViewBuilder
|
||||
func sioGlassChrome<S: Shape>(in shape: S, tint: Color? = nil, interactive: Bool = false) -> some View {
|
||||
if #available(iOS 26.0, macOS 26.0, *) {
|
||||
self.glassEffect(
|
||||
Glass.regular.tint(tint).interactive(interactive),
|
||||
in: shape
|
||||
)
|
||||
} else {
|
||||
self
|
||||
.background(.ultraThinMaterial, in: shape)
|
||||
.overlay(shape.stroke(Color.primary.opacity(0.08), lineWidth: 0.5))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func sioGlassChromeContainer(spacing: CGFloat? = nil) -> some View {
|
||||
if #available(iOS 26.0, macOS 26.0, *) {
|
||||
GlassEffectContainer(spacing: spacing) { self }
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LaneChip: View {
|
||||
let lane: Lane
|
||||
var compact: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Text(lane.label)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.tracking(0.2)
|
||||
.foregroundStyle(lane.color)
|
||||
.padding(.horizontal, compact ? 6 : 8)
|
||||
.padding(.vertical, compact ? 2 : 3)
|
||||
.background(
|
||||
lane.color.opacity(0.14),
|
||||
in: RoundedRectangle(cornerRadius: SIO.chipRadius, style: .continuous)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct AvatarView: View {
|
||||
let name: String
|
||||
let size: CGFloat
|
||||
var tint: Color = SIO.tint
|
||||
|
||||
var initials: String {
|
||||
let parts = name.split(separator: " ").prefix(2)
|
||||
return parts.compactMap { $0.first.map(String.init) }.joined().uppercased()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text(initials)
|
||||
.font(.system(size: size * 0.42, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: size, height: size)
|
||||
.background(tint, in: Circle())
|
||||
}
|
||||
}
|
||||
|
||||
struct AISummaryCard: View {
|
||||
let messageCount: Int
|
||||
let bullets: [String]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "sparkles")
|
||||
.font(.caption2.weight(.bold))
|
||||
Text("SUMMARY · \(messageCount) messages")
|
||||
.font(.caption2.weight(.bold))
|
||||
.tracking(0.6)
|
||||
}
|
||||
.foregroundStyle(SIO.tint)
|
||||
|
||||
ForEach(Array(bullets.enumerated()), id: \.offset) { _, line in
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text("·").foregroundStyle(SIO.tint)
|
||||
Text(line)
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(14)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(SIO.tint.opacity(0.10), in: RoundedRectangle(cornerRadius: 16, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.stroke(SIO.tint.opacity(0.22), lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyboardHint: View {
|
||||
let label: String
|
||||
|
||||
var body: some View {
|
||||
Text(label)
|
||||
.font(.system(size: 11, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.vertical, 1)
|
||||
.background(.secondary.opacity(0.10), in: RoundedRectangle(cornerRadius: 4, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
||||
.stroke(.secondary.opacity(0.20), lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct PrimaryActionStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.headline)
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
.background(SIO.tint, in: RoundedRectangle(cornerRadius: SIO.controlRadius, style: .continuous))
|
||||
.opacity(configuration.isPressed ? 0.85 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
struct SecondaryActionStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 9)
|
||||
.background(.secondary.opacity(0.12), in: RoundedRectangle(cornerRadius: SIO.controlRadius, style: .continuous))
|
||||
.opacity(configuration.isPressed ? 0.85 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
struct DestructiveActionStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.red)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 9)
|
||||
.background(Color.red.opacity(0.12), in: RoundedRectangle(cornerRadius: SIO.controlRadius, style: .continuous))
|
||||
.opacity(configuration.isPressed ? 0.85 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
extension MailPerson {
|
||||
var avatarTint: Color {
|
||||
let hash = email.unicodeScalars.reduce(0) { $0 &+ Int($1.value) }
|
||||
let palette: [Color] = [SIO.laneFeed, SIO.lanePaper, SIO.lanePeople, SIO.tint, .purple, .pink, .teal]
|
||||
return palette[abs(hash) % palette.count]
|
||||
}
|
||||
}
|
||||
@@ -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? {
|
||||
|
||||
@@ -82,7 +82,13 @@ struct MockMailService: MailServicing {
|
||||
],
|
||||
isUnread: true,
|
||||
isStarred: true,
|
||||
tags: ["Design", "Launch"]
|
||||
tags: ["Design", "Launch"],
|
||||
lane: .people,
|
||||
summary: [
|
||||
"Tanya tightened the onboarding copy and softened the empty-state tone.",
|
||||
"Phil signed off with a note to keep playful language on iPhone.",
|
||||
"Desktop first-run copy still needs a lighter pass before handoff."
|
||||
]
|
||||
),
|
||||
MailThread(
|
||||
routeID: "daily-sync-status",
|
||||
@@ -100,7 +106,8 @@ struct MockMailService: MailServicing {
|
||||
],
|
||||
isUnread: false,
|
||||
isStarred: false,
|
||||
tags: ["System"]
|
||||
tags: ["System"],
|
||||
lane: .feed
|
||||
),
|
||||
MailThread(
|
||||
routeID: "investor-update",
|
||||
@@ -118,7 +125,13 @@ struct MockMailService: MailServicing {
|
||||
],
|
||||
isUnread: true,
|
||||
isStarred: false,
|
||||
tags: ["External"]
|
||||
tags: ["External"],
|
||||
lane: .paper,
|
||||
summary: [
|
||||
"Mina wants a concise product update before Friday.",
|
||||
"She's specifically asking for screenshots of the new mail experience.",
|
||||
"Interested in how we differentiate from commodity inboxes."
|
||||
]
|
||||
),
|
||||
MailThread(
|
||||
routeID: "search-ranking-polish",
|
||||
@@ -143,7 +156,8 @@ struct MockMailService: MailServicing {
|
||||
],
|
||||
isUnread: false,
|
||||
isStarred: false,
|
||||
tags: ["Search"]
|
||||
tags: ["Search"],
|
||||
lane: .people
|
||||
),
|
||||
MailThread(
|
||||
routeID: "welcome-to-socialio",
|
||||
@@ -162,7 +176,8 @@ struct MockMailService: MailServicing {
|
||||
],
|
||||
isUnread: false,
|
||||
isStarred: false,
|
||||
tags: ["Draft"]
|
||||
tags: ["Draft"],
|
||||
lane: .people
|
||||
),
|
||||
MailThread(
|
||||
routeID: "roadmap-notes",
|
||||
@@ -180,7 +195,13 @@ struct MockMailService: MailServicing {
|
||||
],
|
||||
isUnread: false,
|
||||
isStarred: true,
|
||||
tags: ["Product"]
|
||||
tags: ["Product"],
|
||||
lane: .paper,
|
||||
summary: [
|
||||
"Three roadmap themes: faster triage, identity-rich threads, calmer notifications.",
|
||||
"Raw notes were cleaned up and archived by Nora.",
|
||||
"Owner split still needs to be reconciled with eng capacity."
|
||||
]
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user