diff --git a/Snapshots/iPad/CountryFlagTests/testCountryFlags.1.png b/Snapshots/iPad/CountryFlagTests/testCountryFlags.1.png index aaa7c521930..dc57a41794b 100644 Binary files a/Snapshots/iPad/CountryFlagTests/testCountryFlags.1.png and b/Snapshots/iPad/CountryFlagTests/testCountryFlags.1.png differ diff --git a/Snapshots/iPad/IconTests/testIcons.1.png b/Snapshots/iPad/IconTests/testIcons.1.png index cd429217b1f..7b1547347b5 100644 Binary files a/Snapshots/iPad/IconTests/testIcons.1.png and b/Snapshots/iPad/IconTests/testIcons.1.png differ diff --git a/Snapshots/iPad/IconTests/testIcons.2.png b/Snapshots/iPad/IconTests/testIcons.2.png index 390479c02c1..4b08d9fbb55 100644 Binary files a/Snapshots/iPad/IconTests/testIcons.2.png and b/Snapshots/iPad/IconTests/testIcons.2.png differ diff --git a/Snapshots/iPad/NavigationButtonTests/testNavigationButtonBack.1.png b/Snapshots/iPad/NavigationButtonTests/testNavigationButtonBack.1.png index f8171a9717a..c2f6daf0226 100644 Binary files a/Snapshots/iPad/NavigationButtonTests/testNavigationButtonBack.1.png and b/Snapshots/iPad/NavigationButtonTests/testNavigationButtonBack.1.png differ diff --git a/Snapshots/iPad/NavigationButtonTests/testNavigationButtonClose.1.png b/Snapshots/iPad/NavigationButtonTests/testNavigationButtonClose.1.png index 8129cae0a5d..3c708964262 100644 Binary files a/Snapshots/iPad/NavigationButtonTests/testNavigationButtonClose.1.png and b/Snapshots/iPad/NavigationButtonTests/testNavigationButtonClose.1.png differ diff --git a/Snapshots/iPad/TextTests/testTexts.2.png b/Snapshots/iPad/TextTests/testTexts.2.png index cb0c930a853..fd43f5a98fc 100644 Binary files a/Snapshots/iPad/TextTests/testTexts.2.png and b/Snapshots/iPad/TextTests/testTexts.2.png differ diff --git a/Snapshots/iPhone/CountryFlagTests/testCountryFlags.1.png b/Snapshots/iPhone/CountryFlagTests/testCountryFlags.1.png index 4013a715dc2..dc57a41794b 100644 Binary files a/Snapshots/iPhone/CountryFlagTests/testCountryFlags.1.png and b/Snapshots/iPhone/CountryFlagTests/testCountryFlags.1.png differ diff --git a/Snapshots/iPhone/IconTests/testIcons.1.png b/Snapshots/iPhone/IconTests/testIcons.1.png index cd429217b1f..7b1547347b5 100644 Binary files a/Snapshots/iPhone/IconTests/testIcons.1.png and b/Snapshots/iPhone/IconTests/testIcons.1.png differ diff --git a/Snapshots/iPhone/IconTests/testIcons.2.png b/Snapshots/iPhone/IconTests/testIcons.2.png index 390479c02c1..4b08d9fbb55 100644 Binary files a/Snapshots/iPhone/IconTests/testIcons.2.png and b/Snapshots/iPhone/IconTests/testIcons.2.png differ diff --git a/Snapshots/iPhone/NavigationButtonTests/testNavigationButtonBack.1.png b/Snapshots/iPhone/NavigationButtonTests/testNavigationButtonBack.1.png index d3f2c320473..f0d3eb162c8 100644 Binary files a/Snapshots/iPhone/NavigationButtonTests/testNavigationButtonBack.1.png and b/Snapshots/iPhone/NavigationButtonTests/testNavigationButtonBack.1.png differ diff --git a/Snapshots/iPhone/NavigationButtonTests/testNavigationButtonClose.1.png b/Snapshots/iPhone/NavigationButtonTests/testNavigationButtonClose.1.png index 0dc69c305b5..4cad55a1592 100644 Binary files a/Snapshots/iPhone/NavigationButtonTests/testNavigationButtonClose.1.png and b/Snapshots/iPhone/NavigationButtonTests/testNavigationButtonClose.1.png differ diff --git a/Snapshots/iPhone/TextTests/testTexts.2.png b/Snapshots/iPhone/TextTests/testTexts.2.png index cb0c930a853..fd43f5a98fc 100644 Binary files a/Snapshots/iPhone/TextTests/testTexts.2.png and b/Snapshots/iPhone/TextTests/testTexts.2.png differ diff --git a/Sources/Orbit/Components/Badge.swift b/Sources/Orbit/Components/Badge.swift index cc37463ba02..0a231be3a65 100644 --- a/Sources/Orbit/Components/Badge.swift +++ b/Sources/Orbit/Components/Badge.swift @@ -20,18 +20,18 @@ public struct Badge: View { if isEmpty == false { HStack(spacing: .xxSmall) { leadingIcon + .iconSize(.small) .font(.system(size: Icon.Size.small.value)) .foregroundColor(labelColor) - Text( - label, - size: .small - ) - .fontWeight(.medium) - .textLinkColor(.custom(labelColor)) - .frame(minWidth: minTextWidth) + Text(label) + .textSize(.small) + .fontWeight(.medium) + .textLinkColor(.custom(labelColor)) + .frame(minWidth: minTextWidth) trailingIcon + .iconSize(.small) .font(.system(size: Icon.Size.small.value)) .foregroundColor(labelColor) } @@ -100,9 +100,9 @@ public extension Badge { style: BadgeStyle = .neutral ) where LeadingIcon == Icon, TrailingIcon == Icon { self.init(label, style: style) { - Icon(icon, size: .small) + Icon(icon) } trailingIcon: { - Icon(trailingIcon, size: .small) + Icon(trailingIcon) } } @@ -225,11 +225,11 @@ struct BadgePreviews: PreviewProvider { .iconColor(.pink) Badge("Flag") { - CountryFlag("us", size: .small) + CountryFlag("us") } Badge("Flag", style: .status(.critical, inverted: true)) { - CountryFlag("cz", size: .small) + CountryFlag("cz") } } diff --git a/Sources/Orbit/Components/BadgeList.swift b/Sources/Orbit/Components/BadgeList.swift index 0315c2699d0..26fe70aca2d 100644 --- a/Sources/Orbit/Components/BadgeList.swift +++ b/Sources/Orbit/Components/BadgeList.swift @@ -13,8 +13,6 @@ public struct BadgeList: View { let label: String let style: Style - let labelColor: LabelColor - let size: Size @ViewBuilder let icon: Icon public var body: some View { @@ -27,17 +25,17 @@ public struct BadgeList: View { .padding(.xxSmall) .background(badgeBackground) - Text(label, size: size.textSize) - .textColor(textColor ?? labelColor.color) + Text(label) .textAccentColor(textAccentColor ?? iconColor) - .textLinkColor(.custom(labelColor.color)) + .textLinkColor(.custom(textColor ?? .inkDark)) } } } @ViewBuilder var badge: some View { if icon.isEmpty { - Orbit.Icon(.grid, size: .small) + Orbit.Icon(.grid) + .iconSize(.small) .opacity(0) } else { icon @@ -86,17 +84,14 @@ public extension BadgeList { init( _ label: String = "", icon: Icon.Symbol? = nil, - style: Style = .neutral, - labelColor: LabelColor = .primary, - size: Size = .normal + style: Style = .neutral ) where Icon == Orbit.Icon { self.init( label, - style: style, - labelColor: labelColor, - size: size + style: style ) { - Icon(icon, size: .small) + Icon(icon) + .iconSize(.small) } } @@ -107,14 +102,10 @@ public extension BadgeList { init( _ label: String = "", style: Style = .neutral, - labelColor: LabelColor = .primary, - size: Size = .normal, @ViewBuilder icon: () -> Icon ) { self.label = label self.style = style - self.labelColor = labelColor - self.size = size self.icon = icon() } } @@ -123,39 +114,10 @@ public extension BadgeList { public extension BadgeList { enum Style: Equatable, Hashable { - case neutral case status(_ status: Status?) case custom(iconColor: Color, backgroundColor: Color) } - - enum LabelColor { - case primary - case secondary - case custom(_ color: Color) - - var color: Color { - switch self { - case .primary: return .inkDark - case .secondary: return .inkNormal - case .custom(let color): return color - } - } - } - - enum Size: Equatable { - case small - case normal - case custom(_ size: Text.Size) - - var textSize: Text.Size { - switch self { - case .small: return .small - case .normal: return .normal - case .custom(let size): return size - } - } - } } // MARK: - Previews @@ -185,7 +147,9 @@ struct BadgeListPreviews: PreviewProvider { } static var smallSecondary: some View { - BadgeList("Neutral BadgeList", icon: .grid, labelColor: .secondary, size: .small) + BadgeList("Neutral BadgeList", icon: .grid) + .textSize(.small) + .textColor(.inkLight) .padding(.medium) .previewDisplayName() } @@ -200,12 +164,14 @@ struct BadgeListPreviews: PreviewProvider { BadgeList(label, icon: .alertCircle, style: .status(.critical)) } VStack(alignment: .leading, spacing: .medium) { - BadgeList(longLabel, icon: .grid, labelColor: .secondary, size: .small) - BadgeList(label, icon: .informationCircle, style: .status(.info), labelColor: .secondary, size: .small) - BadgeList(label, icon: .checkCircle, style: .status(.success), labelColor: .secondary, size: .small) - BadgeList(label, icon: .alertCircle, style: .status(.warning), labelColor: .secondary, size: .small) - BadgeList(label, icon: .alertCircle, style: .status(.critical), labelColor: .secondary, size: .small) + BadgeList(longLabel, icon: .grid) + BadgeList(label, icon: .informationCircle, style: .status(.info)) + BadgeList(label, icon: .checkCircle, style: .status(.success)) + BadgeList(label, icon: .alertCircle, style: .status(.warning)) + BadgeList(label, icon: .alertCircle, style: .status(.critical)) } + .textColor(.inkNormal) + .textSize(.small) } .padding(.medium) .previewDisplayName() @@ -214,16 +180,18 @@ struct BadgeListPreviews: PreviewProvider { static var mix: some View { VStack(alignment: .leading, spacing: .medium) { BadgeList("This is simple BadgeList item with SF Symbol", style: .status(.info)) { - Icon("info.circle.fill", size: .small) + Icon("info.circle.fill") } BadgeList("This is simple BadgeList item with CountryFlag", style: .status(.critical)) { - CountryFlag("us", size: .small) + CountryFlag("us") } - BadgeList("This is BadgeList item with no icon and custom color", labelColor: .custom(.blueDark)) + BadgeList("This is BadgeList item with no icon and custom color") + .textColor(.blueDark) BadgeList("This is a BadgeList with status override", style: .status(nil)) { - Icon("info.circle.fill", size: .small) + Icon("info.circle.fill") } } + .iconSize(.small) .status(.success) .padding(.medium) .previewDisplayName() diff --git a/Sources/Orbit/Components/Button.swift b/Sources/Orbit/Components/Button.swift index cbc4c786e09..817078814e3 100644 --- a/Sources/Orbit/Components/Button.swift +++ b/Sources/Orbit/Components/Button.swift @@ -33,7 +33,7 @@ public struct Button: View { } @ViewBuilder var text: some View { - Text(label, size: size.textSize) + Text(label) .fontWeight(.medium) } } @@ -59,9 +59,9 @@ public extension Button { size: size, action: action ) { - Icon(icon, size: size.textSize.iconSize) + Icon(icon) } disclosureIcon: { - Icon(disclosureIcon, size: size.textSize.iconSize) + Icon(disclosureIcon) } } @@ -178,7 +178,8 @@ public struct OrbitButtonStyle: Primitive @ViewBuilder func content(_ label: some View) -> some View { HStack(spacing: 0) { - TextStrut(size.textSize) + TextStrut() + .textSize(size.textSize) if disclosureIcon.isEmpty, idealSize.horizontal == nil { Spacer(minLength: 0) @@ -203,6 +204,7 @@ public struct OrbitButtonStyle: Primitive .iconColor(iconColor) .foregroundColor(iconColor) } + .textSize(size.textSize) .textColor(resolvedTextColor) .padding(.leading, leadingPadding) .padding(.trailing, trailingPadding) @@ -456,7 +458,7 @@ struct ButtonPreviews: PreviewProvider { Button("Button with Flag", type: .secondary) { // No action } icon: { - CountryFlag("us", size: .normal) + CountryFlag("us") } } .padding(.medium) diff --git a/Sources/Orbit/Components/Checkbox.swift b/Sources/Orbit/Components/Checkbox.swift index ea98dc44b58..58e29927bad 100644 --- a/Sources/Orbit/Components/Checkbox.swift +++ b/Sources/Orbit/Components/Checkbox.swift @@ -33,7 +33,8 @@ public struct Checkbox: View { .fontWeight(.medium) .accessibility(.checkboxTitle) - Text(description, size: .small) + Text(description) + .textSize(.small) .textColor(descriptionColor) .accessibility(.checkboxDescription) } @@ -114,7 +115,8 @@ public extension Checkbox { .fill(indicatorBackgroundColor(isPressed: isPressed)) ) .overlay( - Icon(.check, size: .small) + Icon(.check) + .iconSize(.small) .textColor(isEnabled ? .whiteNormal : .cloudNormal) .opacity(isChecked ? 1 : 0) ) diff --git a/Sources/Orbit/Components/ChoiceTile.swift b/Sources/Orbit/Components/ChoiceTile.swift index 912243a5482..f3006c4d50b 100644 --- a/Sources/Orbit/Components/ChoiceTile.swift +++ b/Sources/Orbit/Components/ChoiceTile.swift @@ -45,7 +45,7 @@ public struct ChoiceTile: View { centerIndicator } - TextStrut(.normal) + TextStrut() .padding(.vertical, .xxSmall) // Minimum 52pt @ normal size } .frame(maxWidth: idealSize.horizontal == true ? nil: .infinity, alignment: .leading) @@ -124,7 +124,8 @@ public struct ChoiceTile: View { @ViewBuilder var iconView: some View { icon - .font(.system(size: titleStyle.iconSize.value)) + .iconSize(custom: titleStyle.lineHeight) + .font(.system(size: Orbit.Icon.Size.fromTextSize(size: titleStyle.size))) .foregroundColor(.inkNormal) .accessibility(.choiceTileIcon) } @@ -267,7 +268,7 @@ public extension ChoiceTile { } content: { content() } icon: { - Icon(icon, size: titleStyle.iconSize) + Icon(icon) } header: { header() } diff --git a/Sources/Orbit/Components/CountryFlag.swift b/Sources/Orbit/Components/CountryFlag.swift index dcde8655d57..a121ac5fc4e 100644 --- a/Sources/Orbit/Components/CountryFlag.swift +++ b/Sources/Orbit/Components/CountryFlag.swift @@ -5,10 +5,11 @@ import SwiftUI /// - Note: [Orbit definition](https://orbit.kiwi/components/countryflag/) public struct CountryFlag: View { + @Environment(\.textSize) var textSize + @Environment(\.iconSize) var iconSize @Environment(\.sizeCategory) var sizeCategory let countryCode: CountryCode - let size: Size let border: Border public var body: some View { @@ -20,10 +21,9 @@ public struct CountryFlag: View { clipShape.strokeBorder(border.color, lineWidth: BorderWidth.hairline) .blendMode(.darken) ) - .alignmentGuide(.firstTextBaseline) { $0[.bottom] } - .alignmentGuide(.lastTextBaseline) { $0[.bottom] } - .frame(width: width, height: height) - .fixedSize() + .frame(width: size, height: size) + .alignmentGuide(.firstTextBaseline) { $0.height * Icon.symbolBaseline } + .alignmentGuide(.lastTextBaseline) { $0.height * Icon.symbolBaseline } .accessibility(label: SwiftUI.Text(countryCode.rawValue)) } @@ -35,22 +35,12 @@ public struct CountryFlag: View { switch border { case .none: return 0 case .default(let cornerRadius?): return cornerRadius - case .default: return width / 10 + case .default: return size / 10 } } - var width: CGFloat { - switch size { - case .width(let width): return width * sizeCategory.ratio - case .icon(let size): return size.value * sizeCategory.ratio - } - } - - var height: CGFloat? { - switch size { - case .width: return nil - case .icon(let size): return size.value * sizeCategory.ratio - } + var size: CGFloat { + (iconSize ?? textSize.map(Icon.Size.fromTextSize(size:)) ?? Icon.Size.normal.value) * sizeCategory.ratio } } @@ -58,21 +48,16 @@ public struct CountryFlag: View { public extension CountryFlag { /// Creates Orbit CountryFlag component. - init(_ countryCode: CountryCode, size: Size = .normal, border: Border = .default()) { + init(_ countryCode: CountryCode, border: Border = .default()) { self.countryCode = countryCode - self.size = size self.border = border } /// Creates Orbit CountryFlag component with a string country code. /// /// If a corresponding image is not found, the flag for unknown codes is used. - init(_ countryCode: String, size: Size = .normal, border: Border = .default()) { - self.init( - .init(countryCode), - size: size, - border: border - ) + init(_ countryCode: String, border: Border = .default()) { + self.init(.init(countryCode), border: border) } } @@ -134,38 +119,43 @@ struct CountryFlagPreviews: PreviewProvider { static var unknown: some View { VStack { CountryFlag("") - CountryFlag("some invalid identifier") + CountryFlag("invalid") } .previewDisplayName() } static var mix: some View { VStack(alignment: .leading, spacing: .xLarge) { - flags(size: .small) - flags(size: .normal) - flags(size: .large) - flags(size: .custom(40)) + flags(.small) + flags(.normal) + flags(.large) + flags(size: 40) HStack(alignment: .firstTextBaseline, spacing: .small) { Text("Borders") - CountryFlag("CZ", size: .icon(.xLarge), border: .default(cornerRadius: 8)) - CountryFlag("cZ", size: .icon(.xLarge), border: .default(cornerRadius: 0)) - CountryFlag("Cz", size: .icon(.xLarge), border: .none) + CountryFlag("CZ", border: .default(cornerRadius: 8)) + CountryFlag("cZ", border: .default(cornerRadius: 0)) + CountryFlag("Cz", border: .none) } + .textSize(.xLarge) } .previewDisplayName() } - static func flags(size: Icon.Size) -> some View { + static func flags(_ size: Icon.Size) -> some View { + flags(size: size.value, label: String(describing: size).titleCased) + } + + static func flags(size: CGFloat, label: String? = nil) -> some View { HStack(alignment: .firstTextBaseline, spacing: .small) { - Text("\(size)".capitalized) - CountryFlag("cz", size: .icon(size)) - CountryFlag("sg", size: .icon(size)) - CountryFlag("jp", size: .icon(size)) - CountryFlag("de", size: .icon(size)) - CountryFlag("unknown", size: .icon(size)) + Text("\(label ?? String(describing: Int(size)))".capitalized) + CountryFlag("cz") + CountryFlag("sg") + CountryFlag("jp") + CountryFlag("unknown") } - .overlay(Separator(color: .redNormal, thickness: .hairline), alignment: .centerFirstTextBaseline) + .textSize(custom: size) + .background(Separator(color: .redNormal, thickness: .hairline), alignment: .centerFirstTextBaseline) } static var snapshot: some View { diff --git a/Sources/Orbit/Components/Coupon.swift b/Sources/Orbit/Components/Coupon.swift index 7c0fadf0c33..3b30606484a 100644 --- a/Sources/Orbit/Components/Coupon.swift +++ b/Sources/Orbit/Components/Coupon.swift @@ -6,12 +6,12 @@ import SwiftUI public struct Coupon: View { let label: String - let size: Text.Size public var body: some View { - Text(label, size: size, isSelectable: true) + Text(label) .fontWeight(.medium) .kerning(0.75) + .textIsCopyable() .padding(.horizontal, .xxSmall) .padding(.vertical, .xxxSmall) .overlay( @@ -26,9 +26,8 @@ public struct Coupon: View { public extension Coupon { /// Creates Orbit Coupon component. - init(_ label: String = "", size: Text.Size = .normal) { + init(_ label: String = "") { self.label = label - self.size = size } } @@ -55,12 +54,13 @@ struct CouponPreviews: PreviewProvider { static var mix: some View { HStack(alignment: .firstTextBaseline, spacing: .small) { - Coupon("HXT3B81F", size: .small) + Coupon("HXT3B81F") .textColor(.blueDark) .previewDisplayName() - Text("PROMOCODE", size: .small) + Text("PROMOCODE") } + .textSize(.small) .padding(.medium) } } diff --git a/Sources/Orbit/Components/Heading.swift b/Sources/Orbit/Components/Heading.swift index 48e4db00589..589da3cceeb 100644 --- a/Sources/Orbit/Components/Heading.swift +++ b/Sources/Orbit/Components/Heading.swift @@ -12,16 +12,18 @@ public struct Heading: View, FormattedTextBuildable { let isSelectable: Bool // Builder properties + var accentColor: Color? var baselineOffset: CGFloat? - var fontWeight: Font.Weight? var color: Color? - var strikethrough: Bool? - var kerning: CGFloat? - var accentColor: Color? + var fontWeight: Font.Weight? var isBold: Bool? var isItalic: Bool? var isMonospacedDigit: Bool? var isUnderline: Bool? + var kerning: CGFloat? + var lineHeight: CGFloat? + var size: CGFloat? + var strikethrough: Bool? public var body: some View { textContent @@ -29,21 +31,19 @@ public struct Heading: View, FormattedTextBuildable { } @ViewBuilder var textContent: Text { - Text( - content, - size: .custom(style.size, lineHeight: style.lineHeight), - isSelectable: isSelectable - ) - .fontWeight(fontWeight) - .textAccentColor(accentColor) - .baselineOffset(baselineOffset) - .bold(isBold) - .italic(isItalic) - .kerning(kerning) - .monospacedDigit(isMonospacedDigit) - .strikethrough(strikethrough) - .underline(isUnderline) - .textColor(color) + Text(content) + .textColor(color) + .textSize(custom: style.size) + .fontWeight(fontWeight) + .baselineOffset(baselineOffset) + .bold(isBold) + .italic(isItalic) + .kerning(kerning) + .monospacedDigit(isMonospacedDigit) + .strikethrough(strikethrough) + .underline(isUnderline) + .textAccentColor(accentColor) + .textLineHeight(style.lineHeight) } func text(textRepresentableEnvironment: TextRepresentableEnvironment) -> SwiftUI.Text { @@ -82,7 +82,8 @@ public extension Heading { // MARK: - Types public extension Heading { - enum Style { + /// Orbit heading style. + enum Style: Equatable { /// 28 pts. case title1 /// 22 pts. @@ -96,7 +97,7 @@ public extension Heading { /// 13 pts. case title6 - /// Font size. + /// Text font size value. public var size: CGFloat { switch self { case .title1: return 28 @@ -108,6 +109,7 @@ public extension Heading { } } + /// Designated line height. public var lineHeight: CGFloat { switch self { case .title1: return 32 @@ -119,21 +121,12 @@ public extension Heading { } } - /// Icon size matching heading line height. - public var iconSize: Icon.Size { - .custom(lineHeight) - } - public var weight: Font.Weight { switch self { case .title1, .title4, .title5, .title6: return .bold case .title2, .title3: return .medium } } - - public var textSize: Text.Size { - .custom(size, lineHeight: lineHeight) - } } } @@ -315,7 +308,8 @@ struct HeadingPreviews: PreviewProvider { .fixedSize() .overlay(Separator(color: .redNormal, thickness: .hairline), alignment: .centerLastTextBaseline) - Text("\(style.size.formatted) / \(style.lineHeight.formatted)", size: .custom(6)) + Text("\(style.size.formatted) / \(style.lineHeight.formatted)") + .textSize(custom: 6) .environment(\.sizeCategory, .large) } .padding(.trailing, .xxSmall) diff --git a/Sources/Orbit/Components/Icon.swift b/Sources/Orbit/Components/Icon.swift index 12af0571fef..56681444c31 100644 --- a/Sources/Orbit/Components/Icon.swift +++ b/Sources/Orbit/Components/Icon.swift @@ -35,17 +35,19 @@ public struct Icon: View, TextBuildable { public static let sfSymbolDefaultWeight: Font.Weight = .medium @Environment(\.iconColor) private var iconColor - @Environment(\.sizeCategory) private var sizeCategory + @Environment(\.iconSize) private var iconSize @Environment(\.textColor) private var textColor @Environment(\.textFontWeight) private var textFontWeight + @Environment(\.textSize) private var textSize + @Environment(\.sizeCategory) private var sizeCategory private let content: Content? - private let size: Size // Builder properties var baselineOffset: CGFloat? var fontWeight: Font.Weight? var color: Color? + var size: CGFloat? public var body: some View { switch content { @@ -53,46 +55,65 @@ public struct Icon: View, TextBuildable { EmptyView() case .symbol(let symbol): SwiftUI.Text(verbatim: symbol.value) - .font(.orbitIcon(size: size.value)) - .foregroundColor(resolvedColor) + .font(.orbitIcon(size: resolvedSize(textRepresentableEnvironment))) + .foregroundColor(resolvedColor(textRepresentableEnvironment)) .flipsForRightToLeftLayoutDirection(symbol.flipsForRightToLeftLayoutDirection) - .frame(height: dynamicSize) - .frame(minWidth: dynamicSize) + .frame(height: frameSize) + .frame(minWidth: frameSize) .alignmentGuide(.firstTextBaseline, computeValue: baseline) .alignmentGuide(.lastTextBaseline, computeValue: baseline) .accessibility(label: .init(String(describing: symbol).titleCased)) case .sfSymbol(let systemName): Image(systemName: systemName) - .font(sfSymbolFont(sizeCategory: sizeCategory, textFontWeight: textFontWeight)) - .foregroundColor(resolvedColor) - .frame(height: dynamicSize) - .frame(minWidth: dynamicSize) + .font(sfSymbolFont(textRepresentableEnvironment)) + .foregroundColor(resolvedColor(textRepresentableEnvironment)) + .frame(height: frameSize) + .frame(minWidth: frameSize) .alignmentGuide(.firstTextBaseline) { $0[.firstTextBaseline] + resolvedBaselineOffset } .alignmentGuide(.lastTextBaseline) { $0[.lastTextBaseline] + resolvedBaselineOffset } } } + private var textRepresentableEnvironment: TextRepresentableEnvironment { + .init( + iconColor: iconColor, + iconSize: iconSize, + textAccentColor: nil, + textColor: textColor, + textFontWeight: textFontWeight, + textSize: textSize, + sizeCategory: sizeCategory + ) + } + public var isEmpty: Bool { content?.isEmpty ?? true } - private func sfSymbolFont(sizeCategory: ContentSizeCategory, textFontWeight: Font.Weight?) -> Font { + private func sfSymbolFont(_ textRepresentableEnvironment: TextRepresentableEnvironment) -> Font { .system( - size: sfSymbolSize * sizeCategory.ratio, - weight: fontWeight ?? textFontWeight ?? Self.sfSymbolDefaultWeight + size: sfSymbolSize(textRepresentableEnvironment) * textRepresentableEnvironment.sizeCategory.ratio, + weight: fontWeight ?? textRepresentableEnvironment.textFontWeight ?? Self.sfSymbolDefaultWeight ) } - private var resolvedColor: Color { - color ?? iconColor ?? textColor ?? .inkDark + private func resolvedColor(_ textRepresentableEnvironment: TextRepresentableEnvironment) -> Color { + color ?? textRepresentableEnvironment.iconColor ?? textRepresentableEnvironment.textColor ?? .inkDark + } + + private var frameSize: CGFloat { + round(resolvedSize(textRepresentableEnvironment) * sizeCategory.ratio) } - private var dynamicSize: CGFloat { - round(size.value * sizeCategory.ratio) + private func sfSymbolSize(_ textRepresentableEnvironment: TextRepresentableEnvironment) -> CGFloat { + round(resolvedSize(textRepresentableEnvironment) * Self.sfSymbolToOrbitSymbolSizeRatio) } - private var sfSymbolSize: CGFloat { - round(size.value * Self.sfSymbolToOrbitSymbolSizeRatio) + private func resolvedSize(_ textRepresentableEnvironment: TextRepresentableEnvironment) -> CGFloat { + size + ?? textRepresentableEnvironment.iconSize + ?? textRepresentableEnvironment.textSize.map(Icon.Size.fromTextSize(size:)) + ?? Icon.Size.normal.value } private var resolvedBaselineOffset: CGFloat { @@ -103,9 +124,8 @@ public struct Icon: View, TextBuildable { dimensions.height * Self.symbolBaseline + resolvedBaselineOffset } - private init(_ content: Content?, size: Size = .normal) { + private init(_ content: Content?) { self.content = content - self.size = size } } @@ -113,19 +133,13 @@ public struct Icon: View, TextBuildable { public extension Icon { /// Creates Orbit Icon component for provided Orbit symbol. - init(_ symbol: Icon.Symbol?, size: Size = .normal) { - self.init( - symbol.map { Icon.Content.symbol($0) }, - size: size - ) + init(_ symbol: Icon.Symbol?) { + self.init(symbol.map { Icon.Content.symbol($0) }) } /// Creates Orbit Icon component for provided SF Symbol that matches the Orbit symbol sizing, reflecting the Dynamic Type settings. - init(_ systemName: String, size: Size = .normal) { - self.init( - .sfSymbol(systemName), - size: size - ) + init(_ systemName: String) { + self.init(.sfSymbol(systemName)) } } @@ -142,8 +156,6 @@ public extension Icon { case large /// Size 28. case xLarge - /// Custom size. - case custom(CGFloat) public var value: CGFloat { switch self { @@ -151,9 +163,13 @@ public extension Icon { case .normal: return 20 case .large: return 24 case .xLarge: return 28 - case .custom(let size): return size } } + + /// Icon size matching text line height. + public static func fromTextSize(size: CGFloat) -> CGFloat { + Text.Size.lineHeight(forTextSize: size) + } } } @@ -190,20 +206,17 @@ extension Icon: TextRepresentable { case .none: return nil case .symbol(let symbol): - return symbolWrapper(sizeCategory: textRepresentableEnvironment.sizeCategory) { - colorWrapper(textRepresentableEnvironment: textRepresentableEnvironment) { + return symbolWrapper(textRepresentableEnvironment) { + colorWrapper(textRepresentableEnvironment) { SwiftUI.Text(verbatim: symbol.value) } } case .sfSymbol(let systemName): return baselineWrapper { - colorWrapper(textRepresentableEnvironment: textRepresentableEnvironment) { + colorWrapper(textRepresentableEnvironment) { SwiftUI.Text(Image(systemName: systemName)) .font( - sfSymbolFont( - sizeCategory: textRepresentableEnvironment.sizeCategory, - textFontWeight: textRepresentableEnvironment.textFontWeight - ) + sfSymbolFont(textRepresentableEnvironment) ) } } @@ -215,9 +228,9 @@ extension Icon: TextRepresentable { case .none: return nil case .symbol(let symbol): - return symbolWrapper(sizeCategory: textRepresentableEnvironment.sizeCategory) { + return symbolWrapper(textRepresentableEnvironment) { baselineWrapper { - colorWrapper(textRepresentableEnvironment: textRepresentableEnvironment) { + colorWrapper(textRepresentableEnvironment) { SwiftUI.Text(verbatim: symbol.value) } } @@ -228,16 +241,15 @@ extension Icon: TextRepresentable { } } - private func colorWrapper(textRepresentableEnvironment: TextRepresentableEnvironment, @ViewBuilder text: () -> SwiftUI.Text) -> SwiftUI.Text { + private func colorWrapper(_ textRepresentableEnvironment: TextRepresentableEnvironment, @ViewBuilder text: () -> SwiftUI.Text) -> SwiftUI.Text { text() - .foregroundColor(resolvedColor(textRepresentableEnvironment: textRepresentableEnvironment)) + .foregroundColor(resolvedColor(textRepresentableEnvironment)) } - private func symbolWrapper(sizeCategory: ContentSizeCategory, @ViewBuilder text: () -> SwiftUI.Text) -> SwiftUI.Text { - imageBaselineWrapper(sizeCategory: sizeCategory) { - text() - .font(.orbitIcon(size: size.value)) - } + private func symbolWrapper(_ textRepresentableEnvironment: TextRepresentableEnvironment, @ViewBuilder text: () -> SwiftUI.Text) -> SwiftUI.Text { + text() + .font(.orbitIcon(size: resolvedSize(textRepresentableEnvironment))) + .baselineOffset(textBaselineOffset(textRepresentableEnvironment)) } private func baselineWrapper(@ViewBuilder text: () -> SwiftUI.Text) -> SwiftUI.Text { @@ -249,17 +261,10 @@ extension Icon: TextRepresentable { } } - private func resolvedColor(textRepresentableEnvironment: TextRepresentableEnvironment) -> Color { - color ?? textRepresentableEnvironment.iconColor ?? textRepresentableEnvironment.textColor ?? .inkDark - } - - private func imageBaselineWrapper(sizeCategory: ContentSizeCategory, @ViewBuilder text: () -> SwiftUI.Text) -> SwiftUI.Text { - text() - .baselineOffset(textBaselineOffset(baselineOffset, sizeCategory: sizeCategory)) - } - - private func textBaselineOffset(_ baselineOffset: CGFloat?, sizeCategory: ContentSizeCategory) -> CGFloat { - (-size.value * sizeCategory.ratio) * (1 - Self.symbolBaseline) + resolvedBaselineOffset + private func textBaselineOffset(_ textRepresentableEnvironment: TextRepresentableEnvironment) -> CGFloat { + (-resolvedSize(textRepresentableEnvironment) * sizeCategory.ratio) + * (1 - Self.symbolBaseline) + + resolvedBaselineOffset } } @@ -343,7 +348,6 @@ struct IconPreviews: PreviewProvider { textStack(.normal, alignment: .firstTextBaseline) textStack(.large, alignment: .firstTextBaseline) textStack(.xLarge, alignment: .firstTextBaseline) - textStack(.custom(30), alignment: .firstTextBaseline) } VStack(alignment: .leading, spacing: .small) { @@ -351,7 +355,6 @@ struct IconPreviews: PreviewProvider { textStack(.normal, alignment: .top) textStack(.large, alignment: .top) textStack(.xLarge, alignment: .top) - textStack(.custom(30), alignment: .top) } } .padding(.medium) @@ -388,19 +391,20 @@ struct IconPreviews: PreviewProvider { HStack(alignment: .firstTextBaseline, spacing: 0) { Group { - Text("Text", size: .small) - Icon(sfSymbol, size: .small) + Text("Text") + Icon(sfSymbol) .iconColor(.blueNormal) - Icon(sfSymbol, size: .small) + Icon(sfSymbol) .baselineOffset(.xxxSmall) - Icon(.informationCircle, size: .small) + Icon(.informationCircle) .iconColor(.blueNormal) - Icon(.informationCircle, size: .small) + Icon(.informationCircle) .baselineOffset(.xxxSmall) } .border(.cloudLightActive, width: .hairline) } + .textSize(.small) .textColor(.greenDark) .overlay( Separator(color: .redNormal, thickness: .hairline), @@ -411,17 +415,18 @@ struct IconPreviews: PreviewProvider { .padding(.top, .xLarge) ( - Text("Text", size: .small) - + Icon(sfSymbol, size: .small) + Text("Text") + + Icon(sfSymbol) .iconColor(.blueNormal) - + Icon(sfSymbol, size: .small) + + Icon(sfSymbol) .baselineOffset(.xxxSmall) - + Icon(.informationCircle, size: .small) + + Icon(.informationCircle) .iconColor(.blueNormal) - + Icon(.informationCircle, size: .small) + + Icon(.informationCircle) .baselineOffset(.xxxSmall) ) + .textSize(.small) .textColor(.greenDark) .overlay( Separator(color: .redNormal, thickness: .hairline), @@ -463,7 +468,8 @@ struct IconPreviews: PreviewProvider { VStack(alignment: .trailing) { ForEach(flippableSymbols, id: \.value) { symbol in HStack { - Text(String(describing: symbol), size: .small) + Text(String(describing: symbol)) + .textSize(.small) Icon(symbol) Icon(symbol) .environment(\.layoutDirection, .rightToLeft) @@ -481,8 +487,10 @@ struct IconPreviews: PreviewProvider { .bold() HStack(alignment: .firstTextBaseline, spacing: .xxSmall) { - Icon(.passengers, size: iconSize) - Text("XLarge text and icon size", size: textSize) + Icon(.passengers) + .iconSize(iconSize) + Text("\(String(describing: textSize).titleCased) text and icon size") + .textSize(textSize) } .overlay(Separator(thickness: .hairline), alignment: .top) .overlay(Separator(thickness: .hairline), alignment: .bottom) @@ -490,23 +498,25 @@ struct IconPreviews: PreviewProvider { } static func headingStack(_ style: Heading.Style, alignment: VerticalAlignment) -> some View { - alignmentStack(style.iconSize, alignment: alignment) { + alignmentStack(alignment: alignment) { Heading("\(style)".capitalized, style: style) } + .iconSize(custom: style.lineHeight) } static func textStack(_ size: Text.Size, alignment: VerticalAlignment) -> some View { - alignmentStack(size.iconSize, alignment: alignment) { - Text("Text \(Int(size.value))", size: size) + alignmentStack(alignment: alignment) { + Text("\(String(describing: size).titleCased)") } + .textSize(size) } - static func alignmentStack(_ size: Icon.Size, alignment: VerticalAlignment, @ViewBuilder content: () -> V) -> some View { + static func alignmentStack(alignment: VerticalAlignment, @ViewBuilder content: () -> V) -> some View { HStack(spacing: .xSmall) { HStack(alignment: alignment, spacing: .xxSmall) { Group { - Icon(sfSymbol, size: size) - Icon(.informationCircle, size: size) + Icon(sfSymbol) + Icon(.informationCircle) content() } .background(Color.redLightHover) diff --git a/Sources/Orbit/Components/Illustration.swift b/Sources/Orbit/Components/Illustration.swift index 1f486f3fd43..85d6a42e7a0 100644 --- a/Sources/Orbit/Components/Illustration.swift +++ b/Sources/Orbit/Components/Illustration.swift @@ -149,25 +149,25 @@ struct IllustrationPreviews: PreviewProvider { Card("MaxHeight = 80", showBorder: false) { VStack { - Text("Frame - Center (default)", size: .small) + Text("Frame - Center (default)") Illustration(.womanWithPhone, layout: .frame(maxHeight: 80)) .border(.cloudNormal) } VStack { - Text("Frame - Leading", size: .small) + Text("Frame - Leading") Illustration(.womanWithPhone, layout: .frame(maxHeight: 80, alignment: .leading)) .border(.cloudNormal) } VStack { - Text("Frame - Trailing", size: .small) + Text("Frame - Trailing") Illustration(.womanWithPhone, layout: .frame(maxHeight: 80, alignment: .trailing)) .border(.cloudNormal) } VStack { - Text("Resizeable", size: .small) + Text("Resizeable") Illustration(.womanWithPhone, layout: .resizeable) .frame(height: 80) .border(.cloudNormal) @@ -177,26 +177,26 @@ struct IllustrationPreviews: PreviewProvider { Card("MaxHeight = 30", showBorder: false) { HStack { VStack(alignment: .leading) { - Text("Leading", size: .small) + Text("Leading") Illustration(.womanWithPhone, layout: .frame(maxHeight: 30, alignment: .leading)) .border(.cloudNormal) } VStack(alignment: .leading) { - Text("Center", size: .small) + Text("Center") Illustration(.womanWithPhone, layout: .frame(maxHeight: 30)) .border(.cloudNormal) } VStack(alignment: .leading) { - Text("Resizeable", size: .small) + Text("Resizeable") Illustration(.womanWithPhone, layout: .resizeable) .frame(height: 30) .border(.cloudNormal) } VStack(alignment: .leading) { - Text("Trailing", size: .small) + Text("Trailing") Illustration(.womanWithPhone, layout: .frame(maxHeight: 30, alignment: .trailing)) .border(.cloudNormal) } @@ -206,14 +206,14 @@ struct IllustrationPreviews: PreviewProvider { Card("Resizeable", showBorder: false) { HStack(alignment: .top, spacing: .medium) { VStack(alignment: .leading) { - Text("Width = 80", size: .small) + Text("Width = 80") Illustration(.womanWithPhone, layout: .resizeable) .frame(width: 80) .border(.cloudNormal) } VStack(alignment: .leading) { - Text("Height = 80", size: .small) + Text("Height = 80") Illustration(.womanWithPhone, layout: .resizeable) .frame(height: 80) .border(.cloudNormal) @@ -221,13 +221,14 @@ struct IllustrationPreviews: PreviewProvider { } VStack(alignment: .leading) { - Text("Width = 80, Height = 80", size: .small) + Text("Width = 80, Height = 80") Illustration(.womanWithPhone, layout: .resizeable) .frame(width: 80, height: 80) .border(.cloudNormal) } } } + .textSize(.small) .previewDisplayName() } } diff --git a/Sources/Orbit/Components/KeyValue.swift b/Sources/Orbit/Components/KeyValue.swift index 7d2be6d36e0..61e54b5523e 100644 --- a/Sources/Orbit/Components/KeyValue.swift +++ b/Sources/Orbit/Components/KeyValue.swift @@ -12,8 +12,10 @@ public struct KeyValue: View { public var body: some View { KeyValueField(key, size: size, alignment: alignment) { - Text(value, size: size.valueSize, isSelectable: true) + Text(value) + .textSize(size.valueSize) .fontWeight(.medium) + .textIsCopyable() .multilineTextAlignment(.init(alignment)) } .accessibilityElement(children: .ignore) diff --git a/Sources/Orbit/Components/List.swift b/Sources/Orbit/Components/List.swift index 643aa4fc053..69bd143aa0d 100644 --- a/Sources/Orbit/Components/List.swift +++ b/Sources/Orbit/Components/List.swift @@ -43,7 +43,8 @@ struct ListPreviews: PreviewProvider { ListItem(listItemText, icon: .circleSmall) ListItem(listItemText, icon: .circleSmall, type: .secondary) ListItem(listItemText) { - Icon(.grid, size: .small) + Icon(.grid) + .iconSize(.small) } ListItem(listItemText, icon: .grid) ListItem(listItemText, icon: .check) @@ -61,9 +62,10 @@ struct ListPreviews: PreviewProvider { } List { - ListItem(listItemText, size: .large) - ListItem(listItemText, size: .large) + ListItem(listItemText) + ListItem(listItemText) } + .textSize(.large) Separator() @@ -73,9 +75,10 @@ struct ListPreviews: PreviewProvider { } List { - ListItem(listItemText, size: .large, type: .secondary) - ListItem(listItemText, size: .large, type: .secondary) + ListItem(listItemText, type: .secondary) + ListItem(listItemText, type: .secondary) } + .textSize(.large) } .previewDisplayName() } @@ -96,11 +99,16 @@ struct ListPreviews: PreviewProvider { Separator() List { - ListItem(listItemText, size: .small) - ListItem(listItemText, size: .normal) - ListItem(listItemText, size: .large) - ListItem(listItemText, size: .xLarge) - ListItem(listItemText, size: .custom(30)) + ListItem(listItemText) + .textSize(.small) + ListItem(listItemText) + .textSize(.normal) + ListItem(listItemText) + .textSize(.large) + ListItem(listItemText) + .textSize(.xLarge) + ListItem(listItemText) + .textSize(custom: 30) } Separator() diff --git a/Sources/Orbit/Components/ListChoice.swift b/Sources/Orbit/Components/ListChoice.swift index 79fc70e5eec..657199448cf 100644 --- a/Sources/Orbit/Components/ListChoice.swift +++ b/Sources/Orbit/Components/ListChoice.swift @@ -49,7 +49,7 @@ public struct ListChoice: View { content } - TextStrut(.normal) + TextStrut() .padding(.vertical, verticalPadding) disclosureView @@ -91,7 +91,8 @@ public struct ListChoice: View { .fontWeight(.medium) .accessibility(.listChoiceTitle) - Text(description, size: .small) + Text(description) + .textSize(.small) .textColor(.inkNormal) .accessibility(.listChoiceDescription) } diff --git a/Sources/Orbit/Components/NotificationBadge.swift b/Sources/Orbit/Components/NotificationBadge.swift index 0482797d831..088c79a48d7 100644 --- a/Sources/Orbit/Components/NotificationBadge.swift +++ b/Sources/Orbit/Components/NotificationBadge.swift @@ -88,7 +88,8 @@ public extension NotificationBadge { style: BadgeStyle = .status(nil) ) where Content == Text { self.init(style: style) { - Text(label, size: .small) + Text(label) + .textSize(.small) .fontWeight(.medium) } } @@ -102,7 +103,8 @@ public extension NotificationBadge { style: BadgeStyle = .status(nil) ) where Content == Icon { self.init(style: style) { - Icon(icon, size: .small) + Icon(icon) + .iconSize(.small) } } } diff --git a/Sources/Orbit/Components/Previews/TextFormattingPreviews.swift b/Sources/Orbit/Components/Previews/TextFormattingPreviews.swift index 34d11bcbfc9..352f65edd26 100644 --- a/Sources/Orbit/Components/Previews/TextFormattingPreviews.swift +++ b/Sources/Orbit/Components/Previews/TextFormattingPreviews.swift @@ -521,7 +521,8 @@ struct TextFormattingPreviews: PreviewProvider { } func text(_ content: String) -> Text { - Text(content, size: .custom(12)) + Text(content) + .textSize(custom: 12) } func plus() -> Text { @@ -548,7 +549,8 @@ private extension View { @available(iOS 16.0, *) func warning() -> some View { overlay( - Icon(.alert, size: .custom(8)) + Icon(.alert) + .iconSize(custom: 8) .textColor(.redNormal) .underline(false) .baselineOffset(0) diff --git a/Sources/Orbit/Components/Radio.swift b/Sources/Orbit/Components/Radio.swift index 901a98a1894..9341c55b76c 100644 --- a/Sources/Orbit/Components/Radio.swift +++ b/Sources/Orbit/Components/Radio.swift @@ -33,7 +33,8 @@ public struct Radio: View { .fontWeight(.medium) .accessibility(.radioTitle) - Text(description, size: .small) + Text(description) + .textSize(.small) .textColor(descriptionColor) .accessibility(.radioDescription) } diff --git a/Sources/Orbit/Components/Separator.swift b/Sources/Orbit/Components/Separator.swift index 4e7be82b7fa..aeb8d0b8df2 100644 --- a/Sources/Orbit/Components/Separator.swift +++ b/Sources/Orbit/Components/Separator.swift @@ -18,7 +18,8 @@ public struct Separator: View { HStack(spacing: .xxxSmall) { leadingLine - Text(label, size: .small) + Text(label) + .textSize(.small) .textColor(textColor ?? .inkNormal) .fontWeight(.medium) .multilineTextAlignment(.center) diff --git a/Sources/Orbit/Components/Slider.swift b/Sources/Orbit/Components/Slider.swift index 30e7b79aa64..44ffa85ecd0 100644 --- a/Sources/Orbit/Components/Slider.swift +++ b/Sources/Orbit/Components/Slider.swift @@ -367,7 +367,9 @@ struct SliderPreviews: PreviewProvider { VStack(spacing: .small) { StateWrapper(30.0) { value in VStack(alignment: .leading, spacing: .medium) { - Text("Slider", size: .xLarge) + Text("Slider") + .textSize(.xLarge) + Slider( value: value, valueLabel: value.wrappedValue.description, @@ -380,7 +382,9 @@ struct SliderPreviews: PreviewProvider { StateWrapper(5.0) { value in VStack(alignment: .leading, spacing: .medium) { - Text("Stepped Slider", size: .xLarge) + Text("Stepped Slider") + .textSize(.xLarge) + Slider( value: value, valueLabel: value.wrappedValue.description, @@ -394,7 +398,9 @@ struct SliderPreviews: PreviewProvider { StateWrapper(30.0...70.0) { selectedRange in VStack(alignment: .leading, spacing: .medium) { - Text("Range Slider", size: .xLarge) + Text("Range Slider") + .textSize(.xLarge) + Slider( valueRange: selectedRange, valueLabel: "\(selectedRange.wrappedValue.lowerBound.formatted) - \(selectedRange.wrappedValue.upperBound.formatted)", @@ -407,7 +413,9 @@ struct SliderPreviews: PreviewProvider { StateWrapper(30.0...70.0) { selectedRange in VStack(alignment: .leading, spacing: .medium) { - Text("Stepped Range Slider", size: .xLarge) + Text("Stepped Range Slider") + .textSize(.xLarge) + Slider( valueRange: selectedRange, valueLabel: "\(selectedRange.wrappedValue.lowerBound.formatted) - \(selectedRange.wrappedValue.upperBound.formatted)", @@ -421,7 +429,8 @@ struct SliderPreviews: PreviewProvider { StateWrapper(30.0) { value in VStack(alignment: .leading, spacing: .small) { - Text("SwiftUI.Slider", size: .xLarge) + Text("SwiftUI.Slider") + .textSize(.xLarge) SwiftUI.Slider(value: value, in: 0.0...100.0) diff --git a/Sources/Orbit/Components/SocialButton.swift b/Sources/Orbit/Components/SocialButton.swift index 824146fa886..3361bc4e1ca 100644 --- a/Sources/Orbit/Components/SocialButton.swift +++ b/Sources/Orbit/Components/SocialButton.swift @@ -34,7 +34,7 @@ public struct SocialButton: View { .scaledToFit() .frame(width: .large * sizeCategory.ratio) - Text(label, size: .normal) + Text(label) .fontWeight(.medium) .padding(.vertical, ButtonSize.default.verticalPadding) @@ -42,7 +42,8 @@ public struct SocialButton: View { Spacer(minLength: 0) } - Icon(.chevronForward, size: .large) + Icon(.chevronForward) + .iconSize(.large) .iconColor(labelColor) } .textColor(textColor ?? labelColor) @@ -56,7 +57,7 @@ public struct SocialButton: View { case .apple: Self.appleLogo.foregroundColor(.whiteNormal).padding(1) case .google: Self.googleLogo case .facebook: Self.facebookLogo - case .email: Icon(.email, size: .large) + case .email: Icon(.email).iconSize(.large) } } diff --git a/Sources/Orbit/Components/Switch.swift b/Sources/Orbit/Components/Switch.swift index 4cd137ba773..eef10d77f94 100644 --- a/Sources/Orbit/Components/Switch.swift +++ b/Sources/Orbit/Components/Switch.swift @@ -60,12 +60,10 @@ public struct Switch: View { @ViewBuilder var indicatorSymbol: some View { if hasIcon { - Icon( - isOn ? .lock : .lockOpen, - size: .custom(Icon.Size.small.value * sizeCategory.controlRatio) - ) - .iconColor(iconTint) - .environment(\.sizeCategory, .large) + Icon(isOn ? .lock : .lockOpen) + .iconSize(.small) + .iconColor(iconTint) + .environment(\.sizeCategory, .large) } else { Circle() .foregroundColor(tint) diff --git a/Sources/Orbit/Components/Tag.swift b/Sources/Orbit/Components/Tag.swift index ca96bddd342..ffb6815dbc4 100644 --- a/Sources/Orbit/Components/Tag.swift +++ b/Sources/Orbit/Components/Tag.swift @@ -35,7 +35,7 @@ public struct Tag: View { HStack(spacing: 6) { icon - .font(.system(size: Text.Size.normal.iconSize.value)) + .font(.system(size: Text.Size.normal.value)) .foregroundColor(textColor ?? labelColor) Text(label) @@ -111,7 +111,8 @@ public extension Tag { isActive: isActive, isSelected: isSelected ) { - Icon(icon, size: Text.Size.normal.iconSize) + Icon(icon) + .iconSize(textSize: .normal) } } } @@ -148,7 +149,8 @@ public struct TagButtonStyle: ButtonStyle { .lineLimit(1) if case .removable(let removeAction) = style { - Orbit.Icon(.closeCircle, size: .small) + Icon(.closeCircle) + .iconSize(.small) .iconColor(iconColor(isPressed: configuration.isPressed)) .onTapGesture(perform: removeAction) .accessibility(addTraits: .isButton) diff --git a/Sources/Orbit/Components/Text.swift b/Sources/Orbit/Components/Text.swift index e4806e6996b..5b352f4f4e8 100644 --- a/Sources/Orbit/Components/Text.swift +++ b/Sources/Orbit/Components/Text.swift @@ -14,13 +14,15 @@ public struct Text: View, FormattedTextBuildable { @Environment(\.textAccentColor) private var textAccentColor @Environment(\.textColor) private var textColor @Environment(\.textFontWeight) private var textFontWeight + @Environment(\.textIsCopyable) private var textIsCopyable + @Environment(\.textLineHeight) private var textLineHeight + @Environment(\.textSize) private var textSize @Environment(\.sizeCategory) private var sizeCategory private let content: String - private let size: Size - private let isSelectable: Bool // Builder properties + var size: CGFloat? var baselineOffset: CGFloat? var fontWeight: Font.Weight? var color: Color? @@ -31,6 +33,7 @@ public struct Text: View, FormattedTextBuildable { var isItalic: Bool? var isUnderline: Bool? var isMonospacedDigit: Bool? + var lineHeight: CGFloat? var isEmpty: Bool { content.isEmpty @@ -48,11 +51,11 @@ public struct Text: View, FormattedTextBuildable { if isEmpty == false { text(textRepresentableEnvironment: textRepresentableEnvironment) .lineSpacing(lineSpacingAdjusted(sizeCategory: sizeCategory)) - .overlay(selectableLabelWrapper) + .overlay(copyableText) // If the text contains links, the TextLink overlay takes accessibility priority .accessibility(hidden: content.containsTextLinks) .overlay(textLinks) - .padding(.vertical, lineHeightPadding) + .padding(.vertical, lineHeightPadding(textRepresentableEnvironment)) .fixedSize(horizontal: false, vertical: true) } } @@ -63,9 +66,9 @@ public struct Text: View, FormattedTextBuildable { } } - @ViewBuilder private var selectableLabelWrapper: some View { - if isSelectable { - SelectableLabelWrapper( + @ViewBuilder private var copyableText: some View { + if textIsCopyable { + CopyableText( attributedString(textRepresentableEnvironment: textRepresentableEnvironment).string ) } @@ -91,7 +94,7 @@ public struct Text: View, FormattedTextBuildable { ) ) .orbitFont( - size: size.value, + size: resolvedSize(textRepresentableEnvironment), weight: resolvedFontWeight(textRepresentableEnvironment) ?? .regular, sizeCategory: textRepresentableEnvironment.sizeCategory ) @@ -101,10 +104,12 @@ public struct Text: View, FormattedTextBuildable { private var textRepresentableEnvironment: TextRepresentableEnvironment { .init( iconColor: nil, - sizeCategory: sizeCategory, + iconSize: nil, textAccentColor: textAccentColor, textColor: textColor, - textFontWeight: textFontWeight + textFontWeight: textFontWeight, + textSize: textSize, + sizeCategory: sizeCategory ) } @@ -212,7 +217,7 @@ public struct Text: View, FormattedTextBuildable { TagAttributedStringBuilder.all.attributedString( content, alignment: multilineTextAlignment, - fontSize: scaledSize, + fontSize: scaledSize(textRepresentableEnvironment), fontWeight: resolvedFontWeight(textRepresentableEnvironment), lineSpacing: lineSpacingAdjusted(sizeCategory: sizeCategory), kerning: kerning, @@ -229,7 +234,7 @@ public struct Text: View, FormattedTextBuildable { TagAttributedStringBuilder.all.attributedString( content, alignment: multilineTextAlignment, - fontSize: size.value * textRepresentableEnvironment.sizeCategory.ratio, + fontSize: resolvedSize(textRepresentableEnvironment) * textRepresentableEnvironment.sizeCategory.ratio, fontWeight: resolvedFontWeight(textRepresentableEnvironment), lineSpacing: lineSpacingAdjusted(sizeCategory: textRepresentableEnvironment.sizeCategory), kerning: kerning, @@ -249,28 +254,35 @@ public struct Text: View, FormattedTextBuildable { color ?? textRepresentableEnvironment.textColor ?? .inkDark } + private func resolvedSize(_ textRepresentableEnvironment: TextRepresentableEnvironment) -> CGFloat { + (size ?? textRepresentableEnvironment.textSize ?? Size.normal.value) + } + private func resolvedFontWeight(_ textRepresentableEnvironment: TextRepresentableEnvironment) -> Font.Weight? { isBold == true ? .bold : fontWeight ?? textRepresentableEnvironment.textFontWeight } - private func designatedLineHeight(sizeCategory: ContentSizeCategory) -> CGFloat { - size.lineHeight * sizeCategory.ratio + private func designatedLineHeight(_ textRepresentableEnvironment: TextRepresentableEnvironment) -> CGFloat { + (lineHeight ?? textLineHeight ?? (Text.Size.lineHeight(forTextSize: resolvedSize(textRepresentableEnvironment)))) + * textRepresentableEnvironment.sizeCategory.ratio } - private var lineHeightPadding: CGFloat { - (designatedLineHeight(sizeCategory: sizeCategory) - originalLineHeight) / 2 + private func lineHeightPadding(_ textRepresentableEnvironment: TextRepresentableEnvironment) -> CGFloat { + (designatedLineHeight(textRepresentableEnvironment) - originalLineHeight(textRepresentableEnvironment)) / 2 } private func lineSpacingAdjusted(sizeCategory: ContentSizeCategory) -> CGFloat { - (designatedLineHeight(sizeCategory: sizeCategory) - originalLineHeight) + lineSpacing + designatedLineHeight(textRepresentableEnvironment) + - originalLineHeight(textRepresentableEnvironment) + + lineSpacing } - private var originalLineHeight: CGFloat { - UIFont.lineHeight(size: scaledSize) + private func originalLineHeight(_ textRepresentableEnvironment: TextRepresentableEnvironment) -> CGFloat { + UIFont.lineHeight(size: scaledSize(textRepresentableEnvironment)) } - private var scaledSize: CGFloat { - size.value * sizeCategory.ratio + private func scaledSize(_ textRepresentableEnvironment: TextRepresentableEnvironment) -> CGFloat { + resolvedSize(textRepresentableEnvironment) * textRepresentableEnvironment.sizeCategory.ratio } } @@ -279,32 +291,26 @@ public extension Text { /// Creates Orbit Text component that displays a string literal. /// - /// Modifiers like `textAccentColor()` or `fontWeight()` can be used to further adjust the formatting. + /// Modifiers like `textAccentColor()`, `textSize()` or `fontWeight()` can be used to further adjust the formatting. /// To specify the formatting and behaviour for `TextLink`s found in the text, use `textLinkAction()` and /// `textLinkColor()` modifiers. /// + /// Use `textIsCopyable()` to allow text to react on long tap. + /// /// - Parameters: /// - content: String to display. Supports html formatting tags ``, ``, ``, `` and ``. - /// - size: Font size. - /// - isSelectable: Determines if text is copyable using long tap gesture. /// /// - Important: The text concatenation does not support interactive TextLinks. It also does not fully support some global SwiftUI native text formatting view modifiers. /// For proper rendering of TextLinks, any required text formatting modifiers must be also called on the Text directly. - init( - _ content: String, - size: Size = .normal, - isSelectable: Bool = false - ) { + init(_ content: String) { self.content = content - self.size = size - self.isSelectable = isSelectable } } // MARK: - Types public extension Text { - /// Orbit text size. + /// Orbit text font size. enum Size: Equatable { /// 13 pts. case small @@ -314,35 +320,36 @@ public extension Text { case large /// 18 pts. case xLarge - /// Custom text size. - case custom(CGFloat, lineHeight: CGFloat? = nil) - /// Font size. + public static func lineHeight(forTextSize size: CGFloat) -> CGFloat { + switch size { + case 13: return 16 + case 15: return 20 + case 16: return 24 + case 18: return 24 + default: return size * Font.fontSizeToLineHeightRatio + } + } + + /// Text font size value. public var value: CGFloat { switch self { case .small: return 13 case .normal: return 15 case .large: return 16 case .xLarge: return 18 - case .custom(let size, _): return size } } - + + /// Designated line height. public var lineHeight: CGFloat { switch self { case .small: return 16 case .normal: return 20 case .large: return 24 case .xLarge: return 24 - case .custom(_, let lineHeight?): return lineHeight - case .custom(let size, nil): return size * Font.fontSizeToLineHeightRatio } } - - /// Icon size matching text line height. - public var iconSize: Icon.Size { - .custom(lineHeight) - } } } @@ -404,7 +411,8 @@ struct TextPreviews: PreviewProvider { @ViewBuilder static var standalone: some View { VStack(alignment: .trailing, spacing: .medium) { Text("Multiline text\nplain with with no formatting") - Text("Multiline text\nselectable (with long tap gesture)", isSelectable: true) + Text("Multiline text\nselectable (with long tap gesture)") + .textIsCopyable() Text( """ @@ -463,7 +471,8 @@ Multiline text underlined, strong, static var multilineFormatting: some View { VStack(alignment: .leading, spacing: .medium) { Group { - Text("Selectable text (on long tap)", isSelectable: true) + Text("Selectable text (on long tap)") + .textIsCopyable() Text("Text with no formatting") Text("Text formatted and accented") .textAccentColor(.orangeNormal) @@ -505,9 +514,9 @@ Multiline text underlined, strong, // This text may reveal issues between iOS TextLink word wrapping Text( - "By continuing, you accept the Terms Of Use and Privacy Policy.", - size: .small + "By continuing, you accept the Terms Of Use and Privacy Policy." ) + .textSize(.small) .textColor(.inkNormal) .textLinkColor(.secondary) .multilineTextAlignment(.leading) @@ -542,14 +551,12 @@ Multiline text underlined, strong, static var lineHeight: some View { VStack(alignment: .trailing, spacing: .medium) { VStack(alignment: .trailing, spacing: .xxxSmall) { - LineHeight(size: .custom(8), formatted: false) LineHeight(size: .small, formatted: false) LineHeight(size: .normal, formatted: false) LineHeight(size: .large, formatted: false) LineHeight(size: .xLarge, formatted: false) } VStack(alignment: .trailing, spacing: .xxxSmall) { - LineHeight(size: .custom(8), formatted: true) LineHeight(size: .small, formatted: true) LineHeight(size: .normal, formatted: true) LineHeight(size: .large, formatted: true) @@ -564,25 +571,26 @@ Multiline text underlined, strong, HStack(alignment: .top, spacing: .xxxSmall) { VStack(alignment: .trailing, spacing: .xxxSmall) { Group { - Text("Text single line", size: .large) + Text("Text single line") .background(Color.redLightHover) - Text("Text single line", size: .large) + Text("Text single line") .background(Color.redLightHover.opacity(0.7)) - Text("Text single line", size: .large) + Text("Text single line") .background(Color.redLightHover.opacity(0.4)) } .overlay(Separator(color: .redNormal, thickness: .hairline), alignment: .centerFirstTextBaseline) } Group { - Text("Multliline\nwith\nformatting", size: .large) - Text("Multliline\nwith\nlinks", size: .large) + Text("Multliline\nwith\nformatting") + Text("Multliline\nwith\nlinks") } .lineSpacing(.xxxSmall) .background(Color.redLightHover.opacity(0.7)) .overlay(Separator(color: .redNormal, thickness: .hairline), alignment: .centerFirstTextBaseline) .overlay(Separator(color: .redNormal, thickness: .hairline), alignment: .centerLastTextBaseline) } + .textSize(.large) .padding(.medium) .previewDisplayName() } @@ -612,9 +620,9 @@ Multiline text underlined, strong, An attributed text that can contain multiple \ HTML links with \ underline and strong support. - """, - size: .custom(12) + """ ) + .textSize(custom: 12) .textColor(.blueLightActive) .bold() .textAccentColor(.blueNormal) @@ -645,9 +653,9 @@ Multiline text underlined, strong, An attributed text that can contain multiple \ HTML links with \ underline and strong support. - """, - size: .custom(22) + """ ) + .textSize(custom: 22) .textColor(.inkNormal) .bold() .lineLimit(2) @@ -667,15 +675,14 @@ Multiline text underlined, strong, var body: some View { HStack(spacing: .xxSmall) { - Text( - "\(sizeText) \(formatted ? "" : "")\(size)\(formatted ? "" : "")", - size: size - ) - .textAccentColor(Status.info.darkColor) - .fixedSize() - .overlay(Separator(color: .redNormal, thickness: .hairline), alignment: .centerLastTextBaseline) - - Text("\(size.value.formatted) / \(size.lineHeight.formatted)", size: .custom(6)) + Text("\(sizeText) \(formatted ? "" : "")\(size)\(formatted ? "" : "")") + .textSize(size) + .textAccentColor(Status.info.darkColor) + .fixedSize() + .overlay(Separator(color: .redNormal, thickness: .hairline), alignment: .centerLastTextBaseline) + + Text("\(size.value.formatted) / \(size.lineHeight.formatted)") + .textSize(custom: 6) .environment(\.sizeCategory, .large) } .padding(.trailing, .xxSmall) @@ -701,7 +708,8 @@ Multiline text underlined, strong, static func text(_ content: String, size: Text.Size, weight: Font.Weight) -> some View { HStack(alignment: .firstTextBaseline, spacing: .small) { - Text(content, size: size) + Text(content) + .textSize(size) .fontWeight(weight) Spacer() Text("\(Int(size.value))/\(Int(size.lineHeight))") diff --git a/Sources/Orbit/Components/Tile.swift b/Sources/Orbit/Components/Tile.swift index 138bff1aefd..98adfaa9d29 100644 --- a/Sources/Orbit/Components/Tile.swift +++ b/Sources/Orbit/Components/Tile.swift @@ -56,7 +56,8 @@ public struct Tile: View { Spacer(minLength: 0) } - TextStrut(.large) + TextStrut() + .textSize(.large) .padding(.vertical, TileButtonStyle.verticalTextPadding) disclosureIcon @@ -70,7 +71,7 @@ public struct Tile: View { if isHeaderEmpty == false { HStack(alignment: .top, spacing: 0) { icon - .font(.system(size: titleStyle.iconSize.value)) + .font(.system(size: Orbit.Icon.Size.fromTextSize(size: titleStyle.size))) .foregroundColor(.inkNormal) .padding(.trailing, .xSmall) .accessibility(.tileIcon) @@ -176,7 +177,7 @@ public extension Tile { } content: { content() } icon: { - Icon(icon, size: titleStyle.iconSize) + Icon(icon) } } diff --git a/Sources/Orbit/Support/Components/BarButton.swift b/Sources/Orbit/Support/Components/BarButton.swift index 22cd6e2d7e3..62da5f04317 100644 --- a/Sources/Orbit/Support/Components/BarButton.swift +++ b/Sources/Orbit/Support/Components/BarButton.swift @@ -3,9 +3,9 @@ import SwiftUI /// An icon-based bar button for suitable for actions inside toolbar or navigation bar. public struct BarButton: View { + @Environment(\.iconSize) private var iconSize @Environment(\.isHapticsEnabled) private var isHapticsEnabled - private let size: Orbit.Icon.Size private let alignment: HorizontalAlignment private let action: () -> Void @ViewBuilder private let icon: Icon @@ -19,6 +19,9 @@ public struct BarButton: View { action() } label: { icon + .iconSize(custom: resolvedIconSize) + .font(.system(size: resolvedIconSize)) + .foregroundColor(.inkDark) .padding(.vertical, .xSmall) .padding(horizontalEdges, .xSmall) .contentShape(Rectangle()) @@ -26,6 +29,10 @@ public struct BarButton: View { .buttonStyle(IconButtonStyle()) } + var resolvedIconSize: CGFloat { + iconSize ?? Orbit.Icon.Size.large.value + } + var horizontalEdges: Edge.Set { switch alignment { case .leading: return .trailing @@ -37,28 +44,22 @@ public struct BarButton: View { /// Creates Orbit BarButton component. public init( _ icon: Icon.Symbol, - size: Icon.Size = .large, alignment: HorizontalAlignment = .center, action: @escaping () -> Void ) where Icon == Orbit.Icon { - self.init( - size: size, - alignment: alignment - ) { + self.init(alignment: alignment) { action() } icon: { - Icon(icon, size: size) + Icon(icon) } } /// Creates Orbit BarButton component with custom icon. public init( - size: Orbit.Icon.Size = .large, alignment: HorizontalAlignment = .center, action: @escaping () -> Void, @ViewBuilder icon: () -> Icon ) { - self.size = size self.alignment = alignment self.action = action self.icon = icon() @@ -71,6 +72,7 @@ struct BarButtonPreviews: PreviewProvider { static var previews: some View { PreviewWrapper { standalone + sizing navigationView } .previewLayout(.sizeThatFits) @@ -83,6 +85,37 @@ struct BarButtonPreviews: PreviewProvider { .previewDisplayName() } + static var sizing: some View { + VStack(alignment: .leading, spacing: .xSmall) { + Group { + BarButton(.grid, action: {}) + + BarButton(.grid, action: {}) + .iconSize(.small) + BarButton(action: {}) { + Icon(.grid) + .iconSize(.small) + } + + BarButton(action: {}) { + Icon("questionmark.circle.fill") + } + BarButton(action: {}) { + Icon("questionmark.circle.fill") + .iconSize(.small) + } + BarButton(action: {}) { + Icon("questionmark.circle.fill") + } + .iconSize(.small) + } + .background(Color.redLight) + .measured() + + } + .previewDisplayName() + } + static var navigationView: some View { NavigationView { Color.cloudNormal @@ -97,12 +130,13 @@ struct BarButtonPreviews: PreviewProvider { // No action } icon: { Icon("questionmark.circle.fill") + .iconSize(.normal) .iconColor(.greenDark) } BarButton { // No action } icon: { - CountryFlag("cz", size: .large) + CountryFlag("cz") } } .border(.cloudNormal.opacity(0.4)) @@ -115,6 +149,7 @@ struct BarButtonPreviews: PreviewProvider { // No action } icon: { Icon("square.and.arrow.up") + .iconSize(.normal) .fontWeight(.medium) } diff --git a/Sources/Orbit/Support/Components/IconButton.swift b/Sources/Orbit/Support/Components/IconButton.swift index 609d060190c..59ff069770d 100644 --- a/Sources/Orbit/Support/Components/IconButton.swift +++ b/Sources/Orbit/Support/Components/IconButton.swift @@ -5,7 +5,6 @@ public struct IconButton: View { @Environment(\.isHapticsEnabled) private var isHapticsEnabled - private let size: Orbit.Icon.Size private let action: () -> Void @ViewBuilder private let icon: Icon @@ -18,7 +17,7 @@ public struct IconButton: View { action() } label: { icon - .font(.system(size: size.value)) + .font(.system(size: Orbit.Icon.Size.normal.value)) .foregroundColor(.inkDark) .contentShape(Rectangle()) } @@ -27,11 +26,9 @@ public struct IconButton: View { /// Creates Orbit IconButton component with custom icon. public init( - size: Orbit.Icon.Size = .normal, action: @escaping () -> Void, @ViewBuilder icon: () -> Icon ) { - self.size = size self.action = action self.icon = icon() } @@ -39,15 +36,12 @@ public struct IconButton: View { /// Creates Orbit IconButton component. init( _ icon: Orbit.Icon.Symbol, - size: Orbit.Icon.Size = .normal, action: @escaping () -> Void ) where Icon == Orbit.Icon { - self.init( - size: size - ) { + self.init() { action() } icon: { - Icon(icon, size: size) + Icon(icon) } } } diff --git a/Sources/Orbit/Support/Components/ListItem.swift b/Sources/Orbit/Support/Components/ListItem.swift index cc287411411..63174972854 100644 --- a/Sources/Orbit/Support/Components/ListItem.swift +++ b/Sources/Orbit/Support/Components/ListItem.swift @@ -12,20 +12,19 @@ public struct ListItem: View { @Environment(\.textColor) private var textColor private let text: String - private let size: Text.Size private let type: ListItemType @ViewBuilder private let icon: Icon public var body: some View { HStack(alignment: .firstTextBaseline, spacing: .xSmall) { if icon.isEmpty { - Orbit.Icon(.placeholder, size: size.iconSize) + Orbit.Icon(.placeholder) .opacity(0) } else { icon } - Text(text, size: size) + Text(text) } .textColor(textColor ?? type.textColor) } @@ -38,27 +37,23 @@ public extension ListItem { init( _ text: String = "", icon: Icon.Symbol? = .circleSmall, - size: Text.Size = .normal, type: ListItemType = .primary ) where Icon == Orbit.Icon { self.init( text, - size: size, type: type ) { - Icon(icon, size: size.iconSize) + Icon(icon) } } /// Creates Orbit ListItem component. init( _ text: String = "", - size: Text.Size = .normal, type: ListItemType = .primary, @ViewBuilder icon: () -> Icon ) { self.text = text - self.size = size self.type = type self.icon = icon() } @@ -106,12 +101,16 @@ struct ListItemPreviews: PreviewProvider { static var sizes: some View { List(spacing: .medium) { - ListItem("ListItem - small", size: .small, type: .primary) - ListItem("ListItem - small, secondary", size: .small, type: .secondary) + ListItem("ListItem - small", type: .primary) + .textSize(.small) + ListItem("ListItem - small, secondary", type: .secondary) + .textSize(.small) ListItem("ListItem - normal") - ListItem("ListItem - normal, secondary", size: .normal, type: .secondary) - ListItem("ListItem - large", size: .large, type: .primary) - ListItem("ListItem - large, secondary", size: .large, type: .secondary) + ListItem("ListItem - normal, secondary", type: .secondary) + ListItem("ListItem - large", type: .primary) + .textSize(.large) + ListItem("ListItem - large, secondary", type: .secondary) + .textSize(.large) } .padding(.medium) .previewDisplayName() @@ -119,7 +118,8 @@ struct ListItemPreviews: PreviewProvider { static var links: some View { List { - ListItem(#"ListItem containing TextLink or Two"#, size: .small, type: .secondary) + ListItem(#"ListItem containing TextLink or Two"#, type: .secondary) + .textSize(.small) ListItem(#"ListItem containing TextLink or Two"#) ListItem(#"ListItem containing TextLink or Two"#) .textColor(.greenNormal) @@ -134,8 +134,10 @@ struct ListItemPreviews: PreviewProvider { static var mix: some View { List { - ListItem("ListItem", size: .xLarge) - ListItem("ListItem with custom icon", icon: .check, size: .xLarge) + ListItem("ListItem") + .textSize(.xLarge) + ListItem("ListItem with custom icon", icon: .check) + .textSize(.xLarge) .textColor(.greenNormal) ListItem("ListItem") ListItem("ListItem with custom icon", icon: .check) diff --git a/Sources/Orbit/Support/Components/PasswordStrengthIndicator.swift b/Sources/Orbit/Support/Components/PasswordStrengthIndicator.swift index 67fd92402eb..ff00b1fa8af 100644 --- a/Sources/Orbit/Support/Components/PasswordStrengthIndicator.swift +++ b/Sources/Orbit/Support/Components/PasswordStrengthIndicator.swift @@ -11,7 +11,8 @@ public struct PasswordStrengthIndicator: View { indicator .animation(.easeOut, value: passwordStrength) - Text(text, size: .small) + Text(text) + .textSize(.small) .textColor(color) .frame(maxWidth: .infinity, alignment: .trailing) } diff --git a/Sources/Orbit/Support/Components/TimelineIndicator.swift b/Sources/Orbit/Support/Components/TimelineIndicator.swift index 6a33039327f..eba5348b3cf 100644 --- a/Sources/Orbit/Support/Components/TimelineIndicator.swift +++ b/Sources/Orbit/Support/Components/TimelineIndicator.swift @@ -72,7 +72,8 @@ struct TimelineIndicator: View { } case .present(.warning), .present(.critical), .present(.success), .past: - Icon(type.icon, size: .small) + Icon(type.icon) + .iconSize(.small) .iconColor(type.color) .background(Circle().fill(.whiteNormal).padding(2)) } diff --git a/Sources/Orbit/Support/Components/TimelineItem.swift b/Sources/Orbit/Support/Components/TimelineItem.swift index 14bdd2a83c2..00f6cd62966 100644 --- a/Sources/Orbit/Support/Components/TimelineItem.swift +++ b/Sources/Orbit/Support/Components/TimelineItem.swift @@ -25,7 +25,7 @@ public struct TimelineItem: View { header .textColor(type.textColor) - Text(description, size: .normal) + Text(description) .textColor(type.textColor) footer @@ -50,7 +50,8 @@ public struct TimelineItem: View { @ViewBuilder var headerContent: some View { Heading(label, style: .title5) - Text(sublabel, size: .small) + Text(sublabel) + .textSize(.small) } var alignment: VerticalAlignment { @@ -254,13 +255,11 @@ struct TimelineItemCustomContentPreviews: PreviewProvider { ) { VStack { - Text( - "1 Passenger must check in with the airline for a possible fee", - size: .custom(50) - ) - .textColor(TimelineItemType.present(.warning).color) - .bold() - .padding(.leading, .xSmall) + Text("1 Passenger must check in with the airline for a possible fee") + .textSize(custom: 50) + .textColor(TimelineItemType.present(.warning).color) + .bold() + .padding(.leading, .xSmall) } } TimelineItem( diff --git a/Sources/Orbit/Support/Environment Keys/IconSizeKey.swift b/Sources/Orbit/Support/Environment Keys/IconSizeKey.swift new file mode 100644 index 00000000000..b8a8012abb5 --- /dev/null +++ b/Sources/Orbit/Support/Environment Keys/IconSizeKey.swift @@ -0,0 +1,55 @@ +import SwiftUI + +struct IconSizeKey: EnvironmentKey { + static var defaultValue: CGFloat? = nil +} + +public extension EnvironmentValues { + + /// An icon size stored in a view’s environment, used for Orbit Icon component. + /// + /// This value has a priority over a `textSize`. + var iconSize: CGFloat? { + get { self[IconSizeKey.self] } + set { self[IconSizeKey.self] = newValue } + } +} + +public extension View { + + /// Set the size for any Orbit `Icon` component within the view hierarchy. + /// + /// The `iconSize` value has a priority over the `textSize` environment value that can be used to size Icon to match the text size. + /// + /// - Parameters: + /// - size: A size that will be used in Orbit `Icon` components. + /// Pass `nil` to ignore environment icon size and to allow the system or the container to provide its own size. + /// If a container-specific override doesn’t exist, the `textSize` or default `.normal` size will be used. + func iconSize(_ size: Icon.Size?) -> some View { + environment(\.iconSize, size?.value) + } + + /// Set the size for any Orbit `Icon` component within the view hierarchy. + /// + /// The `iconSize` value has a priority over the `textSize` environment value that can be used to size Icon to match the text size. + /// + /// - Parameters: + /// - size: A size that will be used in Orbit `Icon` components. + /// Pass `nil` to ignore environment icon size and to allow the system or the container to provide its own size. + /// If a container-specific override doesn’t exist, the `textSize` or default `.normal` size will be used. + func iconSize(custom size: CGFloat?) -> some View { + environment(\.iconSize, size) + } + + /// Set the size, matching line height of a provided text size, for any Orbit `Icon` component within the view hierarchy. + /// + /// The `iconSize` value has a priority over the `textSize` environment value that can be used to size Icon to match the text size. + /// + /// - Parameters: + /// - size: A size that will be used in Orbit `Icon` components. + /// Pass `nil` to ignore environment icon size and to allow the system or the container to provide its own size. + /// If a container-specific override doesn’t exist, the `textSize` or default `.normal` size will be used. + func iconSize(textSize: Text.Size?) -> some View { + environment(\.iconSize, textSize?.lineHeight) + } +} diff --git a/Sources/Orbit/Support/Environment Keys/TextAccentColorKey.swift b/Sources/Orbit/Support/Environment Keys/TextAccentColorKey.swift index 4c83b16ef48..0b7b37c4a94 100644 --- a/Sources/Orbit/Support/Environment Keys/TextAccentColorKey.swift +++ b/Sources/Orbit/Support/Environment Keys/TextAccentColorKey.swift @@ -1,7 +1,7 @@ import SwiftUI struct TextAccentColorKey: EnvironmentKey { - static var defaultValue: Color? = nil + static let defaultValue: Color? = nil } public extension EnvironmentValues { diff --git a/Sources/Orbit/Support/Environment Keys/TextColorKey.swift b/Sources/Orbit/Support/Environment Keys/TextColorKey.swift index 05cff6a0170..38bcaa6fe14 100644 --- a/Sources/Orbit/Support/Environment Keys/TextColorKey.swift +++ b/Sources/Orbit/Support/Environment Keys/TextColorKey.swift @@ -1,7 +1,7 @@ import SwiftUI struct TextColorKey: EnvironmentKey { - static var defaultValue: Color? = nil + static let defaultValue: Color? = nil } public extension EnvironmentValues { diff --git a/Sources/Orbit/Support/Environment Keys/TextIsCopyableKey.swift b/Sources/Orbit/Support/Environment Keys/TextIsCopyableKey.swift new file mode 100644 index 00000000000..2b27078af95 --- /dev/null +++ b/Sources/Orbit/Support/Environment Keys/TextIsCopyableKey.swift @@ -0,0 +1,26 @@ +import SwiftUI + +struct TextIsCopyableKey: EnvironmentKey { + static let defaultValue = false +} + +public extension EnvironmentValues { + + /// A text copyable status stored in a view’s environment, used for Orbit text components, such as `Text` or `Heading`. + var textIsCopyable: Bool { + get { self[TextIsCopyableKey.self] } + set { self[TextIsCopyableKey.self] = newValue } + } +} + +public extension View { + + /// Set the copyable status for any text based Orbit component within the view hierarchy. + /// + /// - Parameters: + /// - enabled: A value that will be used to decide whether text based Orbit components + /// such as `Text` or `Heading` can be copied using long tap gesture. + func textIsCopyable(_ enabled: Bool = true) -> some View { + environment(\.textIsCopyable, enabled) + } +} diff --git a/Sources/Orbit/Support/Environment Keys/TextLineHeightKey.swift b/Sources/Orbit/Support/Environment Keys/TextLineHeightKey.swift new file mode 100644 index 00000000000..796e2ce8686 --- /dev/null +++ b/Sources/Orbit/Support/Environment Keys/TextLineHeightKey.swift @@ -0,0 +1,29 @@ +import SwiftUI + +struct TextLineHeightKey: EnvironmentKey { + static let defaultValue: CGFloat? = nil +} + +public extension EnvironmentValues { + + /// A text line height stored in a view’s environment, used for Orbit text based components, such as `Text` or `Heading`. + /// + /// This environment value serves as a replacement for native line height. + var textLineHeight: CGFloat? { + get { self[TextLineHeightKey.self] } + set { self[TextLineHeightKey.self] = newValue } + } +} + +public extension View { + + /// Set the custom line height for any text based Orbit component within the view hierarchy. + /// + /// - Parameters: + /// - height: An overall height that will be used in text based Orbit components such as `Text` or `Heading`. + /// Pass `nil` to ignore environment text size and to allow the system or the container to provide its own height. + /// If a container-specific override doesn’t exist, the line height will be calculated from text size. + func textLineHeight(_ height: CGFloat?) -> some View { + environment(\.textLineHeight, height) + } +} diff --git a/Sources/Orbit/Support/Environment Keys/TextLinkActionKey.swift b/Sources/Orbit/Support/Environment Keys/TextLinkActionKey.swift index 40bbdaa0b9d..c9136845aa2 100644 --- a/Sources/Orbit/Support/Environment Keys/TextLinkActionKey.swift +++ b/Sources/Orbit/Support/Environment Keys/TextLinkActionKey.swift @@ -1,7 +1,7 @@ import SwiftUI struct TextLinkActionKey: EnvironmentKey { - static var defaultValue: TextLink.Action? = nil + static let defaultValue: TextLink.Action? = nil } public extension EnvironmentValues { diff --git a/Sources/Orbit/Support/Environment Keys/TextLinkColorKey.swift b/Sources/Orbit/Support/Environment Keys/TextLinkColorKey.swift index 027c7511c0d..5c8030ea34f 100644 --- a/Sources/Orbit/Support/Environment Keys/TextLinkColorKey.swift +++ b/Sources/Orbit/Support/Environment Keys/TextLinkColorKey.swift @@ -1,7 +1,7 @@ import SwiftUI struct TextLinkColorKey: EnvironmentKey { - static var defaultValue: TextLink.Color? = nil + static let defaultValue: TextLink.Color? = nil } public extension EnvironmentValues { diff --git a/Sources/Orbit/Support/Environment Keys/TextSizeKey.swift b/Sources/Orbit/Support/Environment Keys/TextSizeKey.swift new file mode 100644 index 00000000000..39de070708f --- /dev/null +++ b/Sources/Orbit/Support/Environment Keys/TextSizeKey.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct TextSizeKey: EnvironmentKey { + static let defaultValue: CGFloat? = nil +} + +public extension EnvironmentValues { + + /// A text size stored in a view’s environment, used for Orbit text based components, such as `Text`, `Heading` or `Icon`. + /// + /// This environment value serves as a replacement for non-public `font` environment value. + var textSize: CGFloat? { + get { self[TextSizeKey.self] } + set { self[TextSizeKey.self] = newValue } + } +} + +public extension View { + + /// Set the text size for any text based Orbit component within the view hierarchy. + /// + /// - Parameters: + /// - size: A size that will be used in text based Orbit components such as `Text` or `Icon`. + /// Pass `nil` to ignore environment text size and to allow the system or the container to provide its own size. + /// If a container-specific override doesn’t exist, the `.normal` size will be used. + func textSize(_ size: Text.Size?) -> some View { + environment(\.textSize, size?.value) + } + + + /// Set the custom text size for any text based Orbit component within the view hierarchy. + /// + /// - Parameters: + /// - size: A size that will be used in text based Orbit components such as `Text` or `Icon`. + /// Pass `nil` to ignore environment text size and to allow the system or the container to provide its own size. + /// If a container-specific override doesn’t exist, the `.normal` size will be used. + func textSize(custom size: CGFloat?) -> some View { + environment(\.textSize, size) + } +} diff --git a/Sources/Orbit/Support/Forms/FieldMessage.swift b/Sources/Orbit/Support/Forms/FieldMessage.swift index 20f9370a3c7..fafd8571ae1 100644 --- a/Sources/Orbit/Support/Forms/FieldMessage.swift +++ b/Sources/Orbit/Support/Forms/FieldMessage.swift @@ -12,9 +12,10 @@ public struct FieldMessage: View { if let message = message, message.isEmpty == false { HStack(alignment: .firstTextBaseline, spacing: spacing) { if let icon = message.icon { - Icon(icon, size: .small) + Icon(icon) + .iconSize(.small) .accessibility(hidden: true) - // A workaround for current Orbit non-matching icon size + // A workaround for current Orbit non-matching icon size .alignmentGuide(.firstTextBaseline) { $0.height * 0.82 } } diff --git a/Sources/Orbit/Support/Forms/InputContent.swift b/Sources/Orbit/Support/Forms/InputContent.swift index 53ca34be8fd..8c47bb407d1 100644 --- a/Sources/Orbit/Support/Forms/InputContent.swift +++ b/Sources/Orbit/Support/Forms/InputContent.swift @@ -130,7 +130,7 @@ struct InputContentPreviews: PreviewProvider { } prefix: { Icon(.visa) } suffix: { - IconButton(.checkCircle, size: .normal) { + IconButton(.checkCircle) { // No action } } diff --git a/Sources/Orbit/Support/Forms/KeyValueField.swift b/Sources/Orbit/Support/Forms/KeyValueField.swift index 5db5ea51f78..c3820c34d40 100644 --- a/Sources/Orbit/Support/Forms/KeyValueField.swift +++ b/Sources/Orbit/Support/Forms/KeyValueField.swift @@ -12,7 +12,8 @@ public struct KeyValueField: View { public var body: some View { VStack(alignment: alignment, spacing: 0) { - Text(key, size: size.keySize) + Text(key) + .textSize(size.keySize) .textColor(.inkNormal) .accessibility(.keyValueKey) diff --git a/Sources/Orbit/Support/Forms/SelectableLabelWrapper.swift b/Sources/Orbit/Support/Forms/SelectableLabelWrapper.swift index aa7e3b14554..df66cdc37d9 100644 --- a/Sources/Orbit/Support/Forms/SelectableLabelWrapper.swift +++ b/Sources/Orbit/Support/Forms/SelectableLabelWrapper.swift @@ -1,19 +1,19 @@ import SwiftUI import UIKit -struct SelectableLabelWrapper: UIViewRepresentable { +struct CopyableText: UIViewRepresentable { - let text: String - private let selectableLabel = SelectableLabel() + private let text: String + private let label = CopyableLabel() init(_ text: String) { self.text = text } func makeUIView(context _: Context) -> UILabel { - selectableLabel.text = text - selectableLabel.textColor = .clear - return selectableLabel + label.text = text + label.textColor = .clear + return label } func updateUIView(_ uiView: UILabel, context _: Context) { @@ -21,12 +21,12 @@ struct SelectableLabelWrapper: UIViewRepresentable { } } -private final class SelectableLabel: UILabel { +private final class CopyableLabel: UILabel { /// Allows override default behaviour (copying whole text) by for example copying only part of it - public var textToBeCopied: String? + var textToBeCopied: String? - override public init(frame: CGRect) { + override init(frame: CGRect) { super.init(frame: frame) isUserInteractionEnabled = true @@ -44,21 +44,21 @@ private final class SelectableLabel: UILabel { fatalError("init(coder:) has not been implemented") } - override public var canBecomeFirstResponder: Bool { + override var canBecomeFirstResponder: Bool { true } - override public func canPerformAction(_ action: Selector, withSender _: Any?) -> Bool { + override func canPerformAction(_ action: Selector, withSender _: Any?) -> Bool { action == #selector(copy(_:)) } // MARK: - UIResponderStandardEditActions - override public func copy(_: Any?) { + override func copy(_: Any?) { UIPasteboard.general.string = textToBeCopied ?? text } // MARK: - Long-press Handler - @objc private func handleLongPress(_ recognizer: UIGestureRecognizer) { + @objc func handleLongPress(_ recognizer: UIGestureRecognizer) { if recognizer.state == .began { self.becomeFirstResponder() UIMenuController.shared.showMenu(from: self, rect: self.bounds) diff --git a/Sources/Orbit/Support/Layout/TextStrut.swift b/Sources/Orbit/Support/Layout/TextStrut.swift index 6cd5df6f4d2..8500c5f492a 100644 --- a/Sources/Orbit/Support/Layout/TextStrut.swift +++ b/Sources/Orbit/Support/Layout/TextStrut.swift @@ -5,21 +5,29 @@ import SwiftUI /// Can be used to force minimal height of elements that contain variable combination of icons and texts. public struct TextStrut: View { - @Environment(\.sizeCategory) var sizeCategory - - let textSize: Text.Size + @Environment(\.textSize) private var textSize + @Environment(\.textLineHeight) private var textLineHeight + @Environment(\.sizeCategory) private var sizeCategory public var body: some View { Color.clear - .frame(width: 0, height: textSize.lineHeight * sizeCategory.ratio) - .alignmentGuide(.firstTextBaseline) { _ in textSize.value } - .alignmentGuide(.lastTextBaseline) { _ in textSize.value } + .frame(width: 0, height: lineHeight * sizeCategory.ratio) + .alignmentGuide(.firstTextBaseline) { _ in size } + .alignmentGuide(.lastTextBaseline) { _ in size } } - /// Creates invisible strut with height based on provided text size. - public init(_ textSize: Text.Size = .normal) { - self.textSize = textSize + private var lineHeight: CGFloat { + textLineHeight ?? Text.Size.lineHeight(forTextSize: size) + } + + private var size: CGFloat { + textSize ?? Text.Size.normal.value } + + /// Creates invisible strut with height based on provided text size. + /// + /// Provide the text size and optional line height using `textSize()` and `textLineHeight()` modifiers. + public init() {} } // MARK: - Previews @@ -38,6 +46,7 @@ struct TextStrutPreviews: PreviewProvider { Select("", value: "Select", action: {}) .overlay( strut(padding: .small) + .textSize(.small) ) .measured() .padding(.medium) @@ -55,11 +64,12 @@ struct TextStrutPreviews: PreviewProvider { .background(Color.redLight) HStack(alignment: .firstTextBaseline) { - Text("Text", size: .xLarge) - TextStrut(.xLarge) + Text("Text") + TextStrut() .frame(width: .xxxSmall) .overlay(Color.greenNormal) } + .textSize(.xLarge) .background(Color.redLight) } .padding(.medium) @@ -69,15 +79,16 @@ struct TextStrutPreviews: PreviewProvider { static var tile: some View { Tile(action: {}) .overlay( - strut(.large, padding: 14) + strut(padding: 14) + .textSize(.large) ) .measured() .padding(.medium) .previewDisplayName() } - static func strut(_ size: Text.Size = .normal, padding: CGFloat) -> some View { - TextStrut(size) + static func strut(padding: CGFloat) -> some View { + TextStrut() .frame(width: .xxxSmall) .overlay(Color.greenNormal) .padding(.vertical, padding) diff --git a/Sources/Orbit/Support/Text/ConcatenatedText.swift b/Sources/Orbit/Support/Text/ConcatenatedText.swift index d375567ccee..96cb19d2d06 100644 --- a/Sources/Orbit/Support/Text/ConcatenatedText.swift +++ b/Sources/Orbit/Support/Text/ConcatenatedText.swift @@ -3,10 +3,12 @@ import SwiftUI struct ConcatenatedText: View { @Environment(\.iconColor) var iconColor + @Environment(\.iconSize) var iconSize @Environment(\.sizeCategory) var sizeCategory @Environment(\.textAccentColor) var textAccentColor @Environment(\.textColor) var textColor @Environment(\.textFontWeight) var textFontWeight + @Environment(\.textSize) var textSize let content: (TextRepresentableEnvironment) -> SwiftUI.Text? @@ -19,10 +21,12 @@ struct ConcatenatedText: View { var textRepresentableEnvironment: TextRepresentableEnvironment { .init( iconColor: iconColor, - sizeCategory: sizeCategory, + iconSize: iconSize, textAccentColor: textAccentColor, textColor: textColor, - textFontWeight: textFontWeight + textFontWeight: textFontWeight, + textSize: textSize, + sizeCategory: sizeCategory ) } } diff --git a/Sources/Orbit/Support/Text/TextBuildable+Heading.swift b/Sources/Orbit/Support/Text/TextBuildable+Heading.swift new file mode 100644 index 00000000000..7397cb0fa47 --- /dev/null +++ b/Sources/Orbit/Support/Text/TextBuildable+Heading.swift @@ -0,0 +1,135 @@ +import SwiftUI + +public extension Heading { + + /// Returns a modified Orbit heading with provided font weight. + /// + /// - Parameters: + /// - weight: One of the available font weights. When the value is `nil`, the environment value will be used instead. + func fontWeight(_ weight: Font.Weight?) -> Self { + set(\.fontWeight, to: weight) + } + + /// Returns a modified Orbit heading with provided accent color. + /// + /// When the value is `nil`, the environment value will be used instead. + /// + /// - Parameters: + /// - color: A color that will be used in `` tags in this heading. + func textAccentColor(_ accentColor: Color?) -> Self { + set(\.accentColor, to: accentColor) + } + + /// Returns a modified Orbit heading with provided color. + /// + /// - Parameters: + /// - color: The color to use when displaying this heading. + /// When the value is `nil`, the environment value `textColor` or the default `inkDark` color will be used in this order. + func textColor(_ color: Color?) -> Self { + set(\.color, to: color) + } + + /// Returns a modified Orbit heading with applied vertical offset for the text relative to its baseline. + /// + /// - Parameters: + /// - baselineOffset: The amount to shift the text vertically (up or down) relative to its baseline. + func baselineOffset(_ baselineOffset: CGFloat) -> Self { + self.baselineOffset(baselineOffset as CGFloat?) + } + + /// Returns a modified Orbit heading with applied vertical offset for the text relative to its baseline. + /// + /// - Parameters: + /// - baselineOffset: The amount to shift the text vertically (up or down) relative to its baseline. When the value is `nil`, the environment value will be used instead. + func baselineOffset(_ baselineOffset: CGFloat?) -> Self { + set(\.baselineOffset, to: baselineOffset) + } + + /// Returns a modified Orbit heading with applied bold font weight. + /// + /// - Parameters: + /// - isActive: A boolean that indicates if the bold style is in effect. If left unspecified, the default is true. + func bold(_ isActive: Bool = true) -> Self { + self.bold(isActive as Bool?) + } + + /// Returns a modified Orbit heading with applied bold font weight. + /// + /// - Parameters: + /// - isActive: A boolean that indicates if the bold style is in effect. When the value is `nil`, the environment value will be used instead. + func bold(_ isActive: Bool?) -> Self { + set(\.isBold, to: isActive) + } + + /// Returns a modified Orbit heading with applied italics. + /// + /// - Parameters: + /// - isActive: A boolean that indicates if the bold style is in effect. If left unspecified, the default is true. + func italic(_ isActive: Bool = true) -> Self { + self.italic(isActive as Bool?) + } + + /// Returns a modified Orbit heading with applied italics. + /// + /// - Parameters: + /// - isActive: A boolean that indicates if the bold style is in effect. When the value is `nil`, the environment value will be used instead. + func italic(_ isActive: Bool?) -> Self { + set(\.isItalic, to: isActive) + } + + /// Returns a modified Orbit heading with applied spacing, or kerning, between characters. + /// + /// - Parameters: + /// - kerning: The spacing to use between individual characters in this text. Value of 0 sets the kerning to the system default value. When the value is `nil`, the environment value will be used instead. + func kerning(_ kerning: CGFloat) -> Self { + set(\.kerning, to: kerning) + } + + /// Returns a modified Orbit heading with applied fixed-width digits, while leaving other characters proportionally spaced. + /// + /// - Parameters: + /// - isActive: A Boolean value that indicates whether the heading has a fixed-width digits style applied. + func monospacedDigit() -> Self { + self.monospacedDigit(true) + } + + /// Returns a modified Orbit heading with applied fixed-width digits, while leaving other characters proportionally spaced. + /// + /// - Parameters: + /// - isActive: A Boolean value that indicates whether the heading has a fixed-width digits style applied. When the value is `nil`, the environment value will be used instead. + func monospacedDigit(_ isActive: Bool? = true) -> Self { + set(\.isMonospacedDigit, to: isActive) + } + + /// Returns a modified Orbit heading with applied strikethrough. + /// + /// - Parameters: + /// - isActive: A Boolean value that indicates whether the text has a strikethrough applied. + func strikethrough(_ isActive: Bool = true) -> Self { + self.strikethrough(isActive as Bool?) + } + + /// Returns a modified Orbit heading with applied strikethrough. + /// + /// - Parameters: + /// - isActive: A Boolean value that indicates whether the text has a strikethrough applied. When the value is `nil`, the environment value will be used instead. + func strikethrough(_ isActive: Bool?) -> Self { + set(\.strikethrough, to: isActive) + } + + /// Returns a modified Orbit heading with applied underline. + /// + /// - Parameters: + /// - isActive: A boolean that indicates if the underline style is in effect. If left unspecified, the default is true. + func underline(_ isActive: Bool = true) -> Self { + self.underline(isActive as Bool?) + } + + /// Returns a modified Orbit heading with applied underline. + /// + /// - Parameters: + /// - isActive: A boolean that indicates if the underline style is in effect. When the value is `nil`, the environment value will be used instead. + func underline(_ isActive: Bool?) -> Self { + set(\.isUnderline, to: isActive) + } +} diff --git a/Sources/Orbit/Support/Text/TextBuildable+Icon.swift b/Sources/Orbit/Support/Text/TextBuildable+Icon.swift index d10dfa39c78..f2250245eb9 100644 --- a/Sources/Orbit/Support/Text/TextBuildable+Icon.swift +++ b/Sources/Orbit/Support/Text/TextBuildable+Icon.swift @@ -34,4 +34,31 @@ public extension Icon { func iconColor(_ color: Color?) -> Self { set(\.color, to: color) } + + /// Returns a modified Orbit icon with provided size. + /// + /// - Parameters: + /// - size: The size to use when displaying this icon. + /// When the value is `nil`, the environment values `iconSize`, `textSize` or the default `.normal` size will be used in this order. + func iconSize(_ size: Icon.Size?) -> Self { + set(\.size, to: size?.value) + } + + /// Returns a modified Orbit icon with provided custom size. + /// + /// - Parameters: + /// - size: The size to use when displaying this icon. + /// When the value is `nil`, the environment values `iconSize`, `textSize` or the default `.normal` size will be used in this order. + func iconSize(custom size: CGFloat?) -> Self { + set(\.size, to: size) + } + + /// Returns a modified Orbit icon with size matching line height of a provided text size. + /// + /// - Parameters: + /// - size: The text size to use when displaying this icon. + /// When the value is `nil`, the environment values `iconSize`, `textSize` or the default `.normal` size will be used in this order. + func iconSize(textSize: Text.Size?) -> Self { + set(\.size, to: textSize?.lineHeight) + } } diff --git a/Sources/Orbit/Support/Text/TextBuildable+Text.swift b/Sources/Orbit/Support/Text/TextBuildable+Text.swift new file mode 100644 index 00000000000..722bcdaa85e --- /dev/null +++ b/Sources/Orbit/Support/Text/TextBuildable+Text.swift @@ -0,0 +1,160 @@ +import SwiftUI + +public extension Text { + + /// Returns a modified Orbit text with provided font weight. + /// + /// - Parameters: + /// - weight: One of the available font weights. When the value is `nil`, the environment value will be used instead. + func fontWeight(_ weight: Font.Weight?) -> Self { + set(\.fontWeight, to: weight) + } + + /// Returns a modified Orbit text with provided color. + /// + /// - Parameters: + /// - color: The color to use when displaying this text. + /// When the value is `nil`, the environment value `textColor` or the default `inkDark` color will be used in this order. + func textColor(_ color: Color?) -> Self { + set(\.color, to: color) + } + + /// Returns a modified Orbit text with provided line height. + /// + /// - Parameters: + /// - height: The line height to use when displaying this text. + /// When the value is `nil`, the environment value `textLineHeight` or the default calcualted size will be used in this order. + func textLineHeight(_ height: CGFloat?) -> Self { + set(\.lineHeight, to: height) + } + + /// Returns a modified Orbit text with provided size. + /// + /// - Parameters: + /// - size: The size to use when displaying this text. + /// When the value is `nil`, the environment value `textSize` or the default `.normal` size will be used in this order. + func textSize(_ size: Text.Size?) -> Self { + set(\.size, to: size?.value) + } + + /// Returns a modified Orbit text with provided custom size. + /// + /// - Parameters: + /// - size: The custom size to use when displaying this text. + /// When the value is `nil`, the environment value `textSize` or the default `.normal` size will be used in this order. + func textSize(custom size: CGFloat?) -> Self { + set(\.size, to: size) + } + + /// Returns a modified Orbit text with provided accent color. + /// + /// - Parameters: + /// - color: A color that will be used in `` tags in this text. When the value is `nil`, the environment value will be used instead. + func textAccentColor(_ accentColor: Color?) -> Self { + set(\.accentColor, to: accentColor) + } + + /// Returns a modified Orbit text with applied vertical offset for the text relative to its baseline. + /// + /// - Parameters: + /// - baselineOffset: The amount to shift the text vertically (up or down) relative to its baseline. + func baselineOffset(_ baselineOffset: CGFloat) -> Self { + self.baselineOffset(baselineOffset as CGFloat?) + } + + /// Returns a modified Orbit text with applied vertical offset for the text relative to its baseline. + /// + /// - Parameters: + /// - baselineOffset: The amount to shift the text vertically (up or down) relative to its baseline. When the value is `nil`, the environment value will be used instead. + func baselineOffset(_ baselineOffset: CGFloat?) -> Self { + set(\.baselineOffset, to: baselineOffset) + } + + /// Returns a modified Orbit text with applied bold font weight. + /// + /// - Parameters: + /// - isActive: A boolean that indicates if the bold style is in effect. If left unspecified, the default is true. + func bold(_ isActive: Bool = true) -> Self { + self.bold(isActive as Bool?) + } + + /// Returns a modified Orbit text with applied bold font weight. + /// + /// - Parameters: + /// - isActive: A boolean that indicates if the bold style is in effect. When the value is `nil`, the environment value will be used instead. + func bold(_ isActive: Bool?) -> Self { + set(\.isBold, to: isActive) + } + + /// Returns a modified Orbit text with applied italics. + /// + /// - Parameters: + /// - isActive: A boolean that indicates if the bold style is in effect. If left unspecified, the default is true. + func italic(_ isActive: Bool = true) -> Self { + self.italic(isActive as Bool?) + } + + /// Returns a modified Orbit text with applied italics. + /// + /// - Parameters: + /// - isActive: A boolean that indicates if the bold style is in effect. When the value is `nil`, the environment value will be used instead. + func italic(_ isActive: Bool?) -> Self { + set(\.isItalic, to: isActive) + } + + /// Returns a modified Orbit text with applied spacing, or kerning, between characters. + /// + /// - Parameters: + /// - kerning: The spacing to use between individual characters in this text. Value of 0 sets the kerning to the system default value. + func kerning(_ kerning: CGFloat) -> Self { + self.kerning(kerning as CGFloat?) + } + + /// Returns a modified Orbit text with applied spacing, or kerning, between characters. + /// + /// - Parameters: + /// - kerning: The spacing to use between individual characters in this text. Value of 0 sets the kerning to the system default value. When the value is `nil`, the environment value will be used instead. + func kerning(_ kerning: CGFloat?) -> Self { + set(\.kerning, to: kerning) + } + + /// Returns a modified Orbit text with applied fixed-width digits, while leaving other characters proportionally spaced. + /// + /// - Parameters: + /// - isActive: A Boolean value that indicates whether the text has a fixed-width digits style applied. When the value is `nil`, the environment value will be used instead. + func monospacedDigit(_ isActive: Bool? = true) -> Self { + set(\.isMonospacedDigit, to: isActive) + } + + /// Returns a modified Orbit text with applied strikethrough. + /// + /// - Parameters: + /// - isActive: A Boolean value that indicates whether the text has a strikethrough applied. If left unspecified, the default is true. + func strikethrough(_ isActive: Bool = true) -> Self { + self.strikethrough(isActive as Bool?) + } + + /// Returns a modified Orbit text with applied strikethrough. + /// + /// - Parameters: + /// - isActive: A Boolean value that indicates whether the text has a strikethrough applied. When the value is `nil`, the environment value will be used instead. + func strikethrough(_ isActive: Bool?) -> Self { + set(\.strikethrough, to: isActive) + } + + /// Returns a modified Orbit text with applied underline. + /// + /// - Parameters: + /// - isActive: A boolean that indicates if the underline style is in effect. If left unspecified, the default is true. + func underline(_ isActive: Bool = true) -> Self { + self.underline(isActive as Bool?) + } + + /// Returns a modified Orbit text with applied underline. + /// + /// - Parameters: + /// - isActive: A boolean that indicates if the underline style is in effect. When the value is `nil`, the environment value will be used instead. + func underline(_ isActive: Bool?) -> Self { + set(\.isUnderline, to: isActive) + } +} diff --git a/Sources/Orbit/Support/Text/TextBuildable.swift b/Sources/Orbit/Support/Text/TextBuildable.swift index 192ad263c08..6da4efabf1f 100644 --- a/Sources/Orbit/Support/Text/TextBuildable.swift +++ b/Sources/Orbit/Support/Text/TextBuildable.swift @@ -8,18 +8,20 @@ protocol TextBuildable { var baselineOffset: CGFloat? { get set } var fontWeight: Font.Weight? { get set } var color: Color? { get set } + var size: CGFloat? { get set } } protocol FormattedTextBuildable: TextBuildable { var accentColor: Color? { get set } - var baselineOffset: CGFloat? { get set } var isBold: Bool? { get set } var isItalic: Bool? { get set } + var isMonospacedDigit: Bool? { get set } var isUnderline: Bool? { get set } var kerning: CGFloat? { get set } + var lineHeight: CGFloat? { get set } + var size: CGFloat? { get set } var strikethrough: Bool? { get set } - var isMonospacedDigit: Bool? { get set } } extension TextBuildable { @@ -30,269 +32,3 @@ extension TextBuildable { return copy } } - -public extension Text { - - /// Returns a modified Orbit text with provided font weight. - /// - /// - Parameters: - /// - weight: One of the available font weights. When the value is `nil`, the environment value will be used instead. - func fontWeight(_ weight: Font.Weight?) -> Self { - set(\.fontWeight, to: weight) - } - - /// Returns a modified Orbit text with provided color. - /// - /// - Parameters: - /// - color: The color to use when displaying this text. - /// When the value is `nil`, the environment value `textColor` or the default `inkDark` color will be used in this order. - func textColor(_ color: Color?) -> Self { - set(\.color, to: color) - } - - /// Returns a modified Orbit text with provided accent color. - /// - /// - Parameters: - /// - color: A color that will be used in `` tags in this text. When the value is `nil`, the environment value will be used instead. - func textAccentColor(_ accentColor: Color?) -> Self { - set(\.accentColor, to: accentColor) - } - - /// Returns a modified Orbit text with applied vertical offset for the text relative to its baseline. - /// - /// - Parameters: - /// - baselineOffset: The amount to shift the text vertically (up or down) relative to its baseline. - func baselineOffset(_ baselineOffset: CGFloat) -> Self { - self.baselineOffset(baselineOffset as CGFloat?) - } - - /// Returns a modified Orbit text with applied vertical offset for the text relative to its baseline. - /// - /// - Parameters: - /// - baselineOffset: The amount to shift the text vertically (up or down) relative to its baseline. When the value is `nil`, the environment value will be used instead. - func baselineOffset(_ baselineOffset: CGFloat?) -> Self { - set(\.baselineOffset, to: baselineOffset) - } - - /// Returns a modified Orbit text with applied bold font weight. - /// - /// - Parameters: - /// - isActive: A boolean that indicates if the bold style is in effect. If left unspecified, the default is true. - func bold(_ isActive: Bool = true) -> Self { - self.bold(isActive as Bool?) - } - - /// Returns a modified Orbit text with applied bold font weight. - /// - /// - Parameters: - /// - isActive: A boolean that indicates if the bold style is in effect. When the value is `nil`, the environment value will be used instead. - func bold(_ isActive: Bool?) -> Self { - set(\.isBold, to: isActive) - } - - /// Returns a modified Orbit text with applied italics. - /// - /// - Parameters: - /// - isActive: A boolean that indicates if the bold style is in effect. If left unspecified, the default is true. - func italic(_ isActive: Bool = true) -> Self { - self.italic(isActive as Bool?) - } - - /// Returns a modified Orbit text with applied italics. - /// - /// - Parameters: - /// - isActive: A boolean that indicates if the bold style is in effect. When the value is `nil`, the environment value will be used instead. - func italic(_ isActive: Bool?) -> Self { - set(\.isItalic, to: isActive) - } - - /// Returns a modified Orbit text with applied spacing, or kerning, between characters. - /// - /// - Parameters: - /// - kerning: The spacing to use between individual characters in this text. Value of 0 sets the kerning to the system default value. - func kerning(_ kerning: CGFloat) -> Self { - self.kerning(kerning as CGFloat?) - } - - /// Returns a modified Orbit text with applied spacing, or kerning, between characters. - /// - /// - Parameters: - /// - kerning: The spacing to use between individual characters in this text. Value of 0 sets the kerning to the system default value. When the value is `nil`, the environment value will be used instead. - func kerning(_ kerning: CGFloat?) -> Self { - set(\.kerning, to: kerning) - } - - /// Returns a modified Orbit text with applied fixed-width digits, while leaving other characters proportionally spaced. - /// - /// - Parameters: - /// - isActive: A Boolean value that indicates whether the text has a fixed-width digits style applied. When the value is `nil`, the environment value will be used instead. - func monospacedDigit(_ isActive: Bool? = true) -> Self { - set(\.isMonospacedDigit, to: isActive) - } - - /// Returns a modified Orbit text with applied strikethrough. - /// - /// - Parameters: - /// - isActive: A Boolean value that indicates whether the text has a strikethrough applied. If left unspecified, the default is true. - func strikethrough(_ isActive: Bool = true) -> Self { - self.strikethrough(isActive as Bool?) - } - - /// Returns a modified Orbit text with applied strikethrough. - /// - /// - Parameters: - /// - isActive: A Boolean value that indicates whether the text has a strikethrough applied. When the value is `nil`, the environment value will be used instead. - func strikethrough(_ isActive: Bool?) -> Self { - set(\.strikethrough, to: isActive) - } - - /// Returns a modified Orbit text with applied underline. - /// - /// - Parameters: - /// - isActive: A boolean that indicates if the underline style is in effect. If left unspecified, the default is true. - func underline(_ isActive: Bool = true) -> Self { - self.underline(isActive as Bool?) - } - - /// Returns a modified Orbit text with applied underline. - /// - /// - Parameters: - /// - isActive: A boolean that indicates if the underline style is in effect. When the value is `nil`, the environment value will be used instead. - func underline(_ isActive: Bool?) -> Self { - set(\.isUnderline, to: isActive) - } -} - -public extension Heading { - - /// Returns a modified Orbit heading with provided font weight. - /// - /// - Parameters: - /// - weight: One of the available font weights. When the value is `nil`, the environment value will be used instead. - func fontWeight(_ weight: Font.Weight?) -> Self { - set(\.fontWeight, to: weight) - } - - /// Returns a modified Orbit heading with provided accent color. - /// - /// When the value is `nil`, the environment value will be used instead. - /// - /// - Parameters: - /// - color: A color that will be used in `` tags in this heading. - func textAccentColor(_ accentColor: Color?) -> Self { - set(\.accentColor, to: accentColor) - } - - /// Returns a modified Orbit heading with provided color. - /// - /// - Parameters: - /// - color: The color to use when displaying this heading. - /// When the value is `nil`, the environment value `textColor` or the default `inkDark` color will be used in this order. - func textColor(_ color: Color?) -> Self { - set(\.color, to: color) - } - - /// Returns a modified Orbit heading with applied vertical offset for the text relative to its baseline. - /// - /// - Parameters: - /// - baselineOffset: The amount to shift the text vertically (up or down) relative to its baseline. - func baselineOffset(_ baselineOffset: CGFloat) -> Self { - self.baselineOffset(baselineOffset as CGFloat?) - } - - /// Returns a modified Orbit heading with applied vertical offset for the text relative to its baseline. - /// - /// - Parameters: - /// - baselineOffset: The amount to shift the text vertically (up or down) relative to its baseline. When the value is `nil`, the environment value will be used instead. - func baselineOffset(_ baselineOffset: CGFloat?) -> Self { - set(\.baselineOffset, to: baselineOffset) - } - - /// Returns a modified Orbit heading with applied bold font weight. - /// - /// - Parameters: - /// - isActive: A boolean that indicates if the bold style is in effect. If left unspecified, the default is true. - func bold(_ isActive: Bool = true) -> Self { - self.bold(isActive as Bool?) - } - - /// Returns a modified Orbit heading with applied bold font weight. - /// - /// - Parameters: - /// - isActive: A boolean that indicates if the bold style is in effect. When the value is `nil`, the environment value will be used instead. - func bold(_ isActive: Bool?) -> Self { - set(\.isBold, to: isActive) - } - - /// Returns a modified Orbit heading with applied italics. - /// - /// - Parameters: - /// - isActive: A boolean that indicates if the bold style is in effect. If left unspecified, the default is true. - func italic(_ isActive: Bool = true) -> Self { - self.italic(isActive as Bool?) - } - - /// Returns a modified Orbit heading with applied italics. - /// - /// - Parameters: - /// - isActive: A boolean that indicates if the bold style is in effect. When the value is `nil`, the environment value will be used instead. - func italic(_ isActive: Bool?) -> Self { - set(\.isItalic, to: isActive) - } - - /// Returns a modified Orbit heading with applied spacing, or kerning, between characters. - /// - /// - Parameters: - /// - kerning: The spacing to use between individual characters in this text. Value of 0 sets the kerning to the system default value. When the value is `nil`, the environment value will be used instead. - func kerning(_ kerning: CGFloat) -> Self { - set(\.kerning, to: kerning) - } - - /// Returns a modified Orbit heading with applied fixed-width digits, while leaving other characters proportionally spaced. - /// - /// - Parameters: - /// - isActive: A Boolean value that indicates whether the heading has a fixed-width digits style applied. - func monospacedDigit() -> Self { - self.monospacedDigit(true) - } - - /// Returns a modified Orbit heading with applied fixed-width digits, while leaving other characters proportionally spaced. - /// - /// - Parameters: - /// - isActive: A Boolean value that indicates whether the heading has a fixed-width digits style applied. When the value is `nil`, the environment value will be used instead. - func monospacedDigit(_ isActive: Bool? = true) -> Self { - set(\.isMonospacedDigit, to: isActive) - } - - /// Returns a modified Orbit heading with applied strikethrough. - /// - /// - Parameters: - /// - isActive: A Boolean value that indicates whether the text has a strikethrough applied. - func strikethrough(_ isActive: Bool = true) -> Self { - self.strikethrough(isActive as Bool?) - } - - /// Returns a modified Orbit heading with applied strikethrough. - /// - /// - Parameters: - /// - isActive: A Boolean value that indicates whether the text has a strikethrough applied. When the value is `nil`, the environment value will be used instead. - func strikethrough(_ isActive: Bool?) -> Self { - set(\.strikethrough, to: isActive) - } - - /// Returns a modified Orbit heading with applied underline. - /// - /// - Parameters: - /// - isActive: A boolean that indicates if the underline style is in effect. If left unspecified, the default is true. - func underline(_ isActive: Bool = true) -> Self { - self.underline(isActive as Bool?) - } - - /// Returns a modified Orbit heading with applied underline. - /// - /// - Parameters: - /// - isActive: A boolean that indicates if the underline style is in effect. When the value is `nil`, the environment value will be used instead. - func underline(_ isActive: Bool?) -> Self { - set(\.isUnderline, to: isActive) - } -} diff --git a/Sources/Orbit/Support/Text/TextRepresentable+Concatenation.swift b/Sources/Orbit/Support/Text/TextRepresentable+Concatenation.swift index 97927fd6ce2..92685dae906 100644 --- a/Sources/Orbit/Support/Text/TextRepresentable+Concatenation.swift +++ b/Sources/Orbit/Support/Text/TextRepresentable+Concatenation.swift @@ -27,20 +27,25 @@ struct TextConcatenationPreviews: PreviewProvider { static var standalone: some View { ( - Heading("Hanoi ", style: .title1) - + Icon(.grid, size: .xLarge) - + Icon(.grid, size: .small) + Heading("Hanoi ", style: .title1) + + Icon(.grid) + .iconSize(.xLarge) + + Icon(.grid) + .iconSize(.small) .iconColor(.redNormal) - + Icon(.grid, size: .small) + + Icon(.grid) + .iconSize(.small) .iconColor(.blueDark) + Heading(" San Pedro de Alcantara", style: .title1) .textColor(.blueNormal) .fontWeight(.black) .strikethrough() + Text(" ") - + Icon("info.circle", size: .large) + + Icon("info.circle") + .iconSize(.large) .iconColor(.blueDark) - + Text(" (Delayed)", size: .xLarge) + + Text(" (Delayed)") + .textSize(.xLarge) .textColor(.inkNormal) ) .previewDisplayName() @@ -48,18 +53,21 @@ struct TextConcatenationPreviews: PreviewProvider { static var formatting: some View { ( - Icon(.grid, size: .xLarge) - + Text(" Text ", size: .xLarge) + Icon(.grid) + .iconSize(.xLarge) + + Text(" Text ") + .textSize(.xLarge) .bold() .italic() - + Icon(.informationCircle, size: .large) + + Icon(.informationCircle) + .iconSize(.large) .baselineOffset(-1) - + Icon("info.circle.fill", size: .large) - + Text( - " Text with formatting", - size: .small - ) - + Icon(.check, size: .small) + + Icon("info.circle.fill") + .iconSize(.large) + + Text(" Text with formatting") + .textSize(.small) + + Icon(.check) + .iconSize(.small) .iconColor(.greenDark) ) .textColor(.blueDark) @@ -83,7 +91,8 @@ struct TextConcatenationPreviews: PreviewProvider { HStack { Heading(label, style: style) .textColor(.inkDark) - + Icon(.flightReturn, size: .custom(style.size)) + + Icon(.flightReturn) + .iconSize(custom: style.size) .iconColor(.inkNormal) + Heading(label, style: style) .textColor(.inkDark) diff --git a/Sources/Orbit/Support/Text/TextRepresentable.swift b/Sources/Orbit/Support/Text/TextRepresentable.swift index ce3946a0847..aaeb3ba516f 100644 --- a/Sources/Orbit/Support/Text/TextRepresentable.swift +++ b/Sources/Orbit/Support/Text/TextRepresentable.swift @@ -11,8 +11,10 @@ public protocol TextRepresentable { /// Environment values required for proper formatting of concatenated Orbit text components. public struct TextRepresentableEnvironment { let iconColor: Color? - let sizeCategory: ContentSizeCategory + let iconSize: CGFloat? let textAccentColor: Color? let textColor: Color? let textFontWeight: Font.Weight? + let textSize: CGFloat? + let sizeCategory: ContentSizeCategory } diff --git a/Sources/OrbitStorybook/Detail/Items/StorybookBadgeList.swift b/Sources/OrbitStorybook/Detail/Items/StorybookBadgeList.swift index f1f864e5851..c3dac55b087 100644 --- a/Sources/OrbitStorybook/Detail/Items/StorybookBadgeList.swift +++ b/Sources/OrbitStorybook/Detail/Items/StorybookBadgeList.swift @@ -16,12 +16,14 @@ struct StorybookBadgeList { BadgeList(label, icon: .alertCircle, style: .status(.critical)) } VStack(alignment: .leading, spacing: .medium) { - BadgeList(longLabel, icon: .grid, labelColor: .secondary, size: .small) - BadgeList(label, icon: .informationCircle, style: .status(.info), labelColor: .secondary, size: .small) - BadgeList(label, icon: .checkCircle, style: .status(.success), labelColor: .secondary, size: .small) - BadgeList(label, icon: .alertCircle, style: .status(.warning), labelColor: .secondary, size: .small) - BadgeList(label, icon: .alertCircle, style: .status(.critical), labelColor: .secondary, size: .small) + BadgeList(longLabel, icon: .grid) + BadgeList(label, icon: .informationCircle, style: .status(.info)) + BadgeList(label, icon: .checkCircle, style: .status(.success)) + BadgeList(label, icon: .alertCircle, style: .status(.warning)) + BadgeList(label, icon: .alertCircle, style: .status(.critical)) } + .textColor(.inkNormal) + .textSize(.small) } .previewDisplayName() } @@ -34,7 +36,8 @@ struct StorybookBadgeList { BadgeList("This is simple BadgeList item with CountryFlag", style: .status(.critical)) { CountryFlag("us") } - BadgeList("This is BadgeList item with no icon and custom color", labelColor: .custom(.blueDark)) + BadgeList("This is BadgeList item with no icon and custom color") + .textColor(.blueDark) } .previewDisplayName() } diff --git a/Sources/OrbitStorybook/Detail/Items/StorybookCountryFlag.swift b/Sources/OrbitStorybook/Detail/Items/StorybookCountryFlag.swift index 34b07cb7660..f9bfdcf2f27 100644 --- a/Sources/OrbitStorybook/Detail/Items/StorybookCountryFlag.swift +++ b/Sources/OrbitStorybook/Detail/Items/StorybookCountryFlag.swift @@ -7,12 +7,14 @@ struct StorybookCountryFlag { VStack(alignment: .leading, spacing: .xLarge) { HStack(spacing: .small) { Text("Small") - CountryFlag("cz", size: .small) - CountryFlag("sg", size: .small) - CountryFlag("jp", size: .small) - CountryFlag("de", size: .small) - CountryFlag("unknown", size: .small) + CountryFlag("cz") + CountryFlag("sg") + CountryFlag("jp") + CountryFlag("de") + CountryFlag("unknown") } + .iconSize(.small) + HStack(spacing: .small) { Text("Normal") CountryFlag("cz") @@ -23,21 +25,26 @@ struct StorybookCountryFlag { } HStack(spacing: .small) { Text("Large") - CountryFlag("cz", size: .large) - CountryFlag("sg", size: .large) - CountryFlag("jp", size: .large) - CountryFlag("de", size: .large) - CountryFlag("unknown", size: .large) + CountryFlag("cz") + CountryFlag("sg") + CountryFlag("jp") + CountryFlag("de") + CountryFlag("unknown") } + .iconSize(.large) + HStack(spacing: .small) { Text("Borders") - CountryFlag("CZ", size: .xLarge, border: .default(cornerRadius: 8)) - CountryFlag("cZ", size: .xLarge, border: .default(cornerRadius: 0)) - CountryFlag("Cz", size: .xLarge, border: .none) + CountryFlag("CZ", border: .default(cornerRadius: 8)) + CountryFlag("cZ", border: .default(cornerRadius: 0)) + CountryFlag("Cz", border: .none) } + .iconSize(.xLarge) + HStack(spacing: .small) { Text("Custom size") - CountryFlag("us", size: .width(60)) + CountryFlag("us") + .iconSize(custom: 60) } } .previewDisplayName() diff --git a/Sources/OrbitStorybook/Detail/Items/StorybookCoupon.swift b/Sources/OrbitStorybook/Detail/Items/StorybookCoupon.swift index effe2eac3ba..a2e4334a276 100644 --- a/Sources/OrbitStorybook/Detail/Items/StorybookCoupon.swift +++ b/Sources/OrbitStorybook/Detail/Items/StorybookCoupon.swift @@ -8,12 +8,13 @@ struct StorybookCoupon { Coupon("COUPONCODE") HStack(alignment: .firstTextBaseline, spacing: .xSmall) { - Coupon("HXT3B81F", size: .small) + Coupon("HXT3B81F") .textColor(.blueDark) .previewDisplayName() - Text("PROMOCODE", size: .small) + Text("PROMOCODE") } + .textSize(.small) } .previewDisplayName() } diff --git a/Sources/OrbitStorybook/Detail/Items/StorybookIcon.swift b/Sources/OrbitStorybook/Detail/Items/StorybookIcon.swift index 67a7c4cc58e..cd404b86879 100644 --- a/Sources/OrbitStorybook/Detail/Items/StorybookIcon.swift +++ b/Sources/OrbitStorybook/Detail/Items/StorybookIcon.swift @@ -29,7 +29,6 @@ struct StorybookIcon { textStack(.small, alignment: .firstTextBaseline) textStack(.normal, alignment: .firstTextBaseline) textStack(.large, alignment: .firstTextBaseline) - textStack(.custom(30), alignment: .firstTextBaseline) } } @@ -50,17 +49,19 @@ struct StorybookIcon { HStack(alignment: .firstTextBaseline, spacing: 0) { Group { - Text("Text", size: .small) - Icon(sfSymbol, size: .small) - Icon(sfSymbol, size: .small) + Text("Text") + Icon(sfSymbol) + Icon(sfSymbol) .baselineOffset(.xxxSmall) - Icon(.informationCircle, size: .small) - Icon(.informationCircle, size: .small) + Icon(.informationCircle) + Icon(.informationCircle) .baselineOffset(.xxxSmall) } .border(.cloudLightActive, width: .hairline) } + .textSize(.small) + .iconSize(.small) .textColor(.greenDark) .overlay( Separator(color: .redNormal, thickness: .hairline), @@ -71,15 +72,17 @@ struct StorybookIcon { .padding(.top, .xLarge) ( - Text("Text", size: .small) - + Icon(sfSymbol, size: .small) - + Icon(sfSymbol, size: .small) + Text("Text") + + Icon(sfSymbol) + + Icon(sfSymbol) .baselineOffset(.xxxSmall) - + Icon(.informationCircle, size: .small) - + Icon(.informationCircle, size: .small) + + Icon(.informationCircle) + + Icon(.informationCircle) .baselineOffset(.xxxSmall) ) + .textSize(.small) + .iconSize(.small) .textColor(.greenDark) .overlay( Separator(color: .redNormal, thickness: .hairline), @@ -114,8 +117,10 @@ struct StorybookIcon { .bold() HStack(alignment: .firstTextBaseline, spacing: .xxSmall) { - Icon(.passengers, size: iconSize) - Text("XLarge text and icon size", size: textSize) + Icon(.passengers) + .iconSize(iconSize) + Text("\(String(describing: textSize).titleCased) text and icon size") + .textSize(textSize) } .overlay(Separator(thickness: .hairline), alignment: .top) .overlay(Separator(thickness: .hairline), alignment: .bottom) @@ -123,25 +128,27 @@ struct StorybookIcon { } static func headingStack(_ style: Heading.Style, alignment: VerticalAlignment) -> some View { - alignmentStack(style.iconSize, alignment: alignment) { + alignmentStack(style.lineHeight, alignment: alignment) { Heading("\(style)".capitalized, style: style) } } static func textStack(_ size: Orbit.Text.Size, alignment: VerticalAlignment) -> some View { - alignmentStack(size.iconSize, alignment: alignment) { - Text("Text \(Int(size.value))", size: size) + alignmentStack(Icon.Size.fromTextSize(size: size.value), alignment: alignment) { + Text("Text \(Int(size.value))") + .textSize(size) } } - static func alignmentStack(_ size: Icon.Size, alignment: VerticalAlignment, @ViewBuilder content: () -> V) -> some View { + static func alignmentStack(_ size: CGFloat, alignment: VerticalAlignment, @ViewBuilder content: () -> V) -> some View { HStack(spacing: .xSmall) { HStack(alignment: alignment, spacing: .xxSmall) { Group { - Icon(sfSymbol, size: size) - Icon(.informationCircle, size: size) + Icon(sfSymbol) + Icon(.informationCircle) content() } + .iconSize(custom: size) .background(Color.redLightHover) } .overlay( diff --git a/Sources/OrbitStorybook/Detail/Items/StorybookIllustration.swift b/Sources/OrbitStorybook/Detail/Items/StorybookIllustration.swift index 102edbef966..3bc0363b74d 100644 --- a/Sources/OrbitStorybook/Detail/Items/StorybookIllustration.swift +++ b/Sources/OrbitStorybook/Detail/Items/StorybookIllustration.swift @@ -12,25 +12,25 @@ struct StorybookIllustration { Card("MaxHeight = 80", showBorder: false) { VStack { - Text("Frame - Center (default)", size: .small) + Text("Frame - Center (default)") Illustration(.womanWithPhone, layout: .frame(maxHeight: 80)) .border(.cloudNormal) } VStack { - Text("Frame - Leading", size: .small) + Text("Frame - Leading") Illustration(.womanWithPhone, layout: .frame(maxHeight: 80, alignment: .leading)) .border(.cloudNormal) } VStack { - Text("Frame - Trailing", size: .small) + Text("Frame - Trailing") Illustration(.womanWithPhone, layout: .frame(maxHeight: 80, alignment: .trailing)) .border(.cloudNormal) } VStack { - Text("Resizeable", size: .small) + Text("Resizeable") Illustration(.womanWithPhone, layout: .resizeable) .frame(height: 80) .border(.cloudNormal) @@ -40,26 +40,26 @@ struct StorybookIllustration { Card("MaxHeight = 30", showBorder: false) { HStack { VStack(alignment: .leading) { - Text("Leading", size: .small) + Text("Leading") Illustration(.womanWithPhone, layout: .frame(maxHeight: 30, alignment: .leading)) .border(.cloudNormal) } VStack(alignment: .leading) { - Text("Center", size: .small) + Text("Center") Illustration(.womanWithPhone, layout: .frame(maxHeight: 30)) .border(.cloudNormal) } VStack(alignment: .leading) { - Text("Resizeable", size: .small) + Text("Resizeable") Illustration(.womanWithPhone, layout: .resizeable) .frame(height: 30) .border(.cloudNormal) } VStack(alignment: .leading) { - Text("Trailing", size: .small) + Text("Trailing") Illustration(.womanWithPhone, layout: .frame(maxHeight: 30, alignment: .trailing)) .border(.cloudNormal) } @@ -69,14 +69,14 @@ struct StorybookIllustration { Card("Resizeable", showBorder: false) { HStack(alignment: .top, spacing: .medium) { VStack(alignment: .leading) { - Text("Width = 80", size: .small) + Text("Width = 80") Illustration(.womanWithPhone, layout: .resizeable) .frame(width: 80) .border(.cloudNormal) } VStack(alignment: .leading) { - Text("Height = 80", size: .small) + Text("Height = 80") Illustration(.womanWithPhone, layout: .resizeable) .frame(height: 80) .border(.cloudNormal) @@ -84,13 +84,14 @@ struct StorybookIllustration { } VStack(alignment: .leading) { - Text("Width = 80, Height = 80", size: .small) + Text("Width = 80, Height = 80") Illustration(.womanWithPhone, layout: .resizeable) .frame(width: 80, height: 80) .border(.cloudNormal) } } } + .textSize(.small) .previewDisplayName() } } diff --git a/Sources/OrbitStorybook/Detail/Items/StorybookList.swift b/Sources/OrbitStorybook/Detail/Items/StorybookList.swift index ebdb4471035..3e85a9f83e9 100644 --- a/Sources/OrbitStorybook/Detail/Items/StorybookList.swift +++ b/Sources/OrbitStorybook/Detail/Items/StorybookList.swift @@ -13,9 +13,10 @@ struct StorybookList { } List { - ListItem(listItemText, size: .large) - ListItem(listItemText, size: .large) + ListItem(listItemText) + ListItem(listItemText) } + .textSize(.large) Separator() @@ -25,9 +26,10 @@ struct StorybookList { } List { - ListItem(listItemText, size: .large, type: .secondary) - ListItem(listItemText, size: .large, type: .secondary) + ListItem(listItemText, type: .secondary) + ListItem(listItemText, type: .secondary) } + .textSize(.large) } .previewDisplayName() } @@ -47,11 +49,16 @@ struct StorybookList { Separator() List { - ListItem(listItemText, size: .small) - ListItem(listItemText, size: .normal) - ListItem(listItemText, size: .large) - ListItem(listItemText, size: .xLarge) - ListItem(listItemText, size: .custom(30)) + ListItem(listItemText) + .textSize(.small) + ListItem(listItemText) + .textSize(.normal) + ListItem(listItemText) + .textSize(.large) + ListItem(listItemText) + .textSize(.xLarge) + ListItem(listItemText) + .textSize(custom: 30) } Separator() diff --git a/Sources/OrbitStorybook/Detail/Items/StorybookText.swift b/Sources/OrbitStorybook/Detail/Items/StorybookText.swift index 3dd8f5d8886..4120444117e 100644 --- a/Sources/OrbitStorybook/Detail/Items/StorybookText.swift +++ b/Sources/OrbitStorybook/Detail/Items/StorybookText.swift @@ -10,7 +10,8 @@ struct StorybookText { VStack(alignment: .leading, spacing: .medium) { Group { Text("Plain text with no formatting") - Text("Selectable text (on long tap)", isSelectable: true) + Text("Selectable text (on long tap)") + .textIsCopyable() Text("Text formatted and accented") .textAccentColor(.orangeNormal) Text("Text with strikethrough and kerning") diff --git a/Sources/OrbitStorybook/Foundation/StorybookColors.swift b/Sources/OrbitStorybook/Foundation/StorybookColors.swift index 54faa784b70..d9da873f61b 100644 --- a/Sources/OrbitStorybook/Foundation/StorybookColors.swift +++ b/Sources/OrbitStorybook/Foundation/StorybookColors.swift @@ -306,12 +306,13 @@ private struct ColorContent: View { ) Group { - Text(label, size: .small) + Text(label) .textColor(.black) .fontWeight(.medium) - + Text(optionalText, size: .small) + + Text(optionalText) .textColor(.gray) } + .textSize(.small) .padding(.small) Spacer(minLength: 0) @@ -337,12 +338,13 @@ private struct ColorGradientContent: View { .frame(height: 88) Group { - Text(label, size: .small) + Text(label) .textColor(.black) .fontWeight(.medium) - + Text("\n:gradient", size: .small) + + Text("\n:gradient") .textColor(.gray) } + .textSize(.small) .padding(.small) Spacer(minLength: 0) diff --git a/Sources/OrbitStorybook/Foundation/StorybookIcons.swift b/Sources/OrbitStorybook/Foundation/StorybookIcons.swift index 4937ac19cff..d9da990e3ad 100644 --- a/Sources/OrbitStorybook/Foundation/StorybookIcons.swift +++ b/Sources/OrbitStorybook/Foundation/StorybookIcons.swift @@ -26,10 +26,12 @@ struct StorybookIcons { VStack(spacing: .xxSmall) { Icon(icon) - Text(String(describing: icon).titleCased, size: .custom(10), isSelectable: true) + Text(String(describing: icon).titleCased) .textColor(.inkNormal) + .textSize(custom: 10) - Text(String(icon.value.unicodeCodePoint), size: .custom(10), isSelectable: true) + Text(String(icon.value.unicodeCodePoint)) + .textSize(custom: 10) .padding(.horizontal, .xxSmall) .padding(.vertical, 1) .overlay( @@ -38,6 +40,7 @@ struct StorybookIcons { .textColor(.inkLight) ) } + .textIsCopyable() .padding(.horizontal, .xxSmall) .padding(.vertical, .xSmall) .frame(maxWidth: .infinity) diff --git a/Sources/OrbitStorybook/Foundation/StorybookIllustrations.swift b/Sources/OrbitStorybook/Foundation/StorybookIllustrations.swift index e5fc87173c2..4a53d670a1e 100644 --- a/Sources/OrbitStorybook/Foundation/StorybookIllustrations.swift +++ b/Sources/OrbitStorybook/Foundation/StorybookIllustrations.swift @@ -28,7 +28,9 @@ struct StorybookIllustrations { Illustration(illustration, layout: .resizeable) .frame(height: 150) - Text(String(describing: illustration).titleCased, size: .small, isSelectable: true) + Text(String(describing: illustration).titleCased) + .textSize(.small) + .textIsCopyable() .textColor(.inkNormal) } .padding(.horizontal, .xxSmall) diff --git a/Sources/OrbitStorybook/Foundation/StorybookTypography.swift b/Sources/OrbitStorybook/Foundation/StorybookTypography.swift index 8ee60035ab6..9cf5333ab89 100644 --- a/Sources/OrbitStorybook/Foundation/StorybookTypography.swift +++ b/Sources/OrbitStorybook/Foundation/StorybookTypography.swift @@ -79,7 +79,8 @@ struct StorybookTypography { @ViewBuilder static func text(_ content: String, size: Orbit.Text.Size, weight: Font.Weight) -> some View { HStack(alignment: .firstTextBaseline, spacing: .small) { - Text(content, size: size) + Text(content) + .textSize(size) .fontWeight(weight) Spacer() Text("\(Int(size.value))/\(Int(size.lineHeight))") diff --git a/Sources/OrbitStorybook/Storybook.swift b/Sources/OrbitStorybook/Storybook.swift index cabeede9b8d..dfe288097ec 100644 --- a/Sources/OrbitStorybook/Storybook.swift +++ b/Sources/OrbitStorybook/Storybook.swift @@ -117,9 +117,9 @@ public struct Storybook: View { @ViewBuilder func tileContent(_ item: Item) -> some View { HStack(alignment: .firstTextBaseline, spacing: 0) { if item.tabs.isEmpty { - Icon(.timelapse, size: .small) + Icon(.timelapse) } else { - Icon(item.sfSymbol, size: .small) + Icon(item.sfSymbol) } Heading(String(describing: item).titleCased, style: .title6) @@ -127,6 +127,7 @@ public struct Storybook: View { Spacer(minLength: 0) } + .iconSize(.small) .padding(.small) }