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:
@@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user