mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Move compound-ios package into the project.
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public extension ButtonStyle where Self == CompoundButtonStyle {
|
||||
/// A button style that applies Compound design tokens to a button with various configuration options.
|
||||
/// - Parameter kind: The kind of button being shown such as primary or secondary.
|
||||
/// - Parameter size: The button size to use. Defaults to `large`.
|
||||
static func compound(_ kind: Self.Kind, size: Self.Size = .large) -> CompoundButtonStyle {
|
||||
CompoundButtonStyle(kind: kind, size: size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Default button style for standalone buttons.
|
||||
public struct CompoundButtonStyle: ButtonStyle {
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.accessibilityShowButtonShapes) private var accessibilityShowButtonShapes
|
||||
|
||||
var kind: Kind
|
||||
public enum Kind {
|
||||
/// A stroked button that uses colour to highlight important actions.
|
||||
case `super`
|
||||
/// A filled button usually representing the default action.
|
||||
case primary
|
||||
/// A stroked button usually representing alternate actions.
|
||||
case secondary
|
||||
/// A plain button with matching dimensions to ``primary`` and ``secondary``.
|
||||
case tertiary
|
||||
/// A plain button with no padding.
|
||||
case textLink
|
||||
}
|
||||
|
||||
var size: Size
|
||||
public enum Size {
|
||||
/// A button that is prominently sized.
|
||||
case large
|
||||
/// A button that is a regular size.
|
||||
case medium
|
||||
/// A button that is a small size.
|
||||
case small
|
||||
/// A (super/primary/secondary) button that should be place within a toolbar.
|
||||
case toolbarIcon
|
||||
}
|
||||
|
||||
private var font: Font {
|
||||
if kind == .textLink, size == .small {
|
||||
.compound.bodyMDSemibold
|
||||
} else {
|
||||
.compound.bodyLGSemibold
|
||||
}
|
||||
}
|
||||
|
||||
private var horizontalPadding: CGFloat {
|
||||
if kind == .textLink {
|
||||
return 0
|
||||
}
|
||||
|
||||
return switch size {
|
||||
case .large: 20
|
||||
case .medium: 20
|
||||
case .small: 16
|
||||
case .toolbarIcon: 3
|
||||
}
|
||||
}
|
||||
|
||||
private var verticalPadding: CGFloat {
|
||||
if kind == .textLink {
|
||||
return 0
|
||||
}
|
||||
|
||||
return switch size {
|
||||
case .large: 14
|
||||
case .medium: 7
|
||||
case .small: 4
|
||||
case .toolbarIcon: 3
|
||||
}
|
||||
}
|
||||
|
||||
private var maxWidth: CGFloat? {
|
||||
if kind == .textLink {
|
||||
return nil
|
||||
}
|
||||
|
||||
return switch size {
|
||||
case .large: .infinity
|
||||
case .medium: nil
|
||||
case .small: nil
|
||||
case .toolbarIcon: nil
|
||||
}
|
||||
}
|
||||
|
||||
private var pressedOpacity: Double {
|
||||
colorScheme == .light ? 0.3 : 0.6
|
||||
}
|
||||
|
||||
private var isUnderlined: Bool {
|
||||
kind == .textLink && accessibilityShowButtonShapes
|
||||
}
|
||||
|
||||
public func makeBody(configuration: Self.Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(font)
|
||||
.underline(isUnderlined)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(textColor(configuration: configuration))
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.vertical, verticalPadding)
|
||||
.frame(maxWidth: maxWidth)
|
||||
.background {
|
||||
makeBackground(configuration: configuration)
|
||||
}
|
||||
.contentShape(contentShape)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeBackground(configuration: Self.Configuration) -> some View {
|
||||
switch kind {
|
||||
case .super:
|
||||
if isEnabled {
|
||||
ZStack {
|
||||
Capsule().fill(.compound.bgCanvasDefault)
|
||||
Capsule().fill(LinearGradient(gradient: .compound.action,
|
||||
startPoint: .top, endPoint: .bottom))
|
||||
.opacity(0.04)
|
||||
Capsule().strokeBorder(LinearGradient(gradient: .compound.action,
|
||||
startPoint: .top, endPoint: .bottom))
|
||||
}
|
||||
.compositingGroup()
|
||||
.opacity(configuration.isPressed ? pressedOpacity : 1)
|
||||
} else {
|
||||
Capsule().strokeBorder(strokeColor(configuration: configuration))
|
||||
}
|
||||
case .primary:
|
||||
Capsule().fill(fillColor(configuration: configuration))
|
||||
case .secondary:
|
||||
Capsule().strokeBorder(strokeColor(configuration: configuration))
|
||||
case .tertiary:
|
||||
EmptyView()
|
||||
case .textLink:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
private var contentShape: AnyShape {
|
||||
switch kind {
|
||||
case .super, .primary, .secondary, .tertiary:
|
||||
return AnyShape(Capsule())
|
||||
case .textLink:
|
||||
return AnyShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
private func fillColor(configuration: Self.Configuration) -> Color {
|
||||
guard isEnabled else { return .compound.bgActionPrimaryDisabled }
|
||||
if configuration.role == .destructive {
|
||||
return .compound.bgCriticalPrimary.opacity(configuration.isPressed ? pressedOpacity : 1)
|
||||
} else {
|
||||
return configuration.isPressed ? .compound.bgActionPrimaryPressed : .compound.bgActionPrimaryRest
|
||||
}
|
||||
}
|
||||
|
||||
private func strokeColor(configuration: Self.Configuration) -> Color {
|
||||
if configuration.role == .destructive {
|
||||
return .compound.borderCriticalPrimary.opacity(configuration.isPressed ? pressedOpacity : 1)
|
||||
} else {
|
||||
return .compound.borderInteractiveSecondary.opacity(configuration.isPressed ? pressedOpacity : 1)
|
||||
}
|
||||
}
|
||||
|
||||
private func textColor(configuration: Configuration) -> Color {
|
||||
if kind == .primary {
|
||||
return .compound.textOnSolidPrimary
|
||||
} else {
|
||||
guard isEnabled else { return .compound.textDisabled }
|
||||
let textColor: Color = configuration.role == .destructive ? .compound.textCriticalPrimary : .compound.textActionPrimary
|
||||
return textColor.opacity(configuration.isPressed ? pressedOpacity : 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
public struct CompoundButtonStyle_Previews: PreviewProvider, TestablePreview {
|
||||
public static var previews: some View {
|
||||
ScrollView {
|
||||
states
|
||||
}
|
||||
.previewLayout(.fixed(width: 390, height: 1875))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
public static var states: some View {
|
||||
Section {
|
||||
buttons(.large)
|
||||
} header: {
|
||||
Header(title: "Large")
|
||||
}
|
||||
|
||||
Section {
|
||||
buttons(.medium)
|
||||
} header: {
|
||||
Header(title: "Medium")
|
||||
}
|
||||
|
||||
Section {
|
||||
buttons(.small)
|
||||
} header: {
|
||||
Header(title: "Small")
|
||||
}
|
||||
|
||||
Section {
|
||||
textLinks(.medium)
|
||||
} header: {
|
||||
Header(title: "Text Link")
|
||||
}
|
||||
|
||||
Section {
|
||||
textLinks(.small)
|
||||
} header: {
|
||||
Header(title: "Text Link Small")
|
||||
}
|
||||
|
||||
Section {
|
||||
startChat
|
||||
.padding(.bottom) // Only for the snapshot.
|
||||
} header: {
|
||||
Header(title: "Start chat")
|
||||
}
|
||||
}
|
||||
|
||||
public static func buttons(_ size: CompoundButtonStyle.Size) -> some View {
|
||||
VStack {
|
||||
Button("Super") { }
|
||||
.buttonStyle(.compound(.super, size: size))
|
||||
|
||||
Button("Disabled") { }
|
||||
.buttonStyle(.compound(.super, size: size))
|
||||
.disabled(true)
|
||||
|
||||
Button("Primary") { }
|
||||
.buttonStyle(.compound(.primary, size: size))
|
||||
|
||||
Button("Destructive", role: .destructive) { }
|
||||
.buttonStyle(.compound(.primary, size: size))
|
||||
|
||||
Button("Disabled") { }
|
||||
.buttonStyle(.compound(.primary, size: size))
|
||||
.disabled(true)
|
||||
|
||||
Button("Secondary") { }
|
||||
.buttonStyle(.compound(.secondary, size: size))
|
||||
|
||||
Button("Destructive", role: .destructive) { }
|
||||
.buttonStyle(.compound(.secondary, size: size))
|
||||
|
||||
Button("Disabled") { }
|
||||
.buttonStyle(.compound(.secondary, size: size))
|
||||
.disabled(true)
|
||||
|
||||
Button("Tertiary") { }
|
||||
.buttonStyle(.compound(.tertiary, size: size))
|
||||
|
||||
Button("Destructive", role: .destructive) { }
|
||||
.buttonStyle(.compound(.tertiary, size: size))
|
||||
|
||||
Button("Disabled") { }
|
||||
.buttonStyle(.compound(.tertiary, size: size))
|
||||
.disabled(true)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
static func textLinks(_ size: CompoundButtonStyle.Size) -> some View {
|
||||
HStack(spacing: 20) {
|
||||
Button("Text Link") { }
|
||||
.buttonStyle(.compound(.textLink, size: size))
|
||||
|
||||
Button("Destructive", role: .destructive) { }
|
||||
.buttonStyle(.compound(.textLink, size: size))
|
||||
|
||||
Button("Disabled") { }
|
||||
.buttonStyle(.compound(.textLink, size: size))
|
||||
.disabled(true)
|
||||
}
|
||||
.padding(.top, 1)
|
||||
}
|
||||
|
||||
static var startChat: some View {
|
||||
Button { } label: {
|
||||
CompoundIcon(\.plus)
|
||||
}
|
||||
.buttonStyle(.compound(.super, size: .toolbarIcon))
|
||||
}
|
||||
|
||||
struct Header: View {
|
||||
let title: String
|
||||
|
||||
var body: some View {
|
||||
Text(title)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding([.leading, .top])
|
||||
.padding(.leading )
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user