Apply Emil Kowalski design across all screens
Near-monochrome palette (ink tokens), black primary buttons, spring animations, 0.5pt hairline dividers, shadow cards, squircle corners. Replaces all blue/coloured accents. Adds ButtonStyles component. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
63
Shared/Components/ButtonStyles.swift
Normal file
63
Shared/Components/ButtonStyles.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: — Primary (black fill)
|
||||
struct PrimaryButtonStyle: ButtonStyle {
|
||||
var color: Color = AppTheme.ink
|
||||
var height: CGFloat = 50
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: height)
|
||||
.background(color)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
||||
.scaleEffect(configuration.isPressed ? 0.97 : 1)
|
||||
.animation(.spring(response: 0.25, dampingFraction: 0.7), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: — Ghost (outline)
|
||||
struct GhostButtonStyle: ButtonStyle {
|
||||
var height: CGFloat = 50
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.foregroundColor(AppTheme.ink)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: height)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)
|
||||
.stroke(AppTheme.cardBorderColor, lineWidth: 1)
|
||||
)
|
||||
.scaleEffect(configuration.isPressed ? 0.97 : 1)
|
||||
.animation(.spring(response: 0.25, dampingFraction: 0.7), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: — Scale (tap anywhere, just scales)
|
||||
struct ScaleButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? 0.97 : 1)
|
||||
.animation(.spring(response: 0.25, dampingFraction: 0.7), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: — Pill (compact, rounded)
|
||||
struct PillButtonStyle: ButtonStyle {
|
||||
var color: Color = AppTheme.ink
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 7)
|
||||
.background(color)
|
||||
.clipShape(Capsule())
|
||||
.scaleEffect(configuration.isPressed ? 0.96 : 1)
|
||||
.animation(.spring(response: 0.2, dampingFraction: 0.7), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
@@ -8,31 +8,37 @@ struct FolderRow: View {
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: item.isDirectory ? "folder.fill" : "doc.fill")
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(isSelected ? .white : (item.isDirectory ? AppTheme.blue : AppTheme.textSecondary))
|
||||
Image(systemName: item.isDirectory ? "folder" : "doc")
|
||||
.font(.system(size: 15, weight: .regular))
|
||||
.foregroundStyle(isSelected ? .white : AppTheme.inkSecondary)
|
||||
.frame(width: 22)
|
||||
|
||||
Text(item.name)
|
||||
.font(AppTheme.bodyFont)
|
||||
.fontWeight(isSelected ? .semibold : .regular)
|
||||
.foregroundColor(isSelected ? .white : AppTheme.textPrimary)
|
||||
.font(.system(size: 15, weight: isSelected ? .medium : .regular))
|
||||
.foregroundStyle(isSelected ? .white : AppTheme.ink)
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
} else if item.isDirectory {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundColor(AppTheme.textTertiary)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(AppTheme.inkQuaternary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(isSelected ? AppTheme.blue : Color.clear)
|
||||
.background(
|
||||
isSelected
|
||||
? AppTheme.ink
|
||||
: Color.clear
|
||||
)
|
||||
.animation(.spring(response: 0.2, dampingFraction: 0.8), value: isSelected)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,21 @@ import SwiftUI
|
||||
|
||||
struct ProgressRing: View {
|
||||
let progress: Double
|
||||
var lineWidth: CGFloat = 14
|
||||
var lineWidth: CGFloat = 8
|
||||
var size: CGFloat = 200
|
||||
var color: Color = AppTheme.blue
|
||||
var backgroundColor: Color = Color(hex: "#E5E7EB")
|
||||
var color: Color = AppTheme.ink
|
||||
var backgroundColor: Color = Color(hex: "#EBEBEB")
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(backgroundColor, lineWidth: lineWidth)
|
||||
.stroke(backgroundColor, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
|
||||
|
||||
Circle()
|
||||
.trim(from: 0, to: CGFloat(min(progress, 1.0)))
|
||||
.stroke(
|
||||
color,
|
||||
style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)
|
||||
)
|
||||
.stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
.animation(.easeInOut(duration: 0.4), value: progress)
|
||||
.animation(.spring(response: 0.5, dampingFraction: 0.8), value: progress)
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
|
||||
@@ -9,18 +9,18 @@ struct ToggleRow: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(AppTheme.blue)
|
||||
.frame(width: 24)
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
.frame(width: 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(title)
|
||||
.font(AppTheme.bodyFont)
|
||||
.foregroundColor(AppTheme.textPrimary)
|
||||
.font(AppTheme.body())
|
||||
.foregroundStyle(AppTheme.ink)
|
||||
if let subtitle {
|
||||
Text(subtitle)
|
||||
.font(AppTheme.captionFont)
|
||||
.foregroundColor(AppTheme.textSecondary)
|
||||
.font(AppTheme.caption())
|
||||
.foregroundStyle(AppTheme.inkTertiary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ struct ToggleRow: View {
|
||||
|
||||
Toggle("", isOn: $isOn)
|
||||
.labelsHidden()
|
||||
.tint(AppTheme.blue)
|
||||
.tint(AppTheme.ink)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user