WIP: local handoff implementation

Local work on the social.io handoff before merging the claude
worktree branch. Includes the full per-spec Sources/Core/Design
module (8 files), watchOS target under WatchApp/, Live Activity +
widget extension, entitlements, scheme, and asset catalog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 16:26:38 +02:00
parent 15af566353
commit 2fe6b8a6df
32 changed files with 3861 additions and 926 deletions
@@ -0,0 +1,37 @@
import SwiftUI
struct AISummaryCard: View {
let count: Int
let bullets: [String]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 7) {
Image(systemName: "sparkles")
Text("SUMMARY")
Text(".")
Text("\(count) \(messageLabel.uppercased())")
}
.font(.caption.weight(.semibold))
.foregroundStyle(SIO.tint)
ForEach(Array(bullets.enumerated()), id: \.offset) { _, bullet in
HStack(alignment: .top, spacing: 8) {
Circle()
.fill(SIO.tint)
.frame(width: 6, height: 6)
.padding(.top, 6)
Text(bullet)
.font(.subheadline)
.fixedSize(horizontal: false, vertical: true)
}
}
}
.padding(16)
.sioCardBackground(tint: SIO.tint)
}
private var messageLabel: String {
count == 1 ? "message" : "messages"
}
}
@@ -0,0 +1,21 @@
import SwiftUI
struct AvatarView: View {
let name: String
var color: Color = SIO.tint
var size: CGFloat = 30
var body: some View {
Text(initials)
.font(.system(size: size * 0.42, weight: .semibold, design: .rounded))
.foregroundStyle(.white)
.frame(width: size, height: size)
.background(color, in: Circle())
.overlay(Circle().strokeBorder(.white.opacity(0.16), lineWidth: 1))
}
private var initials: String {
let parts = name.split(separator: " ")
return String(parts.prefix(2).compactMap { $0.first }).uppercased()
}
}
@@ -0,0 +1,57 @@
import SwiftUI
public struct PrimaryActionStyle: ButtonStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.headline.weight(.semibold))
.foregroundStyle(.white)
.padding(.horizontal, 16)
.padding(.vertical, 11)
.frame(minHeight: 44)
.background(
RoundedRectangle(cornerRadius: SIO.controlRadius, style: .continuous)
.fill(SIO.tint.opacity(configuration.isPressed ? 0.82 : 1))
)
.opacity(configuration.isPressed ? 0.94 : 1)
}
}
public struct SecondaryActionStyle: ButtonStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.headline.weight(.semibold))
.foregroundStyle(.primary)
.padding(.horizontal, 16)
.padding(.vertical, 11)
.frame(minHeight: 44)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: SIO.controlRadius, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: SIO.controlRadius, style: .continuous)
.strokeBorder(Color.primary.opacity(0.08), lineWidth: 1)
)
.opacity(configuration.isPressed ? 0.9 : 1)
}
}
public struct DestructiveStyle: ButtonStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.headline.weight(.semibold))
.foregroundStyle(.red)
.padding(.horizontal, 16)
.padding(.vertical, 11)
.frame(minHeight: 44)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: SIO.controlRadius, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: SIO.controlRadius, style: .continuous)
.strokeBorder(Color.red.opacity(configuration.isPressed ? 0.35 : 0.18), lineWidth: 1)
)
.opacity(configuration.isPressed ? 0.9 : 1)
}
}
@@ -0,0 +1,23 @@
import SwiftUI
public extension View {
@ViewBuilder
func sioGlassChrome() -> some View {
if #available(iOS 26.0, macOS 26.0, *) {
self.glassEffect(.regular)
} else {
self.background(.regularMaterial)
}
}
@ViewBuilder
func sioGlassSurface<S: Shape>(in shape: S, tint: Color? = nil) -> some View {
if #available(iOS 26.0, macOS 26.0, *) {
self.glassEffect(Glass.regular.tint(tint), in: shape)
} else {
self.background(.regularMaterial, in: shape)
.overlay(shape.stroke(Color.primary.opacity(0.08), lineWidth: 1))
.overlay(shape.fill((tint ?? .clear).opacity(0.08)))
}
}
}
+25
View File
@@ -0,0 +1,25 @@
import SwiftUI
#if os(iOS)
import UIKit
#endif
enum Haptics {
static func selection() {
#if os(iOS)
UISelectionFeedbackGenerator().selectionChanged()
#endif
}
static func success() {
#if os(iOS)
UINotificationFeedbackGenerator().notificationOccurred(.success)
#endif
}
static func warning() {
#if os(iOS)
UINotificationFeedbackGenerator().notificationOccurred(.warning)
#endif
}
}
@@ -0,0 +1,19 @@
import SwiftUI
struct KeyboardHint: View {
let title: String
var body: some View {
Text(title)
.font(.caption2.weight(.semibold))
.monospaced()
.foregroundStyle(.secondary)
.padding(.horizontal, 6)
.padding(.vertical, 4)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 6, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.strokeBorder(Color.primary.opacity(0.08), lineWidth: 1)
)
}
}
+24
View File
@@ -0,0 +1,24 @@
import SwiftUI
struct LaneChip: View {
let lane: Lane
var count: Int?
var body: some View {
HStack(spacing: 5) {
Circle()
.fill(lane.color)
.frame(width: 7, height: 7)
Text(lane.label.uppercased())
if let count {
Text(count, format: .number)
.foregroundStyle(lane.color.opacity(0.8))
}
}
.font(.caption2.weight(.semibold))
.padding(.horizontal, 9)
.padding(.vertical, 5)
.background(lane.color.opacity(0.14), in: RoundedRectangle(cornerRadius: SIO.chipRadius, style: .continuous))
.foregroundStyle(lane.color)
}
}
+136
View File
@@ -0,0 +1,136 @@
import SwiftUI
public enum SIO {
public static let tint = Color("SIOTint")
public static let laneFeed = Color("LaneFeed")
public static let lanePaper = Color("LanePaper")
public static let lanePeople = Color("LanePeople")
public static let cardRadius: CGFloat = 14
public static let controlRadius: CGFloat = 10
public static let chipRadius: CGFloat = 6
}
public enum Lane: String, CaseIterable, Codable, Identifiable {
case feed
case paper
case people
public var id: String { rawValue }
public var label: String {
switch self {
case .feed: "Feed"
case .paper: "Paper"
case .people: "People"
}
}
public var color: Color {
switch self {
case .feed: SIO.laneFeed
case .paper: SIO.lanePaper
case .people: SIO.lanePeople
}
}
}
public enum ThreadRowDensity: String, CaseIterable, Identifiable {
case compact
case cozy
case comfortable
public var id: String { rawValue }
public var avatarSize: CGFloat {
switch self {
case .compact: 24
case .cozy: 30
case .comfortable: 30
}
}
public var previewLineLimit: Int {
switch self {
case .compact, .cozy: 1
case .comfortable: 2
}
}
public var rowPadding: CGFloat {
switch self {
case .compact: 10
case .cozy: 12
case .comfortable: 14
}
}
public var showsMetaChips: Bool {
self == .comfortable
}
}
public enum ThemePreference: String, CaseIterable, Identifiable {
case system
case light
case dark
public var id: String { rawValue }
public var label: String {
switch self {
case .system: "System"
case .light: "Light"
case .dark: "Dark"
}
}
public var colorScheme: ColorScheme? {
switch self {
case .system: nil
case .light: .light
case .dark: .dark
}
}
}
public enum ReadingPanePreference: String, CaseIterable, Identifiable {
case right
case bottom
case off
public var id: String { rawValue }
public var label: String {
switch self {
case .right: "Right"
case .bottom: "Bottom"
case .off: "Off"
}
}
}
public extension View {
func sioCardBackground(tint: Color? = nil, cornerRadius: CGFloat = SIO.cardRadius) -> some View {
background(.regularMaterial, in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.fill((tint ?? Color.clear).opacity(0.08))
)
.overlay(
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.strokeBorder(Color.primary.opacity(0.08), lineWidth: 1)
)
}
func sioSoftSelection(_ isSelected: Bool) -> some View {
background(
RoundedRectangle(cornerRadius: SIO.cardRadius, style: .continuous)
.fill(isSelected ? SIO.tint.opacity(0.12) : Color.clear)
)
}
func sioProse() -> some View {
font(.system(size: 15.5))
.lineSpacing(6)
}
}