From d83b84f75764c77dcae4e294d698c3448e4df596 Mon Sep 17 00:00:00 2001 From: Brendan Lensink Date: Thu, 24 Nov 2022 13:59:44 -0800 Subject: [PATCH] Clean up, document and prepare for 1.0 release (#42) * remove stateful view and view+if helpers, we're going to move them into a different utils library and keep nice components nice and simple * save pointing a bunch of polish * clean up documentation for lots more files * finish up updating documentation * shadowStyle -> NiceShadowStyle * fix up readme a little * fix needing to declare color * update changelog * undo change from niceFontStyle -> fontStyle, shouldn't be needed * add copyright notice to all file headers --- .../contents.xcworkspacedata | 7 + CHANGELOG.md | 6 + CONTRIBUTING.md | 2 +- LICENSE | 2 +- .../project.pbxproj | 2 - .../NiceComponentsExample/ContentView.swift | 3 +- .../NiceComponentsExampleApp.swift | 3 +- .../Resources/Theme.swift | 13 +- .../View/AllComponentsView.swift | 21 +- .../View/NiceButtonExampleView.swift | 20 +- .../View/SampleSignInView.swift | 5 +- Package.resolved | 16 ++ README.md | 37 +-- .../Button/BorderlessButton.swift | 26 +- .../Button/DestructiveButton.swift | 23 +- .../NiceComponents/Button/NiceButton.swift | 231 ++++++++++++++++++ .../NiceComponents/Button/PrimaryButton.swift | 24 +- .../Button/SecondaryButton.swift | 24 +- .../NiceComponents/Components/ErrorView.swift | 3 +- .../Components/LoadingView.swift | 33 --- .../Components/NiceButton.swift | 150 ------------ .../{ResizableImage.swift => NiceImage.swift} | 43 +++- .../NiceComponents/Components/NiceText.swift | 67 ----- .../Components/NoDataView.swift | 29 --- .../Components/ThemedDivider.swift | 21 +- .../Helper/ActivityIndicator.swift | 27 -- Sources/NiceComponents/Helper/Color+Hex.swift | 10 +- .../Helper/DynamicTypeSize+Max.swift | 13 +- .../Helper/Font+TypeStyle.swift | 25 +- .../NiceComponents/Helper/ScaledFont.swift | 28 ++- .../Modifiers/NiceShadowModifier.swift | 38 +++ .../Modifiers/ShadowModifier.swift | 31 --- Sources/NiceComponents/Text/BodyText.swift | 25 +- Sources/NiceComponents/Text/DetailText.swift | 25 +- Sources/NiceComponents/Text/NiceText.swift | 99 ++++++++ Sources/NiceComponents/Theme/ColorTheme.swift | 76 ++++-- .../shadow.colorset/Contents.json | 38 +++ Sources/NiceComponents/Theme/Config.swift | 94 +++---- Sources/NiceComponents/Theme/FontTheme.swift | 50 ++++ Sources/NiceComponents/Theme/Layout.swift | 25 -- .../Theme/NiceBorderStyle.swift | 62 ++++- .../Theme/NiceButtonStyle.swift | 123 +++++----- .../NiceComponents/Theme/NiceFontStyle.swift | 45 ++++ .../NiceComponents/Theme/NiceSpacing.swift | 25 ++ .../NiceComponents/Theme/NiceTextStyle.swift | 68 ++++++ Sources/NiceComponents/Theme/TypeStyle.swift | 37 --- Sources/NiceComponents/Theme/TypeTheme.swift | 71 ------ Sources/NiceComponents/Title/ItemTitle.swift | 28 ++- .../NiceComponents/Title/ScreenTitle.swift | 26 +- .../NiceComponents/Title/SectionTitle.swift | 27 +- nice_components.png | Bin 0 -> 48190 bytes 51 files changed, 1189 insertions(+), 738 deletions(-) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 Package.resolved create mode 100644 Sources/NiceComponents/Button/NiceButton.swift delete mode 100644 Sources/NiceComponents/Components/LoadingView.swift delete mode 100644 Sources/NiceComponents/Components/NiceButton.swift rename Sources/NiceComponents/Components/{ResizableImage.swift => NiceImage.swift} (67%) delete mode 100644 Sources/NiceComponents/Components/NiceText.swift delete mode 100644 Sources/NiceComponents/Components/NoDataView.swift delete mode 100644 Sources/NiceComponents/Helper/ActivityIndicator.swift create mode 100644 Sources/NiceComponents/Modifiers/NiceShadowModifier.swift delete mode 100644 Sources/NiceComponents/Modifiers/ShadowModifier.swift create mode 100644 Sources/NiceComponents/Text/NiceText.swift create mode 100644 Sources/NiceComponents/Theme/Colors.xcassets/shadow.colorset/Contents.json create mode 100644 Sources/NiceComponents/Theme/FontTheme.swift delete mode 100644 Sources/NiceComponents/Theme/Layout.swift create mode 100644 Sources/NiceComponents/Theme/NiceFontStyle.swift create mode 100644 Sources/NiceComponents/Theme/NiceSpacing.swift create mode 100644 Sources/NiceComponents/Theme/NiceTextStyle.swift delete mode 100644 Sources/NiceComponents/Theme/TypeStyle.swift delete mode 100644 Sources/NiceComponents/Theme/TypeTheme.swift create mode 100644 nice_components.png diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index fa64c1a..43bc612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.0] +- Lots of prep for initial public release! +- Added a bunch of documentation, comments and clarification. +- Removed some unused, or outdated components that have SwiftUI equivalents now. +- Renamed a handful of components and helpers to be more clear or avoid potential collisions. + ## [0.6.0] - Removed stateful view and view+if helpers, to be moved into a separate utils library. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e783e7..83819fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,5 @@ Welcome! -Our project is small, so we're happy to receive feedback and bug reports via Github issues. @apike or @brendanlensink will work to triage and respond. You can also email us, contact@steamclock.com. +Our project is small, so we're happy to receive feedback and bug reports via Github issues. @brendanlensink will work to triage and respond. You can also email us, contact@steamclock.com. If you'd like to submit a pull request that doesn't fix something there's already an open issue for, it's probably best to start with filing an issue about what change you'd like to make. diff --git a/LICENSE b/LICENSE index 9a18585..1b8ed73 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Steamclock Software +Copyright (c) 2022 Steamclock Software Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/NiceComponentsExample/NiceComponentsExample.xcodeproj/project.pbxproj b/NiceComponentsExample/NiceComponentsExample.xcodeproj/project.pbxproj index beb90a0..e1eac5d 100644 --- a/NiceComponentsExample/NiceComponentsExample.xcodeproj/project.pbxproj +++ b/NiceComponentsExample/NiceComponentsExample.xcodeproj/project.pbxproj @@ -24,7 +24,6 @@ C614E74126E12DAB00F7F87C /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; C628F0D125C4A08B001331AB /* NotoSerif-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSerif-Bold.ttf"; sourceTree = ""; }; C628F0D225C4A08B001331AB /* NotoSerif-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSerif-Regular.ttf"; sourceTree = ""; }; - C65289F226CF0DFF009D486B /* StatefulExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatefulExampleView.swift; sourceTree = ""; }; C671309A25C4948800F75E44 /* NiceComponentsExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NiceComponentsExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; C671309D25C4948800F75E44 /* NiceComponentsExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NiceComponentsExampleApp.swift; sourceTree = ""; }; C671309F25C4948800F75E44 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -111,7 +110,6 @@ children = ( C6CD255725D6F01C008026D5 /* AllComponentsView.swift */, C6CD255C25D6F0B1008026D5 /* SampleSignInView.swift */, - C65289F226CF0DFF009D486B /* StatefulExampleView.swift */, D204536F28860ABC00174C13 /* NiceButtonExampleView.swift */, ); path = View; diff --git a/NiceComponentsExample/NiceComponentsExample/ContentView.swift b/NiceComponentsExample/NiceComponentsExample/ContentView.swift index 647075a..392f5e4 100644 --- a/NiceComponentsExample/NiceComponentsExample/ContentView.swift +++ b/NiceComponentsExample/NiceComponentsExample/ContentView.swift @@ -2,7 +2,8 @@ // ContentView.swift // NiceComponentsExample // -// Created by Brendan on 2021-01-29. +// Created by Brendan on 2021-02-12. +// Copyright © 2022 Steamclock Software. All rights reserved. // import NiceComponents diff --git a/NiceComponentsExample/NiceComponentsExample/NiceComponentsExampleApp.swift b/NiceComponentsExample/NiceComponentsExample/NiceComponentsExampleApp.swift index 5884ff5..ad671d4 100644 --- a/NiceComponentsExample/NiceComponentsExample/NiceComponentsExampleApp.swift +++ b/NiceComponentsExample/NiceComponentsExample/NiceComponentsExampleApp.swift @@ -2,7 +2,8 @@ // NiceComponentsExampleApp.swift // NiceComponentsExample // -// Created by Brendan on 2021-01-29. +// Created by Brendan on 2021-02-12. +// Copyright © 2022 Steamclock Software. All rights reserved. // import NiceComponents diff --git a/NiceComponentsExample/NiceComponentsExample/Resources/Theme.swift b/NiceComponentsExample/NiceComponentsExample/Resources/Theme.swift index 1f846e6..bfa2aa1 100644 --- a/NiceComponentsExample/NiceComponentsExample/Resources/Theme.swift +++ b/NiceComponentsExample/NiceComponentsExample/Resources/Theme.swift @@ -11,16 +11,17 @@ import SwiftUI enum Theme { static var config: Config { var newConfig = Config() - newConfig.primaryButtonStyle = NiceComponents.NiceButtonStyle( + newConfig.primaryButtonStyle = NiceButtonStyle( surfaceColor: Color.orange, - onSurfaceColor: .black, - border: .capsule(borderWidth: 1, borderColor: .clear) + onSurfaceColor: Color.black, + border: .capsule(color: .clear, width: 1) ) - newConfig.secondaryButtonStyle = NiceComponents.NiceButtonStyle( + newConfig.secondaryButtonStyle = NiceButtonStyle( surfaceColor: Color.yellow, - onSurfaceColor: .black, - border: .rounded(radius: 12, borderWidth: 1.5, borderColor: .black) + onSurfaceColor: Color.black, + border: .rounded(color: .black, cornerRadius: 12, width: 1.5) ) + return newConfig } } diff --git a/NiceComponentsExample/NiceComponentsExample/View/AllComponentsView.swift b/NiceComponentsExample/NiceComponentsExample/View/AllComponentsView.swift index 64e0df6..4abf624 100644 --- a/NiceComponentsExample/NiceComponentsExample/View/AllComponentsView.swift +++ b/NiceComponentsExample/NiceComponentsExample/View/AllComponentsView.swift @@ -1,8 +1,9 @@ // // AllComponentsView.swift -// +// NiceComponentsExample // // Created by Brendan on 2021-02-12. +// Copyright © 2022 Steamclock Software. All rights reserved. // import NiceComponents @@ -25,14 +26,14 @@ struct AllComponentsView: View { } } - ThemedDivider() + NiceDivider() VStack(alignment: .leading, spacing: 2) { BodyText("Body Text") DetailText("Detail Text") } - ThemedDivider() + NiceDivider() VStack(alignment: .leading, spacing: 2) { PrimaryButton("Primary Button") { } @@ -41,18 +42,10 @@ struct AllComponentsView: View { DestructiveButton("Destructive Button") { } } - ThemedDivider() + NiceDivider() - HStack { - Spacer() - - LoadingView() - - Spacer() - } - - ResizableImage(URL(string: "https://placekitten.com/200/300"), width: 200, height: 300) + NiceImage(URL(string: "https://placekitten.com/200/300"), width: 200, height: 300) } - }.padding(Layout.Spacing.standard) + }.padding(NiceSpacing.standard) } } diff --git a/NiceComponentsExample/NiceComponentsExample/View/NiceButtonExampleView.swift b/NiceComponentsExample/NiceComponentsExample/View/NiceButtonExampleView.swift index e01734a..64fb60a 100644 --- a/NiceComponentsExample/NiceComponentsExample/View/NiceButtonExampleView.swift +++ b/NiceComponentsExample/NiceComponentsExample/View/NiceButtonExampleView.swift @@ -1,7 +1,9 @@ // // NiceButtonExampleView.swift +// NiceComponentsExample // -// Created by Alejandro Zielinsky on 2022-07-18. +// Created by Brendan on 2021-02-12. +// Copyright © 2022 Steamclock Software. All rights reserved. // import NiceComponents @@ -17,34 +19,34 @@ struct NiceButtonExampleView: View { PrimaryButton("Primary") { print("Tapped") }.withRightImage( - ResizableImage.init(systemIcon: "heart.fill", width: 14, height: 14, tintColor: .red) + NiceImage(systemIcon: "heart.fill", width: 14, height: 14, tintColor: .red) ) PrimaryButton("Bigger Primary", height: 56) { } - PrimaryButton("Disabled Primary", disabled: true) { } + PrimaryButton("Inactive Primary", inactive: true) { } PrimaryButton( "Like Button", surfaceColor: .white, border: .rounded( - radius: 4, - borderWidth: 1.5, - borderColor: .black + color: .black, + cornerRadius: 4, + width: 1.5 ) ) { }.withRightImage( - ResizableImage.init(systemIcon: "heart.fill", width: 14, height: 14, tintColor: .red) + NiceImage(systemIcon: "heart.fill", width: 14, height: 14, tintColor: .red) ) SecondaryButton("Left Image Button") { }.withLeftImage( - ResizableImage.init(systemIcon: "pencil", width: 14, height: 14, tintColor: .black) + NiceImage(systemIcon: "pencil", width: 14, height: 14, tintColor: .black) ) BorderlessButton("Borderless Button") { } BorderlessButton("Stroked Button", border: .stroke(strokeStyle: StrokeStyle(lineWidth: 1.5, lineCap: .round, lineJoin: .round, dash: [8]))) { } } - }.padding(Layout.Spacing.standard) + }.padding(NiceSpacing.standard) .navigationBarTitleDisplayMode(.inline) .navigationTitle("Nice Button Examples") } diff --git a/NiceComponentsExample/NiceComponentsExample/View/SampleSignInView.swift b/NiceComponentsExample/NiceComponentsExample/View/SampleSignInView.swift index d90e7e8..2b893cc 100644 --- a/NiceComponentsExample/NiceComponentsExample/View/SampleSignInView.swift +++ b/NiceComponentsExample/NiceComponentsExample/View/SampleSignInView.swift @@ -3,6 +3,7 @@ // NiceComponentsExample // // Created by Brendan on 2021-02-12. +// Copyright © 2022 Steamclock Software. All rights reserved. // import NiceComponents @@ -13,7 +14,7 @@ public struct SampleSignInView: View { @State private var passwordField: String = "" public var body: some View { - VStack(alignment: .leading, spacing: Layout.Spacing.standard) { + VStack(alignment: .leading, spacing: NiceSpacing.standard) { ScreenTitle("Sign In") DetailText("Email") @@ -27,6 +28,6 @@ public struct SampleSignInView: View { SecondaryButton("Create an Account") {} Spacer() - }.padding(Layout.Spacing.standard) + }.padding(NiceSpacing.standard) } } diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..2a760a7 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "Kingfisher", + "repositoryURL": "https://github.com/onevcat/Kingfisher.git", + "state": { + "branch": null, + "revision": "44e891bdb61426a95e31492a67c7c0dfad1f87c5", + "version": "7.4.1" + } + } + ] + }, + "version": 1 +} diff --git a/README.md b/README.md index 8d9fd8b..548e617 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Nice Components +![Nice Components](nice_components.png) A simple library with some nice looking SwiftUI components to get your next project started 🚀 @@ -12,7 +12,9 @@ Help jump start your prototypes with some sensible default components, then come You can clone and run the example project to see examples of all the default components, plus a little sample of a more customized sign in screen. -### Straight out of the Box +### Straight Out of the Box + +When you're just starting out with your project, you should be able to get some reasonable results just dropping in our components straight out of the box. ```swift import NiceComponents @@ -20,6 +22,10 @@ import NiceComponents struct DemoView: View { var body: some View { ScreenTitle("I'm a nice big title!") + + PrimaryButton("And I'm a nice little button") { + doTheThing() + } } } @@ -31,7 +37,7 @@ Once you're ready to start putting your own touch on components, you've got a co #### Setting a Global Config at Startup -If you'd like to change _all_ instances of a component, we recommend creating a custom config that you can set when your app first starts. Note that you once you've set this config, you'll be unable to update it. +If you'd like to change _all_ instances of a component, we recommend creating a custom config that you can set when your app first starts. Note that you once you've set this config once, you'll be unable to update it. In the case of multiple customizations applying to the same component, the _most specific_ one will take precedence. @@ -51,7 +57,6 @@ import NiceComponents } ``` - #### Extending an Existing Component ```swift @@ -62,9 +67,9 @@ public struct CustomPrimaryButton: View { var body: some View { PrimaryButton( text, - style: NiceComponents.ButtonStyle( - textStyle: Config.current.typeTheme.body1, - surfaceColor: Color.red, + style: NiceButtonStyle( + fontStyle: FontStyle(size: 16), + surfaceColor: .red, onSurfaceColor: .black ) onClick: onClick @@ -82,9 +87,9 @@ var body: some View { PrimaryButton( "Tap me!", style: NiceComponents.ButtonStyle( - textStyle: Config.current.typeTheme.body1, - surfaceColor: Color.red, - onSurfaceColor: .black + fontStyle: FontStyle(size: 16), + surfaceColor: .red, + onSurfaceColor: .black ) ) { print("I've been tapped!") @@ -128,7 +133,7 @@ import NiceComponents #### Setting a Custom Font -Just like how you can set a `colorTheme`, you can also set a `typeTheme` that defines the default font, size and weight for all components. +Just like how you can set a `colorTheme`, you can also set a `fontTheme` that defines the default font, size and weight for all components. | Component | Type Name | | ------------- | ------ | @@ -136,14 +141,14 @@ Just like how you can set a `colorTheme`, you can also set a `typeTheme` that de | Secondary Button | button | | Inactive Button | button | | Destructive Button | button | -| Body Text | body1 | -| Detail Text | body1 | +| Body Text | body | +| Detail Text | detail | ```swift var newConfig = Config() - newConfig.primaryButtonStyle.textStyle = TypeTheme.Text( - name: "Comic Sans MS", - size: 16, + newConfig.primaryButtonStyle.fontStyle = FontStyle( + "Comic Sans MS", + size: 16, weight: .semibold ) Config.current = newConfig diff --git a/Sources/NiceComponents/Button/BorderlessButton.swift b/Sources/NiceComponents/Button/BorderlessButton.swift index 16bb936..1ad2f1c 100644 --- a/Sources/NiceComponents/Button/BorderlessButton.swift +++ b/Sources/NiceComponents/Button/BorderlessButton.swift @@ -1,36 +1,48 @@ // // BorderlessButton.swift -// +// NiceComponents // // Created by Brendan on 2021-07-13. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI +/// A themed button with no border. public struct BorderlessButton: NiceButton { public let text: String + public let inactive: Bool public let style: NiceButtonStyle public let action: () -> Void - public let inactive: Bool - public var leftImage: ResizableImage? - public var rightImage: ResizableImage? - public var rightImageOffset: CGFloat? + public var leftImage: NiceImage? + public var rightImage: NiceImage? + public var leftImageOffset: CGFloat? + public var rightImageOffset: CGFloat? public static var defaultStyle: NiceButtonStyle { Config.current.borderlessButtonStyle } + /** + * Create a new borderless button. + * + * - Parameters: + * - text: The body text of the button. + * - inactive: Whether the button should be interactable or not. Default is `false`. + * - style: The styling to apply to the button. Defaults to the current `borderlessButtonStyle` in your config. + * - action: The action to be performed when the button is tapped. + */ public init( _ text: String, + inactive: Bool = false, style: NiceButtonStyle? = nil, - disabled: Bool = false, action: @escaping () -> Void ) { self.text = text + self.inactive = inactive self.style = style ?? Config.current.borderlessButtonStyle - self.inactive = disabled self.action = action } } diff --git a/Sources/NiceComponents/Button/DestructiveButton.swift b/Sources/NiceComponents/Button/DestructiveButton.swift index 895730c..df1a7db 100644 --- a/Sources/NiceComponents/Button/DestructiveButton.swift +++ b/Sources/NiceComponents/Button/DestructiveButton.swift @@ -1,20 +1,22 @@ // // DestructiveButton.swift -// +// NiceComponents // // Created by Brendan on 2021-02-05. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI +/// A button themed to signal that the action associated with it is destructive. public struct DestructiveButton: NiceButton { public let text: String + public let inactive: Bool public let style: NiceButtonStyle public let action: () -> Void - public let inactive: Bool - public var leftImage: ResizableImage? - public var rightImage: ResizableImage? + public var leftImage: NiceImage? + public var rightImage: NiceImage? public var rightImageOffset: CGFloat? public var leftImageOffset: CGFloat? @@ -22,15 +24,24 @@ public struct DestructiveButton: NiceButton { Config.current.destructiveButtonStyle } + /* + * Create a new destructive button. + * + * - Parameters: + * - text: The body text of the button. + * - inactive: Whether the button should be interactable or not. Default is `false`. + * - style: The styling to apply to the button. Defaults to the current `destructiveButtonStyle` in your config. + * - action: The action to be performed when the button is tapped. + */ public init( _ text: String, + inactive: Bool = false, style: NiceButtonStyle? = nil, - disabled: Bool = false, action: @escaping () -> Void ) { self.text = text + self.inactive = false self.style = style ?? Config.current.destructiveButtonStyle - self.inactive = disabled self.action = action } } diff --git a/Sources/NiceComponents/Button/NiceButton.swift b/Sources/NiceComponents/Button/NiceButton.swift new file mode 100644 index 0000000..6bef25d --- /dev/null +++ b/Sources/NiceComponents/Button/NiceButton.swift @@ -0,0 +1,231 @@ +// +// NiceButton.swift +// NiceComponents +// +// Created by Alejandro Zielinsky on 2022-07-18. +// Copyright © 2022 Steamclock Software. All rights reserved. +// + +import SwiftUI + +/// Defines a structure for buttons presented and managed with NiceComponents. +public protocol NiceButton: View { + associatedtype DefaultBody: View + + /// Text shown inside the button. + var text: String { get } + + /// An inactive button will not trigger its `action` when tapped. + var inactive: Bool { get } + + /// Styling to apply to the button. + var style: NiceButtonStyle { get } + + /// The action to be performed when the button is pressed. + var action: () -> Void { get } + + /// The default style that should be applied to an instance of the button if a style is not provided. + static var defaultStyle: NiceButtonStyle { get } + + @ViewBuilder var defaultBody: DefaultBody { get } + + /// An image that will show to the left of the text. + var leftImage: NiceImage? { get set } + + /// An image that will show to the right of the text. + var rightImage: NiceImage? { get set } + + /// If a `leftImage` is provided, the offset between it and the text. + var leftImageOffset: CGFloat? { get set } + + /// If a `rightImage` is provided, the offset between it and the text. + var rightImageOffset: CGFloat? { get set } + + /// Add an image to the left of any text. + mutating func addLeftImage(_ image: NiceImage?, offset: CGFloat) + + /// Add an image to the right of any text. + mutating func addRightImage(_ image: NiceImage?, offset: CGFloat) + + /** + * Create a new button with the given content and style + * + * - Parameters: + * - text: The text to show in the button. + * - inactive: Whether the button should be interactable or not. Default is `false`. + * - style: The style to apply to the button. Will default to `defaultButtonStyle` if not provided. + * - action: The action to be performed when the button is tapped. + */ + init( + _ text: String, + inactive: Bool, + style: NiceButtonStyle?, + action: @escaping () -> Void + ) +} + +public extension NiceButton { + /** + * Create a new button with the given content and style options. + * + * - Parameters: + * - text: The text to show inside the button. + * - fontStyle: The style to apply to the button text. + * - height: The height of the button. + * - inactive: Whether the button should be interactable or not. Default is `false`. + * - surfaceColor: Surface color of the button. + * - onSurfaceColor: Color of any assets on top of your button. + * - inactiveSurfaceColor: Surface color when set to inactive. Default is your background color. + * - inactiveOnSurfaceColor: Color of any assets on top of your button when inactive. Default is your secondary color. + * - border: Border style for the button. Default is none. + * - action: The action to perform when the button is tapped. + */ + init( + _ text: String, + fontStyle: FontStyle? = nil, + height: CGFloat? = nil, + inactive: Bool = false, + surfaceColor: Color? = nil, + onSurfaceColor: Color? = nil, + inactiveSurfaceColor: Color? = nil, + inactiveOnSurfaceColor: Color? = nil, + border: NiceBorderStyle? = nil, + action: @escaping () -> Void + ) { + self.init( + text, + inactive: inactive, + style: + Self.defaultStyle.with( + fontStyle: fontStyle, + height: height, + surfaceColor: surfaceColor, + onSurfaceColor: onSurfaceColor, + inactiveSurfaceColor: inactiveSurfaceColor, + inactiveOnSurfaceColor: inactiveOnSurfaceColor, + border: border + ), + action: action + ) + } +} + +extension NiceButton { + public var body: some View { + defaultBody + } + + public var defaultBody: some View { + Button(action: action) { + HStack(spacing: 0) { + if let leftImage = leftImage { + leftImage + } + Text(text) + .foregroundColor(inactive ? style.inactiveOnSurfaceColor : style.onSurfaceColor) + .scaledFont( + name: style.fontStyle.name, + size: style.fontStyle.size, + weight: style.fontStyle.weight, + maxSize: style.fontStyle.dynamicTypeMaxSize + ) + .padding(.leading, leftImageOffset) + .padding(.trailing, rightImageOffset) + if let rightImage = rightImage { + rightImage + } + } + .frame(maxWidth: .infinity) + } + .disabled(inactive) + .frame(height: style.height) + .fixedSize(horizontal: false, vertical: true) + .background(inactive ? style.inactiveSurfaceColor : style.surfaceColor) + .cornerRadius(style.border.cornerRadius) + .overlay( + borderOverlay + ) + .padding(paddingToAdd) + + } + + private var paddingToAdd: CGFloat { + if let strokeWidth = style.border.strokeStyle?.lineWidth, strokeWidth > 0.0 { + return strokeWidth / 2 + } else if style.border.width > 0.0 { + return style.border.width / 2 + } + return 0.0 + } + + @ViewBuilder + private var borderOverlay: some View { + if let strokeStyle = style.border.strokeStyle { + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(style: strokeStyle) + } else { + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(style.border.color, lineWidth: style.border.width) + } + } + + private var cornerRadius: CGFloat { + if case .capsule = style.border { + return style.height / 2 + } + + return style.border.cornerRadius + } +} + +public extension NiceButton { + /** + * Add an image to the left of the button. + * + * - Parameters: + * - image: The image to display. + * - offset: The offset to apply to the image. Default is 8. + */ + mutating func addLeftImage(_ image: NiceImage?, offset: CGFloat = 8.0) { + self.leftImage = image + self.leftImageOffset = offset + } + + /** + * Add an image to the right of the button. + * + * - Parameters: + * - image: The image to display. + * - offset: The offset to apply to the image. Default is 8. + */ + mutating func addRightImage(_ image: NiceImage?, offset: CGFloat = 8.0) { + self.rightImage = image + self.rightImageOffset = offset + } + + /** + * Add an image to the left of the given button. + * + * - Parameters: + * - image: The image to display. + * - offset: The offset to apply to the image. Default is 8. + */ + func withLeftImage(_ image: NiceImage?, offset: CGFloat = 8.0) -> Self { + var copy = self + copy.addLeftImage(image, offset: offset) + return copy + } + + /** + * Add an image to the right of the given button. + * + * - Parameters: + * - image: The image to display. + * - offset: The offset to apply to the image. Default is 8. + */ + func withRightImage(_ image: NiceImage?, offset: CGFloat = 8.0) -> Self { + var copy = self + copy.addRightImage(image, offset: offset) + return copy + } +} diff --git a/Sources/NiceComponents/Button/PrimaryButton.swift b/Sources/NiceComponents/Button/PrimaryButton.swift index c18c55a..15acf98 100644 --- a/Sources/NiceComponents/Button/PrimaryButton.swift +++ b/Sources/NiceComponents/Button/PrimaryButton.swift @@ -1,21 +1,22 @@ // // PrimaryButton.swift -// +// NiceComponents // // Created by Brendan on 2021-01-29. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI +/// A button themed to indicate a primary action. public struct PrimaryButton: NiceButton { - public let text: String + public let inactive: Bool public let style: NiceButtonStyle public let action: () -> Void - public let inactive: Bool - public var leftImage: ResizableImage? - public var rightImage: ResizableImage? + public var leftImage: NiceImage? + public var rightImage: NiceImage? public var rightImageOffset: CGFloat? public var leftImageOffset: CGFloat? @@ -23,15 +24,24 @@ public struct PrimaryButton: NiceButton { Config.current.primaryButtonStyle } + /* + * Create a new primary button. + * + * - Parameters: + * - text: The body text of the button. + * - inactive: Whether the button should be interactable or not. Default is `false`. + * - style: The styling to apply to the button. Defaults to the current `primaryButtonStyle` in your config. + * - action: The action to be performed when the button is tapped. + */ public init( _ text: String, + inactive: Bool = false, style: NiceButtonStyle? = nil, - disabled: Bool = false, action: @escaping () -> Void ) { self.text = text + self.inactive = inactive self.style = style ?? Config.current.primaryButtonStyle - self.inactive = disabled self.action = action } } diff --git a/Sources/NiceComponents/Button/SecondaryButton.swift b/Sources/NiceComponents/Button/SecondaryButton.swift index 48ce814..88c27c9 100644 --- a/Sources/NiceComponents/Button/SecondaryButton.swift +++ b/Sources/NiceComponents/Button/SecondaryButton.swift @@ -1,21 +1,22 @@ // // SecondaryButton.swift -// +// NiceComponents // // Created by Brendan on 2021-01-29. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI +/// A button themed to indicate a secondary action. public struct SecondaryButton: NiceButton { - public let text: String + public let inactive: Bool public let style: NiceButtonStyle public let action: () -> Void - public let inactive: Bool - public var leftImage: ResizableImage? - public var rightImage: ResizableImage? + public var leftImage: NiceImage? + public var rightImage: NiceImage? public var rightImageOffset: CGFloat? public var leftImageOffset: CGFloat? @@ -23,15 +24,24 @@ public struct SecondaryButton: NiceButton { Config.current.secondaryButtonStyle } + /* + * Create a new secondary button. + * + * - Parameters: + * - text: The body text of the button. + * - inactive: Whether the button should be interactable or not. Default is `false`. + * - style: The styling to apply to the button. Defaults to the current `secondaryButtonStyle` in your config. + * - action: The action to be performed when the button is tapped. + */ public init( _ text: String, + inactive: Bool = false, style: NiceButtonStyle? = nil, - disabled: Bool = false, action: @escaping () -> Void ) { self.text = text + self.inactive = inactive self.style = style ?? Config.current.secondaryButtonStyle - self.inactive = disabled self.action = action } } diff --git a/Sources/NiceComponents/Components/ErrorView.swift b/Sources/NiceComponents/Components/ErrorView.swift index dde41d0..59e6590 100644 --- a/Sources/NiceComponents/Components/ErrorView.swift +++ b/Sources/NiceComponents/Components/ErrorView.swift @@ -1,8 +1,9 @@ // // ErrorView.swift -// +// NiceComponents // // Created by Brendan on 2022-07-15. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI diff --git a/Sources/NiceComponents/Components/LoadingView.swift b/Sources/NiceComponents/Components/LoadingView.swift deleted file mode 100644 index a3a5745..0000000 --- a/Sources/NiceComponents/Components/LoadingView.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// LoadingView.swift -// -// -// Created by Brendan on 2021-08-12. -// - -import SwiftUI -import UIKit - -public struct LoadingView: View { - let style: UIActivityIndicatorView.Style - let footer: () -> Footer - - public init(_ style: UIActivityIndicatorView.Style = .large, footer: @escaping () -> Footer) { - self.style = style - self.footer = footer - } - - public var body: some View { - VStack(alignment: .center) { - ActivityIndicator(isAnimating: true) { $0.style = style } - - footer() - } - } -} - -public extension LoadingView where Footer == EmptyView { - init(_ style: UIActivityIndicatorView.Style = .large) { - self.init(style, footer: { EmptyView() }) - } -} diff --git a/Sources/NiceComponents/Components/NiceButton.swift b/Sources/NiceComponents/Components/NiceButton.swift deleted file mode 100644 index f8c599c..0000000 --- a/Sources/NiceComponents/Components/NiceButton.swift +++ /dev/null @@ -1,150 +0,0 @@ -// -// NiceButton.swift -// -// -// Created by Alejandro Zielinsky on 2022-07-18. -// - -import SwiftUI - -public protocol NiceButton: View { - associatedtype DefaultBody : View - var text: String { get } - var style: NiceButtonStyle { get } - var action: () -> Void { get } - var inactive: Bool { get } - static var defaultStyle: NiceButtonStyle { get } - @ViewBuilder var defaultBody: DefaultBody { get } - - var leftImage: ResizableImage? { get set } - var rightImage: ResizableImage? { get set } - var leftImageOffset: CGFloat? { get set } - var rightImageOffset: CGFloat? { get set } - - mutating func addLeftImage(_ image: ResizableImage?, spacing: CGFloat) - mutating func addRightImage(_ image: ResizableImage?, spacing: CGFloat) - - init( - _ text: String, - style: NiceButtonStyle?, - disabled: Bool, - action: @escaping () -> Void - ) -} - -public extension NiceButton { - init( - _ text: String, - disabled: Bool = false, - textStyle: TypeTheme.TextStyle? = nil, - height: CGFloat? = nil, - surfaceColor: Color? = nil, - onSurfaceColor: Color? = nil, - disabledColor: Color? = nil, - disabledOnSurfaceColor: Color? = nil, - border: NiceBorderStyle? = nil, - action: @escaping () -> Void - ) { - self.init( - text, - style: - Self.defaultStyle.with( - textStyle: textStyle, - height: height, - surfaceColor: surfaceColor, - onSurfaceColor: onSurfaceColor, - disabledSurfaceColor: disabledColor, - disabledOnSurfaceColor: disabledOnSurfaceColor, - border: border - ), - disabled: disabled, - action: action - ) - } -} - -extension NiceButton { - - public var body: some View { - defaultBody - } - - public var defaultBody: some View { - Button(action: action) { - HStack(spacing: 0) { - if let leftImage = leftImage { - leftImage - } - Text(text) - .foregroundColor(inactive ? style.disabledOnSurfaceColor : style.onSurfaceColor) - .scaledFont( - name: style.textStyle.name, - size: style.textStyle.size, - weight: style.textStyle.weight, - maxSize: style.textStyle.dynamicTypeMaxSize - ) - .padding(.leading, leftImageOffset) - .padding(.trailing, rightImageOffset) - if let rightImage = rightImage { - rightImage - } - } - .frame(maxWidth: .infinity) - } - .disabled(inactive) - .frame(height: style.height) - .fixedSize(horizontal: false, vertical: true) - .background(inactive ? style.disabledSurfaceColor : style.surfaceColor) - .cornerRadius(style.cornerRadius) - .overlay( - borderOverlay - ) - .padding(paddingToAdd) - - } - - private var paddingToAdd: CGFloat { - if let strokeWidth = style.strokeStyle?.lineWidth, strokeWidth > 0.0 { - return strokeWidth / 2 - } else if style.borderWidth > 0.0 { - return style.borderWidth / 2 - } - return 0.0 - } - - @ViewBuilder - private var borderOverlay: some View { - if let strokeStyle = style.strokeStyle { - RoundedRectangle(cornerRadius: style.cornerRadius) - .stroke(style: strokeStyle) - } else { - RoundedRectangle(cornerRadius: style.cornerRadius) - .stroke(style.borderColor, lineWidth: style.borderWidth) - } - } -} - -public extension NiceButton { - - mutating func addLeftImage(_ image: ResizableImage?, spacing: CGFloat) { - self.leftImage = image - self.leftImageOffset = spacing - } - - mutating func addRightImage(_ image: ResizableImage?, spacing: CGFloat) { - self.rightImage = image - self.rightImageOffset = spacing - } - - func withLeftImage(_ image: ResizableImage?, spacing: CGFloat = 8.0) -> Self { - var copy = self - copy.addLeftImage(image, spacing: spacing) - return copy - } - - func withRightImage(_ image: ResizableImage?, spacing: CGFloat = 8.0) -> Self { - var copy = self - copy.addRightImage(image, spacing: spacing) - return copy - } -} diff --git a/Sources/NiceComponents/Components/ResizableImage.swift b/Sources/NiceComponents/Components/NiceImage.swift similarity index 67% rename from Sources/NiceComponents/Components/ResizableImage.swift rename to Sources/NiceComponents/Components/NiceImage.swift index 190f566..c425f79 100644 --- a/Sources/NiceComponents/Components/ResizableImage.swift +++ b/Sources/NiceComponents/Components/NiceImage.swift @@ -1,15 +1,17 @@ // -// ResizableImage.swift -// +// NiceImage.swift +// NiceComponents // // Created by Brendan on 2021-03-12. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI import UIKit import Kingfisher -public struct ResizableImage: View { +/// Image View that allows for creating an image through a variety of sources, including bundleString, systemIcon or URL. +public struct NiceImage: View { public let bundleString: String? public let systemIcon: String? public let url: URL? @@ -22,6 +24,16 @@ public struct ResizableImage: View { @State private var didErrorWithNoFallback: Bool = false + /** + * Create a new image from an asset located in the bundle. + * + * - Parameters: + * - bundleString: The name of the image asset. + * - width: The width of the image. + * - height: The height of the image. + * - tintColor: Optional color to tint the image. Default is `nil`. + * - contentMode: Content mode for the image. Default is `.fill`. + */ public init( _ bundleString: String, width: CGFloat, @@ -40,6 +52,16 @@ public struct ResizableImage: View { self.fallbackImage = nil } + /** + * Create a new image from a system icon. + * + * - Parameters: + * - systemIcon: The name of the icon to use. + * - width: The width of the image. + * - height: The height of the image. + * - tintColor: Optional color to tint the image. Default is `nil`. + * - contentMode: Content mode for the image. Default is `.fill`. + */ public init( systemIcon: String, width: CGFloat, @@ -58,6 +80,19 @@ public struct ResizableImage: View { self.fallbackImage = nil } + /** + * Create a new image from an URL. + * Under the hood, we use Kingfisher to fetch and cache the image. + * + * - Parameters: + * - url: The URL of the image to fetch. + * - width: The width of the image. + * - height: The height of the image. + * - tintColor: Optional color to tint the image. Default is `nil`. + * - fallbackImage: The bundle string for a fallback image to show if something goes wrong. Default is `nil`. + * - contentMode: Content mode for the image. Default is `.fill`. + * - loadingStyle: The UIActivityIndicatorView.Style to use while loading. Default is `nil`. + */ public init( _ url: URL?, width: CGFloat, @@ -101,7 +136,7 @@ public struct ResizableImage: View { .renderingMode(tintColor == nil ? .original : .template) .resizable() .placeholder { - LoadingView(loadingStyle ?? .large) + ProgressView() } .onFailure { _ in if fallbackImage == nil { diff --git a/Sources/NiceComponents/Components/NiceText.swift b/Sources/NiceComponents/Components/NiceText.swift deleted file mode 100644 index 9736474..0000000 --- a/Sources/NiceComponents/Components/NiceText.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// NiceText.swift -// -// -// Created by Alejandro Zielinsky on 2022-06-09. -// - -import SwiftUI - -public protocol NiceText: View { - - var text: AttributedString { get } - var style: TypeStyle { get } - static var defaultStyle: TypeStyle { get } - - init(_ attributedText: AttributedString, style: TypeStyle?) - - init(_ text: String, style: TypeStyle?) - - init(_ text: String, color: Color?, size: CGFloat?, lineLimit: Int?, dynamicMaxSize: DynamicTypeSize?) - - init(_ text: String, color: Color?, size: CGFloat?, lineLimit: Int?, dynamicMaxSize: DynamicTypeSize?, configure: (inout AttributedString) -> Void) -} - -public extension NiceText { - - init( - _ text: String, - color: Color? = nil, - size: CGFloat? = nil, - lineLimit: Int? = nil, - dynamicMaxSize: DynamicTypeSize? = nil - ) { - self.init( - text, - style: Self.defaultStyle.with( - color: color, - size: size, - lineLimit: lineLimit, - dynamicTypeMaxSize: dynamicMaxSize - ) - ) - } - - init(_ text: String, style: TypeStyle?) { - self.init(AttributedString(text), style: style) - } - - init( - _ text: String, - color: Color? = nil, - size: CGFloat? = nil, - lineLimit: Int? = nil, - dynamicMaxSize: DynamicTypeSize? = nil, - configure: (inout AttributedString) -> Void - ) { - var attributedString = AttributedString(text) - configure(&attributedString) - self.init(attributedString, style: Self.defaultStyle.with( - color: color, - size: size, - lineLimit: lineLimit, - dynamicTypeMaxSize: dynamicMaxSize - )) - } -} - diff --git a/Sources/NiceComponents/Components/NoDataView.swift b/Sources/NiceComponents/Components/NoDataView.swift deleted file mode 100644 index a6ba449..0000000 --- a/Sources/NiceComponents/Components/NoDataView.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// NoDataView.swift -// -// -// Created by Brendan on 2022-07-15. -// - -import SwiftUI - -public struct NoDataView: View { - private let message: String - - public init(message: String? = nil) { - let defaultMessage = "No data." - self.message = message ?? defaultMessage - } - - public var body: some View { - VStack(alignment: .center) { - BodyText(message) - } - } -} - -struct NoDataView_Previews: PreviewProvider { - static var previews: some View { - NoDataView() - } -} diff --git a/Sources/NiceComponents/Components/ThemedDivider.swift b/Sources/NiceComponents/Components/ThemedDivider.swift index 20f518c..14d1f9f 100644 --- a/Sources/NiceComponents/Components/ThemedDivider.swift +++ b/Sources/NiceComponents/Components/ThemedDivider.swift @@ -1,22 +1,33 @@ // -// ThemedDivider.swift -// +// NiceDivider.swift +// NiceComponents // // Created by Brendan on 2021-03-12. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI -public struct ThemedDivider: View { +/// A themed divider you can use to separate content on the page. +public struct NiceDivider: View { private var color: Color + private var opacity: CGFloat - public init(color: Color? = nil) { + /** + * Create a new themed divider. + * + * - parameter color: The color the divider should be. Default is your config's onPrimary. + * - parameter opacity: The opacity to use for the divider. Default is 0.6. + */ + public init(color: Color? = nil, opacity: CGFloat? = nil) { self.color = color ?? Config.current.colorTheme.onPrimary + self.opacity = opacity ?? 0.6 } public var body: some View { Divider() .background(color) - .opacity(0.6) + .opacity(opacity) + .frame(height: 1) } } diff --git a/Sources/NiceComponents/Helper/ActivityIndicator.swift b/Sources/NiceComponents/Helper/ActivityIndicator.swift deleted file mode 100644 index a95a4d9..0000000 --- a/Sources/NiceComponents/Helper/ActivityIndicator.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// File.swift -// -// -// Created by Brendan on 2021-08-23. -// - -import SwiftUI -import UIKit - -public struct ActivityIndicator: UIViewRepresentable { - public typealias UIView = UIActivityIndicatorView - - var isAnimating: Bool - var configuration = { (_: UIView) in } - - public init(isAnimating: Bool, configuration: @escaping (ActivityIndicator.UIView) -> ()) { - self.isAnimating = isAnimating - self.configuration = configuration - } - - public func makeUIView(context: UIViewRepresentableContext) -> UIView { UIView() } - public func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext) { - isAnimating ? uiView.startAnimating() : uiView.stopAnimating() - configuration(uiView) - } -} diff --git a/Sources/NiceComponents/Helper/Color+Hex.swift b/Sources/NiceComponents/Helper/Color+Hex.swift index 262c0ee..5ecaea1 100644 --- a/Sources/NiceComponents/Helper/Color+Hex.swift +++ b/Sources/NiceComponents/Helper/Color+Hex.swift @@ -1,14 +1,20 @@ // // Color+Hex.swift -// +// NiceComponents // // Created by Brendan on 2021-02-05. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI -// https://stackoverflow.com/a/56874327 extension Color { + /** + * Create a new color from a hex string + * From https://stackoverflow.com/a/56874327 + * + * - parameter hex: The hex string to create a color from. Can be passed with or without #. + */ public init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 diff --git a/Sources/NiceComponents/Helper/DynamicTypeSize+Max.swift b/Sources/NiceComponents/Helper/DynamicTypeSize+Max.swift index b9982dc..825e425 100644 --- a/Sources/NiceComponents/Helper/DynamicTypeSize+Max.swift +++ b/Sources/NiceComponents/Helper/DynamicTypeSize+Max.swift @@ -1,15 +1,22 @@ // // DynamicTypeSize.swift -// +// NiceComponents // // Created by Alejandro Zielinsky on 2022-08-18. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI public extension DynamicTypeSize { - - /// Based off of iOS scaling logic https://developer.apple.com/design/human-interface-guidelines/foundations/typography/#dynamic-type-sizes + /** + * Gets the max font size for a given base size, based on the given dynamic type size. + * Max size was determined based off the iOS scaling logic given [here](https://developer.apple.com/design/human-interface-guidelines/foundations/typography/#dynamic-type-sizes) + * + * - parameter baseSize: The original size of the font to be scaled + * + * - returns: The new scaled font size. + */ func getMaxFontSize(for baseSize: CGFloat) -> CGFloat? { var resultSize: CGFloat = baseSize diff --git a/Sources/NiceComponents/Helper/Font+TypeStyle.swift b/Sources/NiceComponents/Helper/Font+TypeStyle.swift index fb61124..6f9b562 100644 --- a/Sources/NiceComponents/Helper/Font+TypeStyle.swift +++ b/Sources/NiceComponents/Helper/Font+TypeStyle.swift @@ -1,18 +1,33 @@ // // Font+TypeStyle.swift -// +// NiceComponents // // Created by Alejandro Zielinsky on 2022-06-30. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI public extension Font { - static func custom(_ typeStyle: TypeStyle) -> Font { - if let fontName = typeStyle.theme.name { - return .custom(fontName, size: typeStyle.theme.size) + /** + * Create a custom Font from a given FontStyle + * + * - Parameter fontStyle: The styling to use when creating a Font. + */ + static func custom(_ fontStyle: FontStyle) -> Font { + if let fontName = fontStyle.name { + return .custom(fontName, size: fontStyle.size) } else { - return .system(size: typeStyle.theme.size, weight: typeStyle.theme.weight ?? .regular) + return .system(size: fontStyle.size, weight: fontStyle.weight ?? .regular) } } + + /** + * Create a custom Font from a given NiceTextStyle + * + * - Parameter textStyle: The styling to use when creating a Font. + */ + static func custom(_ textStyle: NiceTextStyle) -> Font { + custom(textStyle.fontStyle) + } } diff --git a/Sources/NiceComponents/Helper/ScaledFont.swift b/Sources/NiceComponents/Helper/ScaledFont.swift index cc8aa29..9ae3c5a 100644 --- a/Sources/NiceComponents/Helper/ScaledFont.swift +++ b/Sources/NiceComponents/Helper/ScaledFont.swift @@ -1,13 +1,15 @@ // // ScaledFont.swift -// +// NiceComponents // // Created by Brendan on 2021-03-05. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI -// https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-dynamic-type-with-a-custom-font +/// Set the Font for a view while respecting Dynamic Type sizing and styling. +/// https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-dynamic-type-with-a-custom-font public struct ScaledFont: ViewModifier { @Environment(\.sizeCategory) var sizeCategory var name: String? @@ -21,12 +23,34 @@ public struct ScaledFont: ViewModifier { } extension View { + /* + * Set a view's font property using the provided scaled font. + * + * - Parameters + * - name: The name of the font to use. + * - size: The size of the font you'd like to use as a base + * - weight: The weight of the font to use. Default is `nil`. + * - maxSize: The max DynamicTypeSize to scale to. Default is `nil`. + * + * - Returns: The font, scaled to the correct size + */ public func scaledFont(name: String?, size: CGFloat, weight: Font.Weight?, maxSize: DynamicTypeSize? = nil) -> some View { return self.modifier(ScaledFont(name: name, weight: weight ?? .regular, size: size, maxSize: maxSize)) } } public extension Font { + /* + * Create a new scaled font, given a base font, size and weight. + * + * - Parameters + * - name: The name of the font to use. + * - size: The size of the font you'd like to use as a base + * - weight: The weight of the font to use. Default is `nil`. + * - maxSize: The max DynamicTypeSize to scale to. Default is `nil`. + * + * - Returns: The font, scaled to the correct size + */ static func scaledFont(name: String?, size: CGFloat, weight: Font.Weight? = nil, maxSize: DynamicTypeSize? = nil) -> Font { var scaledSize = UIFontMetrics.default.scaledValue(for: size) diff --git a/Sources/NiceComponents/Modifiers/NiceShadowModifier.swift b/Sources/NiceComponents/Modifiers/NiceShadowModifier.swift new file mode 100644 index 0000000..a0624ce --- /dev/null +++ b/Sources/NiceComponents/Modifiers/NiceShadowModifier.swift @@ -0,0 +1,38 @@ +// +// NiceShadowModifier.swift +// NiceComponents +// +// Created by Alejandro Zielinsky on 2022-06-09. +// Copyright © 2022 Steamclock Software. All rights reserved. +// + +import SwiftUI + +/// Styling settings for drop shadows. +public struct NiceShadowStyle { + public let color: Color + public let radius: CGFloat + public let x: CGFloat + public let y: CGFloat +} + +/// Attach a drop shadow to the given View. +struct NiceShadowModifier: ViewModifier { + let style: NiceShadowStyle + + func body(content: Content) -> some View { + content + .shadow(color: style.color, radius: style.radius, x: style.x, y: style.y) + } +} + +public extension View { + /** + * Attach a drop shadow to the provided View. + * + * - Parameter style: The style to use for the drop shadow. Defaults to your config's `shadowStyle`. + */ + func shadow(_ style: NiceShadowStyle = Config.current.shadowStyle) -> some View { + modifier(NiceShadowModifier(style: style)) + } +} diff --git a/Sources/NiceComponents/Modifiers/ShadowModifier.swift b/Sources/NiceComponents/Modifiers/ShadowModifier.swift deleted file mode 100644 index 0c58759..0000000 --- a/Sources/NiceComponents/Modifiers/ShadowModifier.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ShadowModifier.swift -// -// -// Created by Alejandro Zielinsky on 2022-06-09. -// - -import SwiftUI - -public struct ShadowStyle { - public let color: Color - public let radius: CGFloat - public let x: CGFloat - public let y: CGFloat -} - -struct ShadowModifier: ViewModifier { - - let style: ShadowStyle - - func body(content: Content) -> some View { - content - .shadow(color: style.color, radius: style.radius, x: style.x, y: style.y) - } -} - -public extension View { - func shadow(_ style: ShadowStyle = Config.current.shadowStyle) -> some View { - modifier(ShadowModifier(style: style)) - } -} diff --git a/Sources/NiceComponents/Text/BodyText.swift b/Sources/NiceComponents/Text/BodyText.swift index d32c87b..76296f1 100644 --- a/Sources/NiceComponents/Text/BodyText.swift +++ b/Sources/NiceComponents/Text/BodyText.swift @@ -1,21 +1,30 @@ // // BodyText.swift -// +// NiceComponents // // Created by Brendan on 2021-01-29. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI +/// A text view that should be used in most places as the primary body text in a view. public struct BodyText: NiceText { public let text: AttributedString - public let style: TypeStyle + public let style: NiceTextStyle - public static var defaultStyle: TypeStyle { + public static var defaultStyle: NiceTextStyle { Config.current.bodyTextStyle } - public init(_ text: AttributedString, style: TypeStyle? = nil) { + /** + * Create a new body text view. + * + * - Parameters: + * - text: The text to display. + * - style: The NiceTextStyle that should be applied to the text. + */ + public init(_ text: AttributedString, style: NiceTextStyle? = nil) { self.text = text self.style = style ?? Self.defaultStyle } @@ -24,10 +33,10 @@ public struct BodyText: NiceText { Text(text) .foregroundColor(style.color) .scaledFont( - name: style.theme.name, - size: style.theme.size, - weight: style.theme.weight, - maxSize: style.theme.dynamicTypeMaxSize + name: style.fontStyle.name, + size: style.fontStyle.size, + weight: style.fontStyle.weight, + maxSize: style.fontStyle.dynamicTypeMaxSize ) .fixedSize(horizontal: false, vertical: true) .lineLimit(style.lineLimit) diff --git a/Sources/NiceComponents/Text/DetailText.swift b/Sources/NiceComponents/Text/DetailText.swift index dcfe090..7b7ecb9 100644 --- a/Sources/NiceComponents/Text/DetailText.swift +++ b/Sources/NiceComponents/Text/DetailText.swift @@ -1,21 +1,30 @@ // // DetailText.swift -// +// NiceComponents // // Created by Brendan on 2021-01-29. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI +/// A text view that should be used for supplementary text accompanying body text. public struct DetailText: NiceText { public let text: AttributedString - public let style: TypeStyle + public let style: NiceTextStyle - public static var defaultStyle: TypeStyle { + public static var defaultStyle: NiceTextStyle { Config.current.detailTextStyle } - public init(_ text: AttributedString, style: TypeStyle? = nil) { + /** + * Create a new detail text view. + * + * - Parameters: + * - text: The text to display. + * - style: The NiceTextStyle that should be applied to the text. + */ + public init(_ text: AttributedString, style: NiceTextStyle? = nil) { self.text = text self.style = style ?? Self.defaultStyle } @@ -24,10 +33,10 @@ public struct DetailText: NiceText { Text(text) .foregroundColor(style.color) .scaledFont( - name: style.theme.name, - size: style.theme.size, - weight: style.theme.weight, - maxSize: style.theme.dynamicTypeMaxSize + name: style.fontStyle.name, + size: style.fontStyle.size, + weight: style.fontStyle.weight, + maxSize: style.fontStyle.dynamicTypeMaxSize ) .fixedSize(horizontal: false, vertical: true) .lineLimit(style.lineLimit) diff --git a/Sources/NiceComponents/Text/NiceText.swift b/Sources/NiceComponents/Text/NiceText.swift new file mode 100644 index 0000000..ae3c18a --- /dev/null +++ b/Sources/NiceComponents/Text/NiceText.swift @@ -0,0 +1,99 @@ +// +// NiceText.swift +// NiceComponents +// +// Created by Alejandro Zielinsky on 2022-06-09. +// + +import SwiftUI + +/// Defines a struct for text views presented and managed by NiceComponents +public protocol NiceText: View { + /// The text to display in the View + var text: AttributedString { get } + + /// The styling to apply to the text. + var style: NiceTextStyle { get } + + static var defaultStyle: NiceTextStyle { get } + + init(_ attributedText: AttributedString, style: NiceTextStyle?) + + init(_ text: String, style: NiceTextStyle?) + + init(_ text: String, color: Color?, size: CGFloat?, lineLimit: Int?, dynamicMaxSize: DynamicTypeSize?) + + init(_ text: String, color: Color?, size: CGFloat?, lineLimit: Int?, dynamicMaxSize: DynamicTypeSize?, configure: (inout AttributedString) -> Void) +} + +public extension NiceText { + + /** + * Create a new text view by passing in custom styling. + * + * - Parameters: + * - text: The text to display. + * - color: The color to style the text. + * - size: The size to make the text. + * - lineLimit: If provided, the number of lines to limit text to. + * - dynamicMaxSize: The maximum dynamic type size the text should scale to. + */ + init( + _ text: String, + color: Color? = nil, + size: CGFloat? = nil, + lineLimit: Int? = nil, + dynamicMaxSize: DynamicTypeSize? = nil + ) { + self.init( + text, + style: Self.defaultStyle.with( + color: color, + size: size, + lineLimit: lineLimit, + dynamicTypeMaxSize: dynamicMaxSize + ) + ) + } + + /** + * Create a new text view by passing in custom styling. + * + * - Parameters: + * - text: The text to display. + * - style: The style to apply to the text. + */ + init(_ text: String, style: NiceTextStyle?) { + self.init(AttributedString(text), style: style) + } + + /** + * Create a new text view by passing in custom styling and allowing for mutating attributed string elements. + * + * - Parameters: + * - text: The text to display. + * - color: The color to style the text. + * - size: The size to make the text. + * - lineLimit: If provided, the number of lines to limit text to. + * - dynamicMaxSize: The maximum dynamic type size the text should scale to. + * - configure: Configuration block for the attributed text. + */ + init( + _ text: String, + color: Color? = nil, + size: CGFloat? = nil, + lineLimit: Int? = nil, + dynamicMaxSize: DynamicTypeSize? = nil, + configure: (inout AttributedString) -> Void + ) { + var attributedString = AttributedString(text) + configure(&attributedString) + self.init(attributedString, style: Self.defaultStyle.with( + color: color, + size: size, + lineLimit: lineLimit, + dynamicTypeMaxSize: dynamicMaxSize + )) + } +} + diff --git a/Sources/NiceComponents/Theme/ColorTheme.swift b/Sources/NiceComponents/Theme/ColorTheme.swift index 622dfc1..28864cb 100644 --- a/Sources/NiceComponents/Theme/ColorTheme.swift +++ b/Sources/NiceComponents/Theme/ColorTheme.swift @@ -1,45 +1,89 @@ // -// Colors.swift -// +// ColorTheme.swift +// NiceComponents // // Created by Brendan on 2021-02-05. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI +/// A collection of styling settings for colors, used across components +/// The language and structure used here is heavily influenced by the [Material Design color system](https://m2.material.io/design/color/the-color-system.html). public struct ColorTheme { + /// The color most frequently displayed across components. public var primary: Color + + /// An optional variant, or shade, of your primary color. public var primaryVariant: Color + + /// The color elements presented on top of primary colors should use. public var onPrimary: Color + /// An alternate theme color, complimentary to the primary color. public var secondary: Color + + /// An optional variant of the secondary theme color. public var secondaryVariant: Color + + /// The color elements presented on top of secondary colors should use. public var onSecondary: Color + /// The color that should appear behind scrollable content within the app. public var background: Color + + /// The color elements presented on top of a background should use. public var onBackground: Color + /// The color used to indicate errors in components. public var error: Color + + /// The color elements presented on top of errors should use. public var onError: Color + /// The color used for background colors in components, such as sheets, cards and menus. public var surface: Color + + /// The color elements presented on top of a surfaces should use. public var onSurface: Color + /// The color used for drop shadows. public var shadow: Color - public init(primary: Color? = nil, - primaryVariant: Color? = nil, - onPrimary: Color? = nil, - secondary: Color? = nil, - secondaryVariant: Color? = nil, - onSecondary: Color? = nil, - background: Color? = nil, - onBackground: Color? = nil, - error: Color? = nil, - onError: Color? = nil, - surface: Color? = nil, - onSurface: Color? = nil, - shadow: Color? = nil) { + /** + * Create a new color theme. + * By default, any color omitted here will user the corresponding color in Colors.xcassets + * + * - Parameters: + * - primary: The color most frequently displayed across components. + * - primaryVariant: An optional variant, or shade, of your primary color. + * - onPrimary: The color elements presented on top of primary colors should use. + * - secondary: An alternate theme color, complimentary to the primary color. + * - secondaryVariant: An optional variant of the secondary theme color. + * - onSecondary: The color elements presented on top of secondary colors should use. + * - background: The color that should appear behind scrollable content within the app. + * - onBackground: The color elements presented on top of a background should use. + * - error: The color used to indicate errors in components. + * - onError: The color elements presented on top of errors should use. + * - surface: The color used for background colors in components, such as sheets, cards and menus. + * - onSurface: The color elements presented on top of a surfaces should use. + * - shadow: The color used for drop shadows. + */ + public init( + primary: Color? = nil, + primaryVariant: Color? = nil, + onPrimary: Color? = nil, + secondary: Color? = nil, + secondaryVariant: Color? = nil, + onSecondary: Color? = nil, + background: Color? = nil, + onBackground: Color? = nil, + error: Color? = nil, + onError: Color? = nil, + surface: Color? = nil, + onSurface: Color? = nil, + shadow: Color? = nil + ) { self.primary = primary ?? Color("primary", bundle: Bundle.module) self.primaryVariant = primaryVariant ?? Color("primaryVariant", bundle: Bundle.module) self.onPrimary = onPrimary ?? Color("onPrimary", bundle: Bundle.module) @@ -57,6 +101,6 @@ public struct ColorTheme { self.surface = surface ?? Color("surface", bundle: Bundle.module) self.onSurface = onSurface ?? Color("onSurface", bundle: Bundle.module) - self.shadow = shadow ?? Color.black.opacity(0.15) + self.shadow = shadow ?? Color("onSurface", bundle: Bundle.module) } } diff --git a/Sources/NiceComponents/Theme/Colors.xcassets/shadow.colorset/Contents.json b/Sources/NiceComponents/Theme/Colors.xcassets/shadow.colorset/Contents.json new file mode 100644 index 0000000..842074c --- /dev/null +++ b/Sources/NiceComponents/Theme/Colors.xcassets/shadow.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.150", + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.150", + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/NiceComponents/Theme/Config.swift b/Sources/NiceComponents/Theme/Config.swift index 9a475cf..90de25d 100644 --- a/Sources/NiceComponents/Theme/Config.swift +++ b/Sources/NiceComponents/Theme/Config.swift @@ -1,18 +1,19 @@ // // Config.swift -// +// NiceComponents // // Created by Brendan on 2021-01-29. +// Copyright © 2022 Steamclock Software. All rights reserved. // import os import SwiftUI -/// Global config settings for all components. +/// Global settings for all components. /// Themes here will be applied to any components that don't define their own theme. public struct Config { /// Your current component configuration. - /// Note that you can only set this configuration once, ideally during app startup. + /// Note that you can only set this configuration once, ideally during app startup. Subsequent updates will be ignored. public static var current: Config { get { return _current @@ -31,89 +32,100 @@ public struct Config { private static var _current = Config() private static var hasSetConfig: Bool = false + /// The collection of color styles and settings used across components. public var colorTheme: ColorTheme - public var typeTheme: TypeTheme + + /// The collection of font styles used across components. + public var fontTheme: FontTheme // Button Styles - public var borderlessButtonStyle: NiceButtonStyle - public var destructiveButtonStyle: NiceButtonStyle public var primaryButtonStyle: NiceButtonStyle public var secondaryButtonStyle: NiceButtonStyle + public var borderlessButtonStyle: NiceButtonStyle + public var destructiveButtonStyle: NiceButtonStyle - // Text Styles + // Text and Title Styles - public var bodyTextStyle: TypeStyle - public var detailTextStyle: TypeStyle - public var itemTitleStyle: TypeStyle - public var screenTitleStyle: TypeStyle - public var sectionTitleStyle: TypeStyle + public var itemTitleStyle: NiceTextStyle + public var screenTitleStyle: NiceTextStyle + public var sectionTitleStyle: NiceTextStyle - /// Default is: x:0, y:4, blur: 4px, opacity: 0.15 (black) - public var shadowStyle: ShadowStyle + public var bodyTextStyle: NiceTextStyle + public var detailTextStyle: NiceTextStyle - public init(colorTheme: ColorTheme? = nil, typeTheme: TypeTheme? = nil) { + /// Default is: x:0, y:4, blur: 4px, opacity: 0.15 (black) + public var shadowStyle: NiceShadowStyle + + /** + * Create a new component configuration to use for all components in your project. + * + * - Parameters: + * - colorTheme: The collection of color styles and settings used across components. + * - fontTheme: The collection of font styles used across components. + */ + public init(colorTheme: ColorTheme? = nil, fontTheme: FontTheme? = nil) { self.colorTheme = colorTheme ?? ColorTheme() - self.typeTheme = typeTheme ?? TypeTheme() + self.fontTheme = fontTheme ?? FontTheme() // Set Button styles + + primaryButtonStyle = NiceButtonStyle( + fontStyle: self.fontTheme.button, + surfaceColor: self.colorTheme.primary, + onSurfaceColor: self.colorTheme.onPrimary + ) + + secondaryButtonStyle = NiceButtonStyle( + fontStyle: self.fontTheme.button, + surfaceColor: self.colorTheme.secondary, + onSurfaceColor: self.colorTheme.onSecondary + ) borderlessButtonStyle = NiceButtonStyle( - textStyle: self.typeTheme.button, + fontStyle: self.fontTheme.button, surfaceColor: Color.clear, onSurfaceColor: self.colorTheme.primary, border: NiceBorderStyle.none ) destructiveButtonStyle = NiceButtonStyle( - textStyle: self.typeTheme.button, + fontStyle: self.fontTheme.button, surfaceColor: self.colorTheme.error, onSurfaceColor: self.colorTheme.onError ) - primaryButtonStyle = NiceButtonStyle( - textStyle: self.typeTheme.button, - surfaceColor: self.colorTheme.primary, - onSurfaceColor: self.colorTheme.onPrimary - ) - - secondaryButtonStyle = NiceButtonStyle( - textStyle: self.typeTheme.button, - surfaceColor: self.colorTheme.secondary, - onSurfaceColor: self.colorTheme.onSecondary - ) - // Set Text styles - bodyTextStyle = TypeStyle( + bodyTextStyle = NiceTextStyle( color: self.colorTheme.onSurface, - theme: self.typeTheme.body1 + fontStyle: self.fontTheme.body ) - detailTextStyle = TypeStyle( + detailTextStyle = NiceTextStyle( color: self.colorTheme.onSurface, - theme: self.typeTheme.caption // body2? + fontStyle: self.fontTheme.detail ) // Set Title styles - itemTitleStyle = TypeStyle( + itemTitleStyle = NiceTextStyle( color: self.colorTheme.onSurface, - theme: self.typeTheme.headline4 + fontStyle: self.fontTheme.itemTitle ) - screenTitleStyle = TypeStyle( + screenTitleStyle = NiceTextStyle( color: self.colorTheme.onSurface, - theme: self.typeTheme.headline1 + fontStyle: self.fontTheme.screenTitle ) - sectionTitleStyle = TypeStyle( + sectionTitleStyle = NiceTextStyle( color: self.colorTheme.onSurface, - theme: self.typeTheme.headline2 + fontStyle: self.fontTheme.sectionTitle ) // Set Shadow style - shadowStyle = ShadowStyle( + shadowStyle = NiceShadowStyle( color: self.colorTheme.shadow, radius: 4.0, x: 0, diff --git a/Sources/NiceComponents/Theme/FontTheme.swift b/Sources/NiceComponents/Theme/FontTheme.swift new file mode 100644 index 0000000..821aa4c --- /dev/null +++ b/Sources/NiceComponents/Theme/FontTheme.swift @@ -0,0 +1,50 @@ +// +// FontTheme.swift +// NiceComponents +// +// Created by Brendan on 2021-03-05. +// Copyright © 2022 Steamclock Software. All rights reserved. +// + +import SwiftUI + +/// A collection of styling settings for fonts used throughout components. +public struct FontTheme { + public var screenTitle: FontStyle + public var sectionTitle: FontStyle + public var itemTitle: FontStyle + + public var body: FontStyle + public var detail: FontStyle + + public var button: FontStyle + + /** + * Create a new type theme by providing overrides for any styles you'd like to customize + * + * - Parameters: + * - screenTitle: The screen title font style to apply. Default is size 48 semibold. + * - sectionTitle: The section title font style to apply. Default is size 34 semibold. + * - itemTitle: The item title font style to apply. Default is size 20 semibold. + * - body: The body type font style to apply. Default is size 16 regular. + * - detail: The body detail type font style to apply. Default is size 14 regular. + * - button: The button font style to apply. Default is size 14 regular. + */ + public init( + screenTitle: FontStyle? = nil, + sectionTitle: FontStyle? = nil, + itemTitle: FontStyle? = nil, + body: FontStyle? = nil, + detail: FontStyle? = nil, + button: FontStyle? = nil + ) { + self.screenTitle = screenTitle ?? FontStyle(size: 48, weight: .semibold) + self.sectionTitle = sectionTitle ?? FontStyle(size: 34, weight: .semibold) + self.itemTitle = itemTitle ?? FontStyle(size: 20, weight: .semibold) + + self.body = body ?? FontStyle(size: 16) + self.detail = detail ?? FontStyle(size: 14) + + self.button = button ?? FontStyle(size: 14) + } +} diff --git a/Sources/NiceComponents/Theme/Layout.swift b/Sources/NiceComponents/Theme/Layout.swift deleted file mode 100644 index 39ab7bf..0000000 --- a/Sources/NiceComponents/Theme/Layout.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// File.swift -// -// -// Created by Brendan on 2021-01-29. -// - -import SwiftUI - -public enum Layout { - public enum Spacing { - /// xLarge: 64 - public static let xLarge: CGFloat = 64 - /// large: 32 - public static let large: CGFloat = 32 - /// medium: 24 - public static let medium: CGFloat = 24 - /// standard: 16 - public static let standard: CGFloat = 16 - /// small: 8 - public static let small: CGFloat = 8 - /// xSmall: 4 - public static let xSmall: CGFloat = 4 - } -} diff --git a/Sources/NiceComponents/Theme/NiceBorderStyle.swift b/Sources/NiceComponents/Theme/NiceBorderStyle.swift index 24921c6..70c308a 100644 --- a/Sources/NiceComponents/Theme/NiceBorderStyle.swift +++ b/Sources/NiceComponents/Theme/NiceBorderStyle.swift @@ -1,17 +1,69 @@ // // NiceBorderStyle.swift -// +// NiceComponents // // Created by Alejandro Zielinsky on 2022-07-18. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI -/// Defines the border style for a component +/// Defines the border style for a component. public enum NiceBorderStyle { + /// No border is shown. case none - case capsule(borderWidth: CGFloat?, borderColor: Color?) - case rounded(radius: CGFloat, borderWidth: CGFloat?, borderColor: Color?) - case border(borderWidth: CGFloat, borderColor: Color) + + /// A standard, square border. + case border(color: Color, width: CGFloat) + + /// A rounded, pill-style, border. + case capsule(color: Color?, width: CGFloat?) + + /// A rounded border with customizable corner radius. + case rounded(color: Color?, cornerRadius: CGFloat, width: CGFloat?) + + /// Set a custom border with the built-in StrokeStyle. case stroke(strokeStyle: StrokeStyle) + + var color: Color { + switch self { + case .border(let color, _): return color + case .capsule(let color, _), .rounded(let color, _, _): + if let color = color { + return color + } + + fallthrough + default: + return .clear + } + } + + var width: CGFloat { + switch self { + case .border(_, let width): return width + case .capsule(_, let width), .rounded(_, _, let width): + if let width = width { + return width + } + + fallthrough + default: + return 0.0 + } + } + + var cornerRadius: CGFloat { + switch self { + case .rounded(_, let radius, _): return radius + default: return 0.0 + } + } + + var strokeStyle: StrokeStyle? { + switch self { + case .stroke(let strokeStyle): return strokeStyle + default: return nil + } + } } diff --git a/Sources/NiceComponents/Theme/NiceButtonStyle.swift b/Sources/NiceComponents/Theme/NiceButtonStyle.swift index 0769e50..a881dfc 100644 --- a/Sources/NiceComponents/Theme/NiceButtonStyle.swift +++ b/Sources/NiceComponents/Theme/NiceButtonStyle.swift @@ -1,102 +1,101 @@ // -// ButtonStyle.swift -// +// NiceButtonStyle.swift +// NiceComponents // // Created by Brendan on 2021-01-29. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI -/// A button style to be applied to a button component +/// A style to be applied to a button component. public struct NiceButtonStyle { - public var textStyle: TypeTheme.TextStyle + /// The text style will be applied to the text inside the button. + public var fontStyle: FontStyle + + /// The height of the button. public var height: CGFloat + + /// The background color of the button. public var surfaceColor: Color + + /// The color the content on top of the background. public var onSurfaceColor: Color - public var disabledSurfaceColor: Color - public var disabledOnSurfaceColor: Color - public var cornerRadius: CGFloat - private var borderStyle: NiceBorderStyle - public var borderColor: Color - public var borderWidth: CGFloat - public var strokeStyle: StrokeStyle? - /** - Create a new button style to apply to a button component. + /// Background color when set to inactive. + public var inactiveSurfaceColor: Color + + /// Content color when set to inactive. + public var inactiveOnSurfaceColor: Color - - Parameter textStyle: Text style to apply to any text in the button. Default is systemFont size 16 - - Parameter height: Height of the button. Default is 44. - - Parameter surfaceColor: Surface color of the button. - - Parameter onSurfaceColor: Color of any assets on top of your button. - - Parameter disabledSurfaceColor: Surface color when disabled. Default is your background color - - Parameter disabledOnSurfaceColor: Color of any assets on top of your button when disabled. Default is your secondary color - - Parameter border: Border style for the button. Default is none + /// The style of the border applied to your button. + public var border: NiceBorderStyle + + /** + * Create a new button style to apply to a button component. + * + * - Parameters: + * - fontStyle: Font style to apply to any text in the button. Default is systemFont size 16 + * - height: Height of the button. Default is 44. + * - surfaceColor: Surface color of the button. + * - onSurfaceColor: Color of any assets on top of your button. + * - inactiveSurfaceColor: Surface color when set to inactive. Default is your surface color. + * - inactiveOnSurfaceColor: Color of any assets on top of your button when inactive. Default is your onSurface color. + * - border: Border style for the button. Default is none. + * + * - Returns: the newly modified button style. */ public init( - textStyle: TypeTheme.TextStyle? = nil, + fontStyle: FontStyle? = nil, height: CGFloat = 44, surfaceColor: Color, onSurfaceColor: Color, - disabledSurfaceColor: Color? = nil, - disabledOnSurfaceColor: Color? = nil, + inactiveSurfaceColor: Color? = nil, + inactiveOnSurfaceColor: Color? = nil, border: NiceBorderStyle? = nil ) { - self.textStyle = textStyle ?? .init(size: 16) + self.fontStyle = fontStyle ?? .init(size: 16) self.height = height self.surfaceColor = surfaceColor self.onSurfaceColor = onSurfaceColor - self.disabledSurfaceColor = disabledSurfaceColor ?? Color("background", bundle: Bundle.module) - self.disabledOnSurfaceColor = disabledOnSurfaceColor ?? Color("secondary", bundle: Bundle.module) - self.borderStyle = border ?? .none - - switch border { - case .capsule(let borderWidth, let borderColor): - self.borderColor = borderColor ?? .clear - self.borderWidth = borderWidth ?? 0.0 - self.cornerRadius = height / 2 - self.strokeStyle = nil - case .rounded(let radius, let borderWidth, let borderColor): - self.borderWidth = borderWidth ?? 0.0 - self.borderColor = borderColor ?? .clear - self.cornerRadius = radius - self.strokeStyle = nil - case .border(let borderWidth, let borderColor): - self.borderWidth = borderWidth - self.borderColor = borderColor - self.cornerRadius = 0.0 - self.strokeStyle = nil - case .stroke(let strokeStyle): - self.cornerRadius = 0.0 - self.borderColor = .clear - self.borderWidth = 0.0 - self.strokeStyle = strokeStyle - default: - self.cornerRadius = 0.0 - self.borderColor = .clear - self.borderWidth = 0.0 - self.strokeStyle = nil - } + self.inactiveSurfaceColor = inactiveSurfaceColor ?? surfaceColor + self.inactiveOnSurfaceColor = inactiveOnSurfaceColor ?? onSurfaceColor + self.border = border ?? .none } } public extension NiceButtonStyle { + /** + * Modify a button style with the given properties. + * + * - Parameters: + * - fontStyle: Font style to apply to any text in the button. Default is systemFont size 16 + * - height: Height of the button. Default is 44. + * - surfaceColor: Surface color of the button. + * - onSurfaceColor: Color of any assets on top of your button. + * - inactiveSurfaceColor: Surface color when set to inactive. Default is your background color. + * - inactiveOnSurfaceColor: Color of any assets on top of your button when inactive. Default is your secondary color. + * - border: Border style for the button. Default is none. + * + * - Returns: the newly modified button style. + */ func with( - textStyle: TypeTheme.TextStyle? = nil, + fontStyle: FontStyle? = nil, height: CGFloat? = nil, surfaceColor: Color? = nil, onSurfaceColor: Color? = nil, - disabledSurfaceColor: Color? = nil, - disabledOnSurfaceColor: Color? = nil, + inactiveSurfaceColor: Color? = nil, + inactiveOnSurfaceColor: Color? = nil, border: NiceBorderStyle? = nil ) -> NiceButtonStyle { NiceButtonStyle( - textStyle: textStyle ?? self.textStyle, + fontStyle: fontStyle ?? self.fontStyle, height: height ?? self.height, surfaceColor: surfaceColor ?? self.surfaceColor, onSurfaceColor: onSurfaceColor ?? self.onSurfaceColor, - disabledSurfaceColor: disabledSurfaceColor ?? self.disabledSurfaceColor, - disabledOnSurfaceColor: disabledOnSurfaceColor ?? self.disabledOnSurfaceColor, - border: border ?? self.borderStyle + inactiveSurfaceColor: inactiveSurfaceColor ?? self.inactiveSurfaceColor, + inactiveOnSurfaceColor: inactiveOnSurfaceColor ?? self.inactiveOnSurfaceColor, + border: border ?? self.border ) } } diff --git a/Sources/NiceComponents/Theme/NiceFontStyle.swift b/Sources/NiceComponents/Theme/NiceFontStyle.swift new file mode 100644 index 0000000..d282e37 --- /dev/null +++ b/Sources/NiceComponents/Theme/NiceFontStyle.swift @@ -0,0 +1,45 @@ +// +// FontStyle.swift +// NiceComponents +// +// Created by Brendan on 2022-11-22. +// Copyright © 2022 Steamclock Software. All rights reserved. +// + +import SwiftUI + +/// Styling settings for a font. +public struct FontStyle { + /// The name of the font the text should use. + public let name: String? + + /// Font weight to use. + public let weight: Font.Weight? + + /// The size of the text to use. + public let size: CGFloat + + /// The maximum dynamic type size text should be scaled to. + public var dynamicTypeMaxSize: DynamicTypeSize? + + /** + * Create a new font style. + * + * - Parameters: + * - name: The name of the font to use. + * - size: The size the font should be. + * - weight: The font weight the font should be. Default is `nil`. TODO: Should this just be regular or something? Need to figure out where it's applied + * - dynamicTypeMaxSize: The maximum dynamic type size the font should be scaled to. Default is `nil`, meaning the font will scale to the maximum allowed by iOS. + */ + public init( + _ name: String? = nil, + size: CGFloat, + weight: Font.Weight? = nil, + dynamicTypeMaxSize: DynamicTypeSize? = nil + ) { + self.name = name + self.size = size + self.weight = weight + self.dynamicTypeMaxSize = dynamicTypeMaxSize + } +} diff --git a/Sources/NiceComponents/Theme/NiceSpacing.swift b/Sources/NiceComponents/Theme/NiceSpacing.swift new file mode 100644 index 0000000..f797993 --- /dev/null +++ b/Sources/NiceComponents/Theme/NiceSpacing.swift @@ -0,0 +1,25 @@ +// +// NiceSpacing.swift +// NiceComponents +// +// Created by Brendan on 2021-01-29. +// Copyright © 2022 Steamclock Software. All rights reserved. +// + +import SwiftUI + +/// A collection of common spacing values to ensure consistent spacing across components. +public enum NiceSpacing { + /// xLarge: 64 + public static let xLarge: CGFloat = 64 + /// large: 32 + public static let large: CGFloat = 32 + /// medium: 24 + public static let medium: CGFloat = 24 + /// standard: 16 + public static let standard: CGFloat = 16 + /// small: 8 + public static let small: CGFloat = 8 + /// xSmall: 4 + public static let xSmall: CGFloat = 4 +} diff --git a/Sources/NiceComponents/Theme/NiceTextStyle.swift b/Sources/NiceComponents/Theme/NiceTextStyle.swift new file mode 100644 index 0000000..8bfde90 --- /dev/null +++ b/Sources/NiceComponents/Theme/NiceTextStyle.swift @@ -0,0 +1,68 @@ +// +// NiceTextStyle.swift +// NiceComponents +// +// Created by Brendan on 2021-03-05. +// Copyright © 2022 Steamclock Software. All rights reserved. +// + +import SwiftUI + +/// Styling settings for a text element. +public struct NiceTextStyle { + /// The color your text should be. + public var color: Color + + /// The font style to apply to the text. + public var fontStyle: FontStyle + + /// The number of lines to limit the text to. + public var lineLimit: Int? + + /** + * Create a new text style to apply to a text element. + * + * - Parameters: + * - color: The color your text should be. + * - fontStyle: The font style to apply to the text. + * - lineLimit: The number of lines to limit the text to. + */ + public init( + color: Color, + fontStyle: FontStyle, + lineLimit: Int? = nil + ) { + self.color = color + self.fontStyle = fontStyle + self.lineLimit = lineLimit + } +} + +public extension NiceTextStyle { + /** + * Modify a text style with the given properties. + * + * - Parameters: + * - color: The color your text should be. + * - size: The font size to change. + * - fontStyle: The font style to apply to the text. + * - lineLimit: The number of lines to limit the text to. + */ + func with( + color: Color? = nil, + size: CGFloat? = nil, + lineLimit: Int? = nil, + dynamicTypeMaxSize: DynamicTypeSize? = nil + ) -> NiceTextStyle { + NiceTextStyle( + color: color ?? self.color, + fontStyle: FontStyle( + self.fontStyle.name, + size: size ?? self.fontStyle.size, + weight: self.fontStyle.weight, + dynamicTypeMaxSize: dynamicTypeMaxSize ?? self.fontStyle.dynamicTypeMaxSize + ), + lineLimit: lineLimit ?? self.lineLimit + ) + } +} diff --git a/Sources/NiceComponents/Theme/TypeStyle.swift b/Sources/NiceComponents/Theme/TypeStyle.swift deleted file mode 100644 index 5835f6e..0000000 --- a/Sources/NiceComponents/Theme/TypeStyle.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// TypeStyle.swift -// -// -// Created by Brendan on 2021-03-05. -// - -import SwiftUI - -public struct TypeStyle { - public var color: Color - public var theme: TypeTheme.TextStyle - public var lineLimit: Int? - - public init(color: Color, - theme: TypeTheme.TextStyle, - lineLimit: Int? = nil) { - self.color = color - self.theme = theme - self.lineLimit = lineLimit - } -} - -public extension TypeStyle { - func with(color: Color? = nil, size: CGFloat? = nil, lineLimit: Int? = nil, dynamicTypeMaxSize: DynamicTypeSize? = nil) -> TypeStyle { - TypeStyle( - color: color ?? self.color, - theme: TypeTheme.TextStyle( - self.theme.name, - size: size ?? self.theme.size, - weight: self.theme.weight, - dynamicTypeMaxSize: dynamicTypeMaxSize ?? self.theme.dynamicTypeMaxSize - ), - lineLimit: lineLimit ?? self.lineLimit - ) - } -} diff --git a/Sources/NiceComponents/Theme/TypeTheme.swift b/Sources/NiceComponents/Theme/TypeTheme.swift deleted file mode 100644 index 79f4d1f..0000000 --- a/Sources/NiceComponents/Theme/TypeTheme.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// TypeTheme.swift -// -// -// Created by Brendan on 2021-03-05. -// - -import SwiftUI - -public struct TypeTheme { - public struct TextStyle { - public let name: String? - public let weight: Font.Weight? - public let size: CGFloat - public var dynamicTypeMaxSize: DynamicTypeSize? - - public init( - _ name: String? = nil, - size: CGFloat, - weight: Font.Weight? = nil, - dynamicTypeMaxSize: DynamicTypeSize? = nil - ) { - self.name = name - self.size = size - self.weight = weight - self.dynamicTypeMaxSize = dynamicTypeMaxSize - } - } - - public var headline1: TextStyle - public var headline2: TextStyle - public var headline3: TextStyle - public var headline4: TextStyle - public var subtitle1: TextStyle - public var subtitle2: TextStyle - - public var body1: TextStyle - public var body2: TextStyle - - public var button: TextStyle - public var caption: TextStyle - public var overline: TextStyle - - public init(headline1: TextStyle? = nil, - headline2: TextStyle? = nil, - headline3: TextStyle? = nil, - headline4: TextStyle? = nil, - subtitle1: TextStyle? = nil, - subtitle2: TextStyle? = nil, - body1: TextStyle? = nil, - body2: TextStyle? = nil, - button: TextStyle? = nil, - caption: TextStyle? = nil, - overline: TextStyle? = nil ) { - self.headline1 = headline1 ?? TextStyle(size: 48, weight: .semibold) - self.headline2 = headline2 ?? TextStyle(size: 34, weight: .semibold) - self.headline3 = headline3 ?? TextStyle(size: 24, weight: .semibold) - self.headline4 = headline4 ?? TextStyle(size: 20, weight: .semibold) - - self.subtitle1 = subtitle1 ?? TextStyle(size: 16, weight: .semibold) - self.subtitle2 = subtitle2 ?? TextStyle(size: 14, weight: .semibold) - - self.body1 = body1 ?? TextStyle(size: 16) - self.body2 = body2 ?? TextStyle(size: 14) - - self.button = button ?? TextStyle(size: 14) - self.caption = caption ?? TextStyle(size: 12) - self.overline = overline ?? TextStyle(size: 12) - - } -} diff --git a/Sources/NiceComponents/Title/ItemTitle.swift b/Sources/NiceComponents/Title/ItemTitle.swift index 57b1a41..4fb0168 100644 --- a/Sources/NiceComponents/Title/ItemTitle.swift +++ b/Sources/NiceComponents/Title/ItemTitle.swift @@ -1,22 +1,32 @@ // // ItemTitle.swift -// +// NiceComponents // // Created by Brendan on 2021-01-29. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI +/// A smaller title style meant to emphasize elements on a page or further divide sections. public struct ItemTitle: NiceText { public let text: AttributedString - public let style: TypeStyle + public let style: NiceTextStyle - static public var defaultStyle: TypeStyle { + static public var defaultStyle: NiceTextStyle { Config.current.itemTitleStyle } - public init(_ text: AttributedString, style: TypeStyle? = nil) { + /** + * Create a new title title. + * Item titles should be used to further break up content within sections, roughly equivalent to

. + * + * - Parameters: + * - text: The text to display. + * - style: Any customizations to the style that should be applied. By default your config's `itemTitleStyle` is used. + */ + public init(_ text: AttributedString, style: NiceTextStyle? = nil) { self.text = text self.style = style ?? Self.defaultStyle } @@ -25,10 +35,10 @@ public struct ItemTitle: NiceText { Text(text) .foregroundColor(style.color) .scaledFont( - name: style.theme.name, - size: style.theme.size, - weight: style.theme.weight, - maxSize: style.theme.dynamicTypeMaxSize + name: style.fontStyle.name, + size: style.fontStyle.size, + weight: style.fontStyle.weight, + maxSize: style.fontStyle.dynamicTypeMaxSize ) .fixedSize(horizontal: false, vertical: true) .lineLimit(style.lineLimit) @@ -37,7 +47,7 @@ public struct ItemTitle: NiceText { struct ItemTitle_Previews: PreviewProvider { static var previews: some View { - VStack(spacing: Layout.Spacing.large) { + VStack(spacing: NiceSpacing.large) { ItemTitle("Item Title") ItemTitle("Item Title", color: .red) ItemTitle("Item Title", size: 64) diff --git a/Sources/NiceComponents/Title/ScreenTitle.swift b/Sources/NiceComponents/Title/ScreenTitle.swift index 68f0930..e5a9814 100644 --- a/Sources/NiceComponents/Title/ScreenTitle.swift +++ b/Sources/NiceComponents/Title/ScreenTitle.swift @@ -1,21 +1,31 @@ // // ScreenTitle.swift -// +// NiceComponents // // Created by Brendan on 2021-01-29. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI +/// Text styled to represent a main heading or top of page title. public struct ScreenTitle: NiceText { public let text: AttributedString - public let style: TypeStyle + public let style: NiceTextStyle - public static var defaultStyle: TypeStyle { + public static var defaultStyle: NiceTextStyle { Config.current.screenTitleStyle } - public init(_ text: AttributedString, style: TypeStyle? = nil) { + /** + * Create a new screen title + * A screen title should be used to represent a main heading, roughly equivalent to

. + * + * - Parameters: + * - text: The text to display. + * - style: Any customizations to the style that should be applied. By default your config's `screenTitleStyle` is used. + */ + public init(_ text: AttributedString, style: NiceTextStyle? = nil) { self.text = text self.style = style ?? Self.defaultStyle } @@ -24,10 +34,10 @@ public struct ScreenTitle: NiceText { Text(text) .foregroundColor(style.color) .scaledFont( - name: style.theme.name, - size: style.theme.size, - weight: style.theme.weight, - maxSize: style.theme.dynamicTypeMaxSize + name: style.fontStyle.name, + size: style.fontStyle.size, + weight: style.fontStyle.weight, + maxSize: style.fontStyle.dynamicTypeMaxSize ) .fixedSize(horizontal: false, vertical: true) .lineLimit(style.lineLimit) diff --git a/Sources/NiceComponents/Title/SectionTitle.swift b/Sources/NiceComponents/Title/SectionTitle.swift index bed17d7..581f3ba 100644 --- a/Sources/NiceComponents/Title/SectionTitle.swift +++ b/Sources/NiceComponents/Title/SectionTitle.swift @@ -1,22 +1,31 @@ // // SectionTitle.swift -// +// NiceComponents // // Created by Brendan on 2021-01-29. +// Copyright © 2022 Steamclock Software. All rights reserved. // import SwiftUI +/// A section title should be used to break up content, usually in-line. public struct SectionTitle: NiceText { - public var text: AttributedString - public let style: TypeStyle + public let style: NiceTextStyle - public static var defaultStyle: TypeStyle { + public static var defaultStyle: NiceTextStyle { Config.current.sectionTitleStyle } - public init(_ text: AttributedString, style: TypeStyle? = nil) { + /** + * Create a new section title. + * Section titles should be used to break up content, roughly equivalent to

. + * + * - Parameters: + * - text: The text to display. + * - style: Any customizations to the style that should be applied. By default your config's `sectionTitleStyle` is used. + */ + public init(_ text: AttributedString, style: NiceTextStyle? = nil) { self.text = text self.style = style ?? Self.defaultStyle } @@ -25,10 +34,10 @@ public struct SectionTitle: NiceText { Text(text) .foregroundColor(style.color) .scaledFont( - name: style.theme.name, - size: style.theme.size, - weight: style.theme.weight, - maxSize: style.theme.dynamicTypeMaxSize + name: style.fontStyle.name, + size: style.fontStyle.size, + weight: style.fontStyle.weight, + maxSize: style.fontStyle.dynamicTypeMaxSize ) .fixedSize(horizontal: false, vertical: true) .lineLimit(style.lineLimit) diff --git a/nice_components.png b/nice_components.png new file mode 100644 index 0000000000000000000000000000000000000000..b9c6e9f30af138d35b51f9f6b862379b1f98a5de GIT binary patch literal 48190 zcmbrmWmH>Xw=N2W7He^7aVt>VrMMOM;_hxmg1c)eQrz9$f)t118r)ri6Pz3R-LtM97#MgD7#LVn#COm;34qvi=pQ5p zNlhmh7~IdVzpzrulxNVJuujSnA~2O>ga^-`!x3 zpKKEd)l=6H1ja|{(fQ$L`<`a%V1~r7gz6;lu|AHoLr_7W~sS??>?2^+E#>r7H8}=>d>bzSQZN zbqjoNO*VCrFm!{v4*v-3sal-~h3`;}k9jX1-2p;WDNu?hT)2GCjA&;p%@nxZ#k0Xsd* z%{vJQUj$nyco!clJskERF*o3*mvc00+Y%elibGZLCj8oD+r#fzB5;%Mu$n-a>(0s7 zaYv*!&(kX6BTYc;V-w&AoX4fHl+J4x<)= zJnx99_6OjgA@hNx<-W%hbgKWex`i3g@UqUH|AYV;^YL!kdw$su9k#1N@$4w^&Ti=R zeX;lQE&x4CtUYf;gxxH4`G)0JC$CpVoxUMd9F z>cAcMfR$(APNMHC+0UV;Vogjrk&x$j!Omst7Ybj9KEjjh{9N($WzcL^KSoC3Nx@U9;YZS^b!1Fa6$i#m|%9)5Pg+Z+9TUBGX9P*lntDNWf}wu=Ojg^xoH zhmo#U#WN^c@~rb zv)N0S;^OE`ywB@cJ9%mHJbjsc+4NQi1o{BX^Ac~Z&iZy=4)B<6?!46@%gK38kLNCX z%{1ejFJniqG7wyHct~SAuLyB%@TKDeJqki9fekOt#!yXITd?af6rG4!bgjGtC|s?+ z*xNSy{I=c@m~5w*=lFOUe$?E#*tQG=&pl%`2?C$QHw~4Yj}0H}pW8tXF~6MguUK9u#b3we1vmSjp6dP}aO!+!^4l%bAL8g~3)8(mKrye^Yt1mmgr;P-3 z!#~{ecK0Do!(HCvKQ2i!_ca0f&rk*Vmelkh7jf#L^kN95dV{G7N(llTVf62ovL}EC zi{rx!s2x)E#4G6Fgi~?~uF7jKkvnM#eHpD{hsyu&+HF_sQW5b?TJcjTHRdScl2>y} zEnvml{Xl;om87{N4_Ehy$cZ14G56EG0rXrI^q?IFcK0a)d+%rZ+B9EO=shUB5&g7D zMV?j}6|2H+u>|GsGCsF&RL;04cpydfKB8X>n-sA(jpF0$cqC#Rnp z-#4KJ@$N{@p>oGm9lf*8h z!tRpJpfGD6cBS{c7s9*%B5lCZr^e@t%BhH>F=irR9Z*oy(t38F#p!d5(xUJ88_$N9 zE|=duNA2nl3iD242*4*I-5nuiIcLme_nB6@rIXGTX$S1(l#^bBUdINX3zI(`RLMz0 zG4|8GQIwW}gi-ICd>O$fo5Fwq(KV`%9Y_GGPNaRiy*AzF5hPFD7$z6c9&h{(WoT#n z@1W1U#-f&7#9!>jKW!c{vpcr$i52XL zW!?_m1UU`*2xfi?_C)xR0w4cD-r+XGX?wr5&-e3@peUI}?&hN1OP7;OAjV=|rUax@ zY?cd~z2`a7B6L1u`@YY`r;2rtlHZq`cdHUi^*m5};7L?*ykDCuib}1})$p;}m%Bbv z+QHL3fcNynA;k7}tLr7h2~le1{Iaq5QztsEe~@wI-u)?Y%%|?6jHiO>bcEPXL0?z; zTszlhpW`R4iO&7LNM+%jEUW6BgTx6SFG*^9bS9pi@}VrvPfd;8_VbKIQ1yW;F@Z!^ zPde<`^>s@DZGkI|E!rm0!@ZI(tpign1DD{f*{xECmod8X+8So;)ssB&kbMiS8DC#4 zG*+HAv!RFOThh;^euPaq#jBr>n_Fr1Al5UVGv5m!=W5KaEIcE;xIJD@(!{zai=qu! zERidGkKR(6SU*J^z|)14OVTU+z@WvB9R)5i4v1Fy=MfvXE|xiS@g80+2i~9wGJ2{_ zxu)H}_}mUpp4y{jY>oB&DDY)*9#^`td66<$Sb5HawSH-Ly^b4aU(LT}Y8?RJehxUD z>eKS!mx+^*lG|6Z&M@|MK9voK<-E??AK4NhHS9V;d)|quUvjxqHc0N?$N#D_k9P3$ z%L3Oip^bBv=!Flr1z(|O99h>GC*Q@&PJ?$ROCZ8YhOn#oSzPnN$w!O+_#4Kupx!9Y zhCC!QKE;LSrziGx^Q{e$TP`#Jso?<8hq!MhylbS*ErcZj`$d$EYA7WI-)-Z`rhlg2K~7jGwMSqv5Z~n~INu8HEf88D!_OL%@ zzUA)9zHOt#nv0oj>l7shTA3OPX90w)4&iaZ8eg;SkHZjMMBBd2LK$Pj>YkaMix7bMU+C zha95c6Rr>(5)sz8+J}jfVXEU#T@`psSRvJ$VU!2euE)fd$5SZ`sL{vhLzLJ)Sl&t+ zry8w&iD&n5?^}=5Eg#%czH%MKT*MtCwgzcZ5gTO^gIBKewYQ~aPQl4LKfJKQzLIYU zGf21!EHMTcATrK02HrH)KiyT6J9?2QOr4e}&+_QU5L7u#-Ggd250;ChEW&O`I8xX3 zBVs9@4|Bw*cUOI{7wAeME5=bLjLco9r`2umt9oMU4q~03#?M!n2n8H}y$xV=)m+gIVU<@bZ^5CnQ%vx~P4i50@|sseh||p>maX(T00bX89S| zKd1DdFk{yUI9RUfF~f@$y(bZx;cFU5(gg|9{y}><@wGU3Xq~l|u}VxrHAdn0|Ks%Z z9PDjF(rmBIQ|r;R3P30vJt^vOUI2Ddb*wpm@N(a-La9bN^mATm1XNkf!v_|EVe6db zl7hb08Y|pjDgk6Y5VSbvB@+*{viBPmKWY%zc=fCgs@jAS_|4z=t)13u9N-e>&e5sj zXPg1wX>L0Ut_dXd#2MLBJFy#O zR}%#dBZwq+;R>&)p`Q;?6FUb1o-0}7!NLwxaVlN|TW+;tJrU*`vT{Z>9324a)hr-M z%aM$yipxvN7(2tGmwEX}U{%NLP8p-4c4N`9u&|aiwp-DUW!|A4=cauL?prw1 zvR~p3*v>mtRojb*GMq!l?R`CiY)E(Tb7KBh(wJ?XF~Xn}(9f-VvVryE&F^3P_l`6D zL9Iz~@d=)flf~@<)6bzeTCIz|kL}^EG1}IEhrN?iIT4%dwmR5}pZpHX2S-m?*R6bqUV>8sOQNA7ou!^#M#sh%@NCN2W)L}% zeVNN-KEwGeTHk^^Pf$~J*TW~{#tQCkrbV7fV#?|P2q0p~n(sKe)BcZAP*%Z-W_L`m zGQQUvU47F6inE}vjsc}jw%wN_{xwn)x4C5FG2E~j7qaYjVZ+kj_DjEpqSFPD^PavS z`E+cV;YDU_bWzTR1p^;4_KUD4W6*$05jDUkv|HkaK_t(xllwWA2x${JQxS2=Ry&?J z95W5llOPb2#}tNC_l2$+{2GalUC0@|z%(P&Jf8?8+I-)vt`z>y8yFRz@ zXUlE8AfIFOHxtSn3Amod5|`N$)ocFMRWI5sVmrpndCLDY2@Fhipd{i~dLzMoR7-Tt z;1cnAzr^E z@-H{SRgm3F_Ra53KzfJ?Wn^Yv8NeP$vHzHi?mJ@i@~l#-H#edM|KADTU0mqD;N3fP zA+gKI@05t=uyB;G^@sZ24tI))vMMw0BCFzscW3{VchFk;<$F2H8!7Rd?Bc)UDfg&- zZ}B)jK8kai#U!ie+PKScCfZLTMiEBcHOvBCSaz>TvV3qVwf;NA7`o&`hd?@)2*K#f zKovXUe?R(PuN2i{Dpx;!t!4Y~y=21B=R=4~8N|bP{3Cx=g+Q@pV^gq=lT2ykXkU-q zoBw{ZE;AWjLJgNR0K!IUAJaG#*>NT7mW7 zfrEq%nCYQZPMdkg7{!HZF5ei)G&24#r5pTEsTM?+xJFGzeaJo9 z&+qtm{5Z?{t(JUf%tYQCpKR@nyF^e$f%Rx#6&KQfN!$MBv;)83dSp!2vUx=ilU~0p z_`}Zi;Kt^$C1zqeS8bHvdDWqHBG5Cksyh|of5!cRlWmYLRBqqQw6jv&84R?(8Mkdv z00>|SwlulbD<%F#f^=EmbJYoL!~V~hvmhche%Y+%-<2PK-DN!jI=vX4-=8*YS(gcx z^XE{fjrN@w2>j2tVMz6@WSXuG)hMpnLD@_Hz5oB^mBFH$?X__9e}@Pers%d-EYXu_ z1B={SVt^H2?3R92)cs#faT`)kvODP6@YZJY?F}*Sla5ZYQ(p)dNE)={yNdACqgYh< z@05S^Zv>G@Y!hQ5i_1m9!e-GQ-V)D^w$uM#%C|c8NKGwO$|kq!BUO;0K9d^?)F=Dz zEbII^WD2+J3C{Ec|4V`1isj0w5+fZ0qFSvn>cs3({>x3jH`zD5RNp<`kTB#-xX{&J z`yb){&vZgcVHNJ|=uR|8-&aWFQ0s|5UjEN#Fxa6&!=lf%=cW>>RizYMExPgKrs%qv0jeyjkCtP~Wx(vUZI+&oWi0s9vf7FSA(q3we%b&&o4F>G#cnug>|_ied~$l})ROAZMzAJJf7c6Ra9 zP6_Q({#t_I7&Wax$+DfX<6qp#Ni3E<(0^HIr@5 zJ|gnlZEofh8@_?@!@O8LT=7V|6NxM)9$cotL=+UE8<+z#2qym3aXNR1hk^Mcy6F?! zu$E^aUbPY8Nrzqvm4=`4=3Jq815w6tb+{g~7_r6;49wr4R&yd(et&krmT4ap!N^kzs&* zr$-!gt8iC7<{9WKdlYF3JJ9@0AC~dT%d0@v5tv>Q1(iMwgk+-7)82kpU1zac?*ZfZ z3!~AXSv7*y*vDInTx3qnsU9AdcZ0@m#|xh+CXJbL{o19m>zm|UW5*95xI?l6%;!a< zsH2|%@^$TFUyRrml^L0&K0ilCNIf)r4_%wfB)pZE{Xw^#_2_oKZwh5^`XuRP;ASkp z1VYj>#XbEi-Lmjx?#><}${)zDozojXN~6*6eVxqOxG5zXQ!OfdaYn*2#!49iab!k5 z7%!-I?d>l<$;dzRk0;D0SH-8$u3X>?KFhhCXdhv}QyP0uou*%0v8vaF%mYJeucNO; zaP=vrzJTJDXx2PWTxx&VShE$(gC9J8V{j3%@>e`t;mWiQHGwF(eS^lvP4Jwou+s-HEkV#mE0VoasF2AUs6C5%wL=cPgGvj!s4LC)3s7ul` z;#40}(HYim0Xl5cy9;eEHp*ujiSL3Lh;1pK9%~vr%fN>I7jTYDFfc7nHrHTQ76=;l zDefJZo>-Uab>aj}QJ93V1U9;Y7~YP8JZnfBdhTMcdjvV0;ji8SJk!p<4_;S8l3A{jkaZ7(c2CE+MW6iu6XzIrj0fbFP8OpRlj8k#1*M(#W z5%~=iI+QzAdD#KhPZN)$v1p@fzkcu9>&}7(oq|lFkpr0ZJzTB_T3QJ1^wbne8d&$) z@Z-akM;@WYgnySff(Y7td_;~*6TL>!AiPdk(pAI4y=|2`a^QNqQM;^h2OdtPj*F2v zBUnI74cy8(C?UqkiPyPGc+10SK1h-L(LWg$rsdZN1Ik<|bVi{R{|UIvjRG!v#xr6y z%=<-Hk7Mumzjz}DlEn6raRN2X>a?c0*?6iyCyGqD~j9?^NmCtL=Ro8lwhFE`xd4+ke6@m~fC3XUp z=*L3N9tP^0<;>x)8~~^D>gUyP8loz#ff*XXRJHBn_CDiiN)lWv2Kb9S2`@vEP8k@O zpA_lEB<6_xuN#+g+o^gCmyfW6_u-yn*kjhrqucC${8r66rb7)Q-M!LhO!uvEH)8#7 zU+)0q9Hpf|R~;p_x_mqLp-$>ko*EzYFk|^2=}vj;bczd*{ouFGL4|?2$xd2lLx&cA zVElfOT9wH>hJ;PK%C4U)a>*}aa0xrUH2bdb!l0)tOs-zWk}&wRe`@`)PzP36quxLn zs9ft5;=xFoo!sF?kwBH$nav-nvlQP=rvdnA#EJ2x6>H=UL;1y{tnK}?UO&B};-1I} zYIb2@M7s&|a|%gw#cl(t?FGHC zC!$R}GtUzAipW&PEFOuYUD#__jlX8m)TzPFLtXdl-i%9hW$rbNxrJRBU+1}v)jBUO z#Inct)f6XoSwmh6ImMl5ot(Ck$e)KLJY^tOg``rs#~6+=V1exFN2m-IrrVtE2Q_Nq zdw(A#v*YkVwLTeH>S8203H2Welny%ln)6{CHIlg59=1UL7>ioSDTu68y%Q#jQ}C^7lW!F~FLlwTS z)UC5%+U2@8Uh&9WzdDI?zl`d4I;%jfLG-fVjvs*Yvwo5VTgZ}|6SB) zD{fQn=yHQJ%jU0^XT|`cv;99aZRL9-0!v3un~AWPORGw zS4IroV|I|xJ}htXEXX|k;hj!qMuq35G`pPJPgmVK2PI@gN2?kiFcYz6ngjB!U8RxovKYUsM2Q?bPkvC|BG{o5h8n7d<`cst;-_U8ajw#`6?5)fy ziY^Y2T=1aA>I@wa>lPT&%bN8cQ;G^TSqMh&ip+3S{3qhE1PXK^;jhNwdsL2d_}k2$ zRL!y$H}dv!W9Z=&aKXonKG>4`^N+dt?}CV{;V1&-IzQkDk~;aH)1I^{6n``}IXX+8 z<4+TrL9a0Cv&~X zV@?B2K@qNwol3!&jl6a1hCqtN)g^)a{GDPLNbAZC&J-Dj20zK9M7?oDn0Y9`)8ZfA zk%l`Rn>wTD%}&;OwY_Ed9zVD1Vo#p&xCr_(&|l%}o@U;gE%>NJAc_R591#3h&O&y^ zsGkTzXezs($xKk;W^X4l0SlAx7R2Q%n`hp){2k@nRXB93LM%L2o@<~`4~Vtua2umP znS}Wli~7%Gif*)px_}p|`{T>q%gQ6JTGKazH{;2k0@7-G!F*1xs;V5DpcS;Qlgz5J(JF=VOgs*Nka@O(;-#cA*y~ zNg&k5O-pV2=;yIG8vpqUhDsbpNO;4*n2Y88H6f&#C<=PcF`8}VzOaRi5B>b6i!d@i zW{=5$HJCwjav#3Cxj_Z{6SGo&6nxqoRByF(`aSLbG*)r#n-Ne)n?uG`EesI03ubJ$ z9JFa%F%CB4R2s|1OS+>ifM+W+RSbOHrlKr2a?ciVS;jmcxx-qj5&cm28EqC1mv*Oj z%b;+Ij99v>H>wfZ#}Gwl3nDyYy{1O*fu< z%YgF7ddFHRQh+lncMp@!4+S^K$q(y8(sG$s=hi_HK7^g0Le^bjSvuCX#L@myAXgiYQ~et5ij&;=;{(bW|`p( zx$RH-&e)w=`p6w8@oy(VakR zRZ*{4JM5iftXNiJE(+#&k}WuMoWu5^((Cv4?`6%Nyyaeh!;jXOnmw9?;*EuU;r_!N z8hGL4-@I&ijqeT3Bf@nWlfJxNrrf)b;F50_f7?~zBbYr@9^wHP*_POdxbfHac%fX2 zXjzwgGm&}T_CzFOy(fI$ix%fJz@SW9BCT;O)*xN-=+%QO%938mezA<;)1?<|bF4)V?6ySrz@}pbF!f34r37W_R%*K7<8Z}w z)VVpQ@Cl7uhMc^mZp$osvvC*%!4(k4baPb+x)^=qqFTwS>q2|$h^%w0)1uL;feqS{ z70>(__7Cxp`A@K!j?CGQ3)pAsC}a7i!dNX_mOM>~udzAGMNWu*(N0eg-al|{$#E{% zO@(%I5EiC~Q~6tFs0g@mm!u1M4@Jtk=H`!AhQ#l#KUN^Lm8}7ulrGtTFG1sw^0v1+ zj*x4p2}Cij`G+A8`BnOI?nyv>->WWy-l7t5TW2pyoTguKDxP2?@k>vK^Oxl7GSyCC zy&KsPc3hv-}haEkKC~)H);dxXj9W1;`n)E*Wee=@0FzCn72{iM=l8 zQkd)T2III-JmM-azKN)JcR~ZU7AU@-TQRZp#w1s`;^p}34}H&c;I7mnHoPccCx}R& z@FPFpR7UB~?seOtjh;pg|AQcl4#l{lKQ*{%F!rm4lgL6UaXc}>zg<}W)N7|CEce}O zu1HRG4Ai8mWJyskPn%)oFKJP-VQkbUG^HxVk4AE~?rEQ*wCZG{lxv*A-W-*PwzpGs zoOJFVl?q3&WVN*fHLch#?uoNgOqrzQRLF|glg`&-ohGx-NlIa83beWcIIVxjmC!6H z$^xGb4`aty^&c1U{g)>NtOXll281_yWbK}FAiUH9v#}K`))DhZIGYXB-&ZUciEkEl z37`y1<|MtsPCr@y^}PySo+EMH#rtr9bW$$WdD8?pU@M}&Nc8fwg1>P*w z%6G&fu9yVWX1qIUT0mA;i`Dh5cHX-(v|q8A?iq=>(er?R!_HqJgN~SL-Mx!LO><}u z^fU&VHT2^RmHCiXj(89 z8pk@r>EL*_HNiV{uNP310l2`41_ce;>tCwcurSSLp=k#ANudSuA2s5!TsN4idb;gR ziiXN~fACEuw>^seovATA{?k#J*^oaKfs`BI1G-pMc#GB~l+I>0hxeig>736>JHPF9a3j8feyZcf~sm3pUf z6HeoD&nkyWeP+SQo)L9jo8HptX2Kx(`BXsWQy%DUe{n@7fi)%JM1Dp~`Yld_gNBNu zoPM5Hii?O#&O?yDq&Xz(gq_Cu(1K>$XSQqyO9(& z9F4}MiLur(QW&guiAHlxdzvLt*x#CviB%7318`#*sh`?>=8)1-v^OkAGAX{8Jx;bJ zBo5{U#Aq9)ctX?ot^tck)X5Bog`26KUP@~eHSVnnZx`;0^Cd~FWvIP;Q>X^n!zKg6 zbOc|ui5a%}GRyfrK-Q)Y3sGPM-5a?-%FA(8w|f)$vq<##@pHFE_j~emm}wl^cfm41 zADfd^RgB47k!f`L-+H=F=OVaCe0jY#%1~T^mC~fk^`$fQTgGhmoRV%Tdm3&2huWkm z2sFu{{o~Cd67nX6X>9ADT({LB00(aPC6#l9$^SL8Q?V_25*x;!;KJQY6@@vgPPNt}U46!(9Y%8Zm`7Qq0}P z{ZLD;+45oEN*~1@1>pVZWcZ4J&ju}yAJn|LylE9v31}@+ZDu{No+j-7WzHp^-kdI~ zTXm8Lw`cjj-ey)coNBxf%WgLWV`}EZRmu17$3xT+IP)Tl=a?N7z zWWJ^{urUh3%zRdDJxU%fhK+S$&AfqI(hUMT-9uCH_cDW4YlDs zfi%jCE&~Cr2zTkO{L*OxbK4z~#ps4D{O9Yd@^>UlPo?DA<_zN&y5n|znRhz2*Ec&# zbq?aD+IdKMFTHd@PI$66*9SO(08 zud$#Z8;l=Pk5VSXFVb(Z1b*l=)PlC@HUd`ZI(HUN+lDpHbGr9ba>?sl^rIyWV?BTl zN7F?yVsUD#Z;kAtit!Ob$*wEfWCe13IOlFg^tKGsLBWW+h6)rYF=iP~*VNX4zJ*U7 zhWTGHdinl}_|2}*8JpF3NVDx-R==A}W`&0I!#W13*bVphG%dJ26fPzIclqy5gM#Tx zcxZI|htwa=Z;sWjTpaWfb7ZC~5m*epf-G|@aReNpW>^_ljZl6~KPSOWdY0a3 z13qTM`(c{f3k^oc3?|0_NAbps1JR(Q63Tjump?*(5cbc-0^bgNI%3~+JhA$f8JVf#W@o;&z6HVSR9of_fU;8vmRT96iD-3|wn}Y9s8-6rY9d(xyz!WG zr+uINqgBQ8eDc1E32Q%_?%r7$IRal8Ux>^@eumA0t8MO@zdZK~uVT_Fia@ zrW8X@JTfLUxqD#3$d>a{>X4l>i6_l<@F1L$_9KaAyXPb~SyF95p&GyCTzs1YMb?%q z-#Qx(jt4%wp@-(ArQbO%k;A|j6tItD>LcJ`_ErTHPSiQioEUWNbXmi=WYd_Cu( z;bJ`fPbOcR;}0I)VPenojSpdbp8oYmTU%6?Di|Z|D8DOU;*JtnIXxj2IfEmnG8OL1 zxDw#tuJp777V;fLA3GBVD|^xOve|6p&$Mq^9{TQLc49*ve_bVC%`dZb6b~#i#S=nT zgt>qd8OOeKc$&E*HMnR-v=!PuK%1(J9l-mpoAYa+={9(lcc2crZ5S(A=vxxaAA&;A)Yj3My}j`3u>NJYTJq zL9Nh!$`36Od^&^jAx%2MumD)nll%Q*YenqSVyVxjk#c60JSRwf@Qy}iu66<Su#B(O|#KKbm}}b;Yez#yR9Sl7~5PGvt&bv<2o%R8SCF ze_bmAE-3ShMBNXJ%a3r9o-DVhKN01C@_iU|xa0slrU^F*VJfzo;Mh;K1RpR*KFJqz z6uy&#Asb@cFWs1>Bta>6ua!~i;3*k$Aa&hEuUG$u=y8IDGhIpj^e?`>z}BT1?0d^S zLJS}VYeJV3s_~nEE4IqgA?_=cm@e)ywdRIKOnmIjCmDUlGL`Q+L=7xiTk2EluxC=^ zQ|lXwQ&Z5-XYAV3ww>ZQ6sTmi!dS4vD%cTmNWFh-5#lCE8^y(@D8vuIr!3cEdoJ4- z#PLJpSQwZEW9NoCPYPxi_>|^v)rU3o1t}KhQW(>f54c4X=w7%n^zSSqP{3maPj*Rj ze5_p;5#{nwy<4^~XV)c?yfWlm(993>&Z3fxY?k6VOXpFPEX(|38yRY$E#gA5($qR6 zd;FT$ff~q=#nY9!in;br!zT!>4J}Xp0+`9tZ^|;sEVUV>9SlzuxhScX+o16UbJcxP zxXfgfS#a&o#Xx!nd2HOfAgb^Qu|Cv+-6WgOsr%5(PMz4N!V*p_Tq83Jy5Gl6MhEaX zKR@;CSxBNLXSo}9%|}Z5rEH4~z(-BT$HBL~S#<1=hfe*Sd7*H(eSd3z5fd z{(6awgDz7mBx*c%XE&S2ml$#59$$@YE09>xO)v7wR-rhHt=sVaeTbCP^p zHN+^n$*IctA`6)1Tod*qs#@+LXXPOyEb}t_U`|zhcCs1MBWMJNrAacUMq<$t!#oN3 z&3Q5|<@XaGCrb2kt^H5lwPu}o^i+)K>TMESU=rkGROSZAPHr&E0r`M)-WK2z? zG(^pk&#VlmRr;-vvhxc=J<4nSg?UucV34BpT9Whzk-(cVjv*FN)}xR(&L4~2FoZOY z)#|NM9K&euEG09h_FFn2s9V-7SWbHdt?AcKGZKt&KGD{AlN4L)ZVR)t9H4pd}z6(}$rB0e58QGZkgZHdy}sL@69+tb*ccE4)YG*O)L8^IYB=0LT(Xcf=OAnDho zGmBgtp!+WSec3Xeo;i&z$s&ornF*-WL%q$C`uWKHa{HIHs!W2wFw^K-`qPZh!KT?( z&~Kv)+|lxryhbgOXe+X?d1yu%s-P@g@IJxiV!pNCPZ`^gvuq}Eq!l=|>N`)(iKk%^ z`LVX2H{%+hpnHvVWEJ?qZ*l`u`>NPwcCx`UT4`&W+i;_Tgo#)tLD!mzmU7@yqq$hQ z>>Gtf=VSL_#ukGL&xx8*QhKw>rSS5CzoC8bp6In;Wu*HLmtU}Xv$X?AQYhn{?$&pc zywvSjVgWae9;f(h8F?5&|H?_(^RJ2D$kMap371lMAw2w_cN5E_az@|=*5-qwQnQri zCbz3vf@aV+qGq^GBo*H#nGD)Aj#WITj1US3{kA$c8$OnlJD0z()DttmeAJHq2pErt zOSTsTBzLl0wAI}xqGZA)W@V6BVEdUA(OJ$fyVQuE>2cW2fA6mFcF7R%Y5^Y z_{pnxU#-71G!uLFzPoLxUG|T7Dm<(3bmeQ-(~dKxDEwW#DKy|*N-yWEl}>z0DE$pl zdU*f&k)QfZXCZeyNy2f~V;A`5chNBmrFS1M4j_~5CJIN>zzTFN#1BYDePgI1|9`?| zj50?FV%5F95DjfJ&`$<@o9K@;*-qiK%3(_iKTqGPFDJ`?sk);Hc z?UqZQ)Pz>W(9NLx%=Ut&y2lN|xbi`zorW5hCW9&^=@}cY6(e>6(}TF@HZd$FRtd0& z^3d`!+8+J*`@gueGxcN8r$RVrK3`ZdC5^Hri2?hF&h?j(bll79tCnAb>YYF8hfP+h zg=JP1DoF1$XwMz*)LbhwOVYA65o744GEql6n{MU>0Gu*Mph4Xq#uk7f_!Q1pzA5sN zUiq0<8wnb&dUB5jT&-zh$|d1p$tDT=57LFmMq!ayRoZb_c3V)j2p#AAWv$_ve#(qM zs!Y>N`KhhQXy+))iJts@85;k;hN>K>Z1lsd zZA)1agWV{*L%X|-`gc;sVwGsxc2T$gl>N(Vh0gyWAJUjrvDaL+sq%h<@H6at)-&KEA@s8$ELP_x;#m( zool^JuEDw5j{4M!Zpp_s>w9qooOqx@;MO}$`9D4$tkxL`iFmTLV$Zc-jH*ihOy>>| z2!!(*ctOj4tK5yQ_Ql-%VdJmfG$H=*wT8csWo^5z=VN}p&k76ECW^2P%}!7GrL2)M zGdTBVi* z=)1%9Js8G63W*3)++tgJ!QFw0^j^DW8VVxLLHh^lKmXakdJWWvyOnY>6G7yN0lW>j?{BBcM0 zgoz}z%olYY$$adNIl5^~m_JOJQzLhutU6=rkNSEmT7CC}zW&8Qr3$2jN0y-n^A#LQ z<7&>-7XCKKJpipJi>*}X>Y|Tv6Xb9^_MRo3GEE2?{(N;vhPc#o|+|(_U-`5e(?$Ad!)S1QMU9z3|)<7(8xPS03uJ zK0m*DW|#=SUQ})Q{c!Qjbr=6rT*OTZ8>G>Lbxt>76P3}~YmPU1Iq_O_wUXHe4)J_K z$>T~qmdZ7bo2XS`^L6seJntQg1U<$*ntcx2JU!x+2lTw^eXNEgxGLHetretcp7R7= zEyZX5ErzK6aw7!}A2b#ZA+9*fN9B6hSmDL%%^ZL2l(v0)>q_uUByk%w3*^(N`3IO1 z!E2q0+zyR%n2|^_L%HM`Nm79&nn)^`%qM-JTt40@5utM#3$R(1|N7FTMc6NJxKd57 zmoY2x<80|Z^zOrE|0F%}sHryL#6pnGU?!YPd6yWLqb$2OT!_poE*A)q*8@$4z|sPj!3B;4}JeIJIs#|{`j_R0Dqy8xi@AM0Gdu4mc)AV%wVjI>A$3Pz-T@V3g^x!SfVz&eNR zGia%PkL*G==FfGqd(`^KjnQ_8j--Z7fQ^<<1IDWi^HG9`VWflJZI%xvRf=&TxkZ~- zV%4#EWwYD)+Lp2pQbY}MG&Bb`#}lbOZ2$NvX&}iCG!fo{R#TyuGc*@wov+Hcc-^@3!t!xC_zZ3YSG`MMC2 zBSRRIo;EFb;clX>V{7?@udN>DL$;ppba8Wzw$bX?mbDn-J#{b;%RNH!Z3i6hE`q!4 zH09^}{()&lBho7vMdcZ}-f(_x-el1 zPN#y*Zv6m8&}u!QHn6F5Gv>i*pr&ML!LFuAT=j!AIC1t376TE`XYD~lk`~M@d{Rqy z8e4%iyPOB@*#R_!;YxM?ny-~vGz)&E9Y)&EF_5cFoBb!WCx8YAA3mu!iOR(_g_&HS z1gK!agxfIZvLUH(J!(juxqxwJXhd_t4@rWlxlrlvwfM@+r_{=S@m zow9>?m6UZtSt416B1q!Rk2Q~%8M`izSI0r~p>#{qq@l;ZxQ^nRLX!&`Ot#LyWvpFl zA{q`jr7u8da z(R&xXSrZKfA7kGZBBcZ6^*GY4FqF!OL1>^MUn{%D<{!{xYb`LGevQZ5aSTvU_$W5Kx_W1{Fhm` zp7QvCcR1)AYl4j)1L6E;}u!HuHnD}Y)7jvtVa}a5#<~*IXISD!W+x>l~<8GxoZ3pYNvY6C)b!bfkI~s1GdVr z6GV{Ih7-UnKGV=xs&JW{wu?I0zWIlK^@a<6n()ps+iQprBoIgSYRzG=6D)|Ta=y&2 zG%M^C2dobp98Rp~S!&5!j0DmE704WSau!_sacO_$ZeBp$a>)SFkK9>lyR3ZkVu?Di5z$M=kaL8_8Yr?ccowNe_49J)N?II8>Zme1{~ z>H^RQiic+0ZpWm08DDXtkYzC@)T`9YdVZa#Qi?3SGp-(LZz{atfa;$-^!m37=_Q4{pi!8y%gHZ71hVB>Jem5O!Yh(8tE z*0NJ|TUJi(m|B>xo8Lf*j{TKhzYCaH24;?8-oP{jxo#KHy4~`$*C*p?* zn}ZrcY{cKucP@B3fq|@OFOa6u+wP z*s*CsMW*HB=fh1JXg^n$OUK2e6%|!bt(5%s9qVJ)0D& zl0W{Ez%c~v8Xlm49Y;|oH0($Sna7eXC;N649O|&JII5D$!z5N9Q&P%yjmqc3kSf=^>qI0YF8A41`f(Uj_(86toq^sAsHHHJkUbz z{JD0mNQD%%tI%DVem#6}5bH?_OBZqtjna?U$6qOhB=KaNC{cri+L74Hv)l0+qH+>KH50=Cr7-rIKMpY z#d{-$0<_E5|1=O|^Z6PYF0Sy5V;`kci%XL$aM7&k%4SD-udhD>l9FlFAs!0F-Zzuc z_WybALkQOW4$@JDmSgCT1sFspVBKBnmlF%RTe1^T{#XVFb3;B@zLPVU*Uagf)93+Y zw#;bC-^}2U+7Kh|n;meZ_nf)dF8MAm3nciBTo8{=3T=rqR0$eOa;XWmEr9A@&1~$y z$RaztnI)ZocAE8|zn-3ajT5YzWPn=wj{hFpZ_}uxLF}28SColfQFvb(xJrq}4aFSc z$W|_0#u57-3}8k=o{c)075K@7OQ-nykjTDFOPuMMzu*#^jAo#lNwBmu)J`^I$U8xf z?iaH-I~+6q@eesdn#Vs~w5wTuzJz~v{lnARC@K2@jsJ2=#ad=Y0W~kZU}|){+~4Ha zga?*7?%>(c2AylgEGNh}wWo$MWXt#(3p8Q%ofKNYfKL)P+Y&aTb4o`>E}~Ry3BSFA ziZU(a4pR=+P-wlPF3${vA3_q`=_s<2QZ(4=5-+M=(cPR7BVEVRbeDKv(Skfwi={W- z%rahRokE~368!f+PpX`~pK?b>6}^yj0ilpmpw4wVrN1XKcZ^r(tWqR}+3&YweG1Y7 z3iw()tDWsvs!o@<$N~D6NgJMg(%kc=?Cb?z8w`}U3+^C{NJDqG5~8y0R{IxScWL(g zt$m3|(&3(V%Fj@h0Z9|9Q*ptH z=t7Hw?M+IC$}AP%K7W?M7{$vKk0`MC&ry84%(90RgEJ(! zL-4@h1a}P(2=4Cg?(QDkVQ_bMcXyZI?(R90`+dICzoDmlX!c&K)>YN-A9;i9AuNe z?fa7w#p7vtl^4JY^%$q%dqMlvZ#f=~U zb!1#i)yVW%d|jqJQ2!5}UvNCwJM3?IR^;QA+e;XrYHoV`&vVsxN zH@tdwR`$HRWsDk!6Ic-#ldm-<>lnA-JrKm8?ul2FZ3AX}-2EvetVR9AY2QwW&;(HC;z%wj$&srHl#T_Oe(kl-UBJ=y!xa7OaD6p)pJDVZh1Zv0$2`oqy5$PmS?93+P0hu!L1)zW-KxIa zbGc9ke~$X9#6x8uWl8IgYVvt4fr()DaU&w8%)ML_Q~&F~=P+C>!!{(f>+Iq_s~(Z{ z<1ACb0^0Ydz!5HF!;V{zr=1`q4dL{_fkE5fD`O~S{7)FTP6`oP=mt&o+JP5l3kTJe zwsXG>m6Q~g_%||!7ZsjnYNND}JX+%Zot)T1JI&{ER@*_4y+r%@XmF$s+7~ErBo1Hh z`eG2J^)&bQc&X|?o&DcK5E*dWyr(@rNH;(zgj|e){ zedivE59b-Ztyfz6f)4x?gUj&u;&H+E5DFE|Sl><&a3MC8O)4`m&6Q&r?4K%w4sO=} zI|O=WI7x8T35}H|S%-f<}w^y9HPHBB@68ygl_#`X9LLVKYMf?dtbgNUOf8hc&C2&r@$1@tD zU2po|b%~H^zEyp}OosDqm-8T4aD|kiQ*vB4Zrx}soXH(xj~xnQQu(K6DHi8oJelnp zD!>pm*5bg6;dU3M)nNXoiGa@(3Ac0OOG@_A^^41z+1f(BJHp49#d&w&aVTc^))MhQuJ6+@ z?hl30p65$isl4Gl9&^v#+U18Z@m71rTeJVGHo@KmfImYK*y6f{sz7xIE!bFW?`Kbe z91A=EV<-48Yv|}6fhV?q{`b>Q2>uKKwM>aZ(a3ggHyCdEcTGU5hp*rHxn=%$5nH&O z$MKM1d5C>mn7`=7%%}X< zeuIy_->}-?5ow{{iA9-9(&~lyfYpnu%k?rsV$6?w4JSRWADnYcH&|}r;k=c%=?0bS zg$cUGbPoL||NGb48yvg81l1H)tTe=YhfN6ozbydcqo9!|sdQ0slLGZ|eqyB0MY%XI z6ae}C(Bn&` z+rt)PpZnZaT)Vr6jLLQff~iV~54ioKcGJUCWpzx7>H9M(p5}t2!N5piVk7Qf{u%iA zuJw;I?PT$BNQ8`q^EK-h=gt+1{vkX+Kq2d0oS;ZAJ6X&cjjYxfcXKs3Vx+=QDTmPP zpffF<^nbtqi5P0WYzsWmYfL{THKt5KP@F@pEswRA8jAZF7*Ju@oo3=_yccmxWwevV zD-a$1|CT_>lpnWE^f4u*M@-;;ez{zA!t)#iP`ck2c#f@gx8Apz*d1+^`(^=3F*dNi zrMcH|P>NftjEgjiMQ^cTE#8_3k9subK~8kWjant+cr{ZA5k_@tW=#QSMoOh@=`ihJ zPvLh&)2zy9I`f6NhvW2maDQ$`y6`3O69!_#Jg5Lz*Rd+SZXM`)%k176#s*a@RA*jO zVp@(y%M?IG+PDN@A&{#nT19nBDX`R!HeMw~R@5|0$E2pDmhDjNa$8(TQo@6B`eTo5 zQu67j`)@?}*Q%KIUj5%~21{1TD$wfr6UAtrm1r!!2IoZ^zC3if`sIa^C7eargU`Gu1d?wi7^lP{Ce)i3yOen5N=HQ?YAE=Y>4;GvcjDU(6d5kC;oY&xD zaTC!WYQL-3CPwk3^3`_Dby1>ko5I2num3!_eqT!xO=u@k$O|-Z^87oj0e|=;P#qXB zB_ApTJ}$vDE#tSijL=0z67p317}S z6glMUO@z_o%M^SCywEjKx3X0%5s$r2F!wh7c2fi4KAIoFlDy`GFN>roTF??QE|h6E zo2@Krrk>pVW@HlaGLLpgM|rHU795@EOSdfGEG;hUdiQ_c@qY!~PIZo1(9zfkUIE;W zT{?^Ken$m@h)cM@b$r{`H>1ecxtq*^yfH<{&K>I;r60(YCM@u`dHBH3dLvAsKOCAC zwKhuTLYqZj7W43n=;Psz5C!2FFFYp=Y0mD37Lp4|%tNiT(iJ{f9BnIH*1X`pAia8e2k}M5p{68r#x0MVINRF z@MQTmoR;i!*!EJr{;q!P&oJ~gt*haT&^~%C;Jw`M+~leq zX@2eH2a`FLH~W`DEiTP;dHo$9&QWv!bUQ>EREj_kv4C>vlbR7pJwy$|6gn z-W8tKKh!p5>hyP{X`|^w$A4Kd0d3q| zEAc}A(3*mm?;@g1#t#N}EvlPUO9#!tGt`?vUk3RX&E40GuKVr~CEg}=rBWvE5^sl| zlIpyHQ#-`xZEoNprGdYJa#LM7cO{0nhZE3dF!ij}Z)#g_jO4ZFY;Rn=4h;d)&UY`I zLD|&|9max(S15DY)AEssa1ojl+0TUCsHKoEVZS-S3XNNuh(=PPPg=J(2-q6#ZOekN z5WK!zvrt$kLijgQbCWDGz3%Jf9xK~K>AS}e^nqL-{4HOe*S(4JUqJucsyI~;h#0;u z$7wV@BQQ#w*}feVIrampR%(%s)FYhsDWCPp*8Fec&(k8b^rMfrbaw2>tAdP^9((ml zfE%La-N8yD3tVgsKA7W+($SH=5hsGn_ghG^%A`vhmWA~@R0}6CAWpPk>R%>iXOBV) zMf?>D92UX0MjC>0vStLkm} zs6*v%&+E6~FAOosi#KguPfPcknM0|jl=MQ=J~CaPu~t>FZh2mXLxfFJ$60@R=#7;I zSubvkXoKS(%T1aSE}xrf_M7use^zoZA>3W}#+3F~NB%MHW%F2(%X7Z!<(1G{EKE_7 z_Yd={UVosSxxB{m$w1EYrMB&UjikbObwp;{HB+Hnc)c1{5edDX{4c7zr=3Qn#^TeB zNVF=G+1@1vI}5 z9G@HVXTjj7E(6`iqoaDXtMyN%qjr!Lj^QEDYcd9p2AeI6z-bOil7kuMy>Jh)<c;ADV6XNYhw6QLHRdpu5~w8LdXySdY)`Jq*dQ+XLjHKq;sS|rk+sWEH@5NX{hE532Y8V3Ts!Yr4-4M$*WAt5h#*!*)PEdr)`(CZOl0rCi#1o1eWbdczks0q_ zuQSK?Zt+3jfxKLa_qorT?uPoBoTqoY?Ke3A9v!{yInXkdmzaXD!g(_C;-59cMr`*s zJ91=At}mS__OAjwTXq4_l2GM*ZL{e*yAc%75_^p9nWPrco!460-?Q2rw#VVB0IB42 z+D_;I2IcxD6-EANWyQLZfVKhAsqw>~dTeMO_7ZY^>?D{l_an`9nw881o zd;_fm6;^_dYv-rY!HvdU|G!qlUg9b6)N8#v2CtQIBn|cpnS?_;_-h*jlfvBaa6Ai6 z?2IW-2-@vwi37iE(CYb~xJZ@*cEPY&`~0aCYn2me^b!rKpRSVlMI$FbQFW&KRjs_3 zf-}iKf(Lw8Y7fxH``(q(hY098@)E3R-_fO4 zD4u1PNQee`%tr`~GjGyXScc%0X)ivbE-Isi08^D?fxTgxI(uBw6NC$HPl!c=99qEj zQZ1XB(LhCA|N0Y2ZAaKXmVPFbk_VMsEB*eH`X|yOH!&8*r<-q{`CeaK2@uE{We<*L zptoPFPs`4mzrhF;SLW(3rouQ0arM=nVLeWU!X2cOqq*rbH7z3;RAZDOnvB<;&Y7Wk zxuXmP@psV*`Wlh{WmC%M(}GHGW#ZZHE43p|!rK3cWr=Mz^rPZsBjs0GKFCI4~QZrHLS^4cVmkpJLe!fYl&7a<0YbHw4%1*}!ZkVPB z^j(=C=~Wa#d;KhjG;Jq$oTRz>Ge9};z^3v=zXu&1sTYB(x*&lIs!)s4S z*^c_o#c@lX6})oxOvV6)NzycIU6=5dGzi7MI9kzw z4XXB4U8!*Dm@D(D3FaLY8oCpi{54`)d5nPaq(hqx<5JheoMO`m1nXco&L~D9urLno zgd~eLBZQ4;r-K;Q|5Q>9ziVp%#!|Y}w3?kmxd$4L7!5axWn3lU3J0BNKHF=sy0Bw8 z2^!Euq#6BWE*VH)egM=svf=ExhN?e6_q+@W+u$gXHF-Gz5#SsW!`fIRzcj5bSUnKs z$I^pZ-p{~X7kS!uJVl0-7XK!8uOvg^gEehTciFtx#B2835@=fx(G2aLN=KIwyL z7dl-U?d$wu;37hzB=n#o~ye~ri*vn zj|mTNBaCN?x@RRXMruaDz&3ZJaI|7Zt}laO;38Gg4s%q5Mk@LRm8=LS_IpRi+VdbI5g%CU|@s6+=N z>0gxLTW5%BSE!YwShjjU+TvYANgruDBP+0)U2d&r5B?z7ldq`xTvt15m{W+3G0A6{ zVL2UR+Kj2E_PxJ|`4~vSA$=c|?EU&$%3IH=NW83tKL%zQbQGV%s2Zmo#;_yurGR(s zV#Jt^3(2*bYN)HG0J`63Sv$J#i*b6bF-u#<4305ZsT=%`Um$dV%OvfEolT0c>k~hv zBtD;ipaYZ>n7o&Ijs;$$!FE)_LW~i0fyJKU#D|@KQvj^?z7*N|3x%wUWlJ?K#xvg0 zL&-!zNVuthX=LJt5li`c>(Ykv{EFK8cS`^wK^+YTUy>5=X|MLv>FV%y{?M62IqP`< zCr0UKOWrgP$|~x1nDpc1g9cL|p0&2Uvn<$Ez0m3|iTmBWs~S7@sE%dCg6l-HSgidG zEW8^iEy*=i2pmyGh(wF7nan+qe!BSvJ2(fnZ~4m@wg2v(3WKQPZNH8h$j?H8iwV-= zrN%MuRrl}tHCjeq9KaX5Nz7|;O&`@``oB8ME6o&<+mudk+7@|avJ@yGai|9!-vQ6$ zc%bLS-47|8q>Q34DvZ_LKrlWV585qKmNtO5aMjb1*et>@MVfi2wY1Qmn9W0_lkh-Y zFA6=0SO4xZJG38do#7twYjmvZwnOX)@Z(yT(PLCFiTXf*qP~^a%CO@lrI719Z<#rg z>K44~CU4{)e1(WXiSoEHeOENkEr&&4;!uG0B*wIWL98F6Sc^>9m6kW~#PA5lt?&G2 zQXT0XFlikbPvfT1)5pLr-Y{J=yZu;ei3~xb7XVMHh7=44;?NhDgZSUGyx+Nu#H>Mt zoXr5E4vxYa7YZ{1W=7;ND%LEsmPq>8W>%(@I^F1>d`%qT@3QW+eI zA8{BYPQWXKD5E%I0eSW?5rVsr8YuGvbkikm$ugNG-E5_2eQ9RISRnlM^M&t)7e5US znNhdp>?tD>-W=WM*q=iTyWu?HjRXY9`*EyhD$ZjdfxT1?QAxpF8_WP|6Z}7xv-Gn6 z7&$(5em4#=_xJsSYfH*GHB_+~MHFezsnDVjiMB;zKWQFr^3Z^a45d1D1 z;E)3B*Bqf%qEf=R<;O@m2+T=ggz;5Onpv*z*;9>Vhd4+^BOrW9zk|hFWGVh)s-e$E zRP=S>$?CeWa4PM$;+)&7;rmiRY*2eSElMkDH*j+eYgFE3(uemuV3!c5iiM z6-xq!nUpWP+p>a}?L4;rMhwYn0+*u-bP>!$xIX=LCxEw#GH zvjP21HiGx;KYf!O4V{Y({YIPOwdvUjelx((Pc`zQYT6QwKSzdi3}qm{c&_V?OFDoV zJ**!RbK;qn1Ju&?vqtLX+C5+4ZUM%63&o<$tEgEZp)AsTdlu86J#2uEo=i7x0O4ZGC^2ZP{r!L4?->({KsYs?bttdVGeW#q^Jv7M%tqfGkqjqOLqX!S^ z@^h)O_0{2hZK$z#yvPjFkc+Q&0anVlyA%iqD|O;o*GU+z+rB%WYR;3yVD>d({~Od9M|XPHA-P4g(cg9AvKZ-X zE*a$$C0iK>ry^OW@8q#R0psz!JUZ+#8e%7FppYQr?!aPl4c*E^deB9TTLB~GR=rWsDABo+;MD|MJ89zjEZJI89V7q<8 zXDn?J9UKx$?KAAtxqf0S5z{aKRByh_cw;|&k@_2o`&(!z`WJOL{&)tuIq2zT>Q1Tt zAezqsdimHeFeX&Lj=ucazv_GbOI7v8$bSUwqq}i^T$HZc2G;cs}j2b znv{OsR6VBdKKf`(cWKBlesYdpIhKAaE#x~&dA^hL{KKAvhaAmqz5isbA`<}-?rNGT zzPSQ-mODkB+Q-jSg$@)k7T8e^HGL=2vjCK|!U25v_3bxX66tUR{_#WV-ZHvX`duq-Xv2fNEIE4L1 zc55LGQP|4yOA#DqKL_?$WH{dAX0s>K-Y`Y_Ih-S@$q>Kwe3diL7H###-7oF&D4;Ta ziV?1`bKo8fgw(Bl(PST;@Xh6CgE&=<^B{I>Q#=-zapJ7v zF)UOD?X~k?^T0d2dQL`-6`{n@1@Uv+8qUs=nItv&XROlD5h~33ZHBqfz$A~1DvvyL z!MS1MJrSP)&#!jw^4|=S_r=k}Yim*9T$Hze(s|D>&>Xw;+j)S(PLBrV(w^zgQ^NVW z*YG9sPb@$4)_lemgS%zP!dL4Vrh|9r&sV8@AO#x&N4@;3mKwvL~? znajO~@RY-}Jx4wiC}(x3ei6z5%@@~qnNBO#Se>Is@C7``vR4>S-X@KpFW+(m{S+ZA z5^ny>dYY6Lp!0>$396*qP@CjCj)cK0xbz1bFRqMw8A%p@ zN!`I|DMvt#H|Mgh!(Iq#aX-8(Yk6fX^5brj%90N$(5vM}VBfWX|djQ!7{>F7DHwwZ+q%N=FrrXv78njNeid)&PJ zVVRE*otY4V{~n4I$#@=pS!J~BfG{T&zKpeOReKC~|38o2Kh+s|+EMcK|f%iv)cfI^zE+ubaDFtGo%aiR(NtIZ4pQP0ION>|w;zh5(WGjS%q zK%sm6cF~`Yd&G@ATmKMsO%k7yf^Z;Ch1$Fu=E}%+1)I}4c%5pTTzR8?Py)71(t)N9*f56&4logp0*gHQ%7}_6fvEkz&!gHkHAo?a&*SWSj>N3CM zPa%9EX!3KCJ|W(k~U|zR{Rw>`=@Tg z?F%*hWG00muSuuS7OrDuo?!Yyt4W$rS%T;hN!!%t_+tWLzd`u3F!&`(9iC>k0Ysz} zU_6F%L8ifCDj?=HJTrlrBRi)eV47w=DvQI0bHhx5jAoyaYYI^98-ls*PmC;7( zr66BlpFG|FV_y--E{dPd`w77Pz>xFb2q}vuv99+PZIN;T0GC5+wBUZAFtldy2A7!UB90ECkm+x8t3t9%M5tJ@cy_i z{3C0Z+VN|{8LCHnoH%iHvjak8n?O|~8B-r`s}TYl$_vjA-xB);7rUW~`KsdmNrUHE zJxmwEji-ZOBTSr9XPTbXFXVHj=wgXUSEQQF?s<#6C!?&0`ZC~{(f6{Kz^Jrb^bIZ7 zdsfgz)@oGho zgWKc&VrSre7WACk<~-Q`w&s(Y`LKpJ%vif`_k%Q!d}FvZDt}opT)6!z>W^Sp-DG5h z?MLqVT!zi}^ZGhkDZq2v1O^yeKcP*opr=!XPWc=wno2iYe_ zGFt*jLN}b50kTpIjr)3c1D^tJ*{2kGQ1v!Q*9ac_-)&Od4uJ@fFM6-V>Rd4APyZ6m z%n)RiJ`Vr1w`6OePOe0Y3r;#IQzZ8wJPB^nIIm7t>nHQ|J{yPLGAbw0#IIKjd7`wV zpj!Y1opU^KsoP{}{=#IR$=ZcHw}*2J7SaMQvOkO?(oql6GUq}>mYDK3p)lR-g@h|n zmp>N;Up0-PnUiC9sNUsE&J1L3d{2jc7AtV~`xP0^l~#~rbR;MOE}EGZ?!hb|;a70z z83;>CyLgNb{3TBww@bCmh6R`acJIU{?c7Hj^pjntb;xaEmxolx!X%L1m;pMth`g$04uu3yi>y05^EWhEq^!=!SY_aM!9R>o_$o_^wa5pW;vbbfU+j9ciu zZevG!WEPwY*;v6s`ICZVdg{UV|z4(?qR<~iEm=Gq_1Z7H-wUr56(yHUt!lWpzi%Ghdl-+H%xzF=Jjp}TCnVU ziZV}JbPWU$_aoG0=9vv5)S_Jo#_A^~l6s_bdoNv9nSS3Nx+&r;rdp0%2|JE@qi`SBlgjyH*m^ItBjdn_nG}T9?vy zjo=?J9F8U)C&@9~gq&Nd_B&5-nfH@mE!R}vGa+xZYa9?iTvIj8Q^sjnHZ1wL_3E@G zx0WSclGuUGtnfTPX7m&H2?8v>wa#XjHQ&xe#hb9EF!dAswR7mz+`W_R5a6ePJM5~< zCt+bR1b3qvO*&~^PEN9`w(xvY-M|0j`|CHB?lkrpmpL`hmU=hLn)@`Yg5QalA4fvF zv|(2unovPb{1J1;~l2#HcagdF&GHs>&g#&Zt&&e80BUV6HeR6Hg%)G)HE z^h4s``0WiGVmRRa*k>x;x_96_hDr8W^eIE*d+`j*&Oxc$Z=%7R7H3k|Bku`W0ARi8 zjnyr)Livaq69?0cI(X5{)p@XQM%)ih5}ec-i&$PSg-Y5oSd3ChE32kkIfFs=pq4y^ z`%*3r%FZQ~qC&ZRjd392h=i#LeUITG|25<#N9O{bq_~4)k4kRE0<>){8wB0Z0C&*A zLUeABu|;q~o*!V4?Lh@?+yU})7MREJsrSCAZanWEEjpsl?_+-Dv05npmLPuwsH2JL zqA@49a2%_vFQ}H>-wil@-MSd{tOONO7ft{-w4!0r4P5ta{agDNd+L; z74h(xznzF~#=o$@yqNz^@<=jfYKK#Vodh~f7CJOleW@v~u+rFQRRm`>MJSNJq!bmX zQBoI7qZ7A_de^<#zqhrdiv1pi@UG=59j@bUi^o2eCUgCzz#V?yndysRcIegN(qe-J z%9tZ>OKe>aB&4WoY7pGSLPxEXnL7VCf$ir+u#L0GK>R}`UtYT&?ZiIYTD&s}o@CSr zD5~FRy`d{m4dD1mor;WpnxIW`lx7QGMhELO^XjxO!byB8^JKoGfMlu_QL`YBZeq`$ z>~V<2<*}A3cx1m9--|>cUaDFZ=c0B6zM(x+rZj_g-w8180~PO2)f+I5SJzf|iJ96C zv(x^9gn1sY@Q3%$9YuqSw-en@iL&(#2A(3Zl^{=!MS_%*6P3b+39nz%I7JC$bFD{Q z)P2TSbBsS0rqAsTK(Pf=ZoroeSIfttMLreJEC=KOjbZoHUqWtl?ie`{d+c?VtEZh2 zywtv?-yXgBvc*wK%K2V%`LW3V1>-6V?s1x;k*eNGl8hG$a>^%Rw)s+;6736s9ue1o zWeOL{1{*1<4he8OIiKLT_BP;<;di;)Mr{cv!(a-9^7u# z=)Ias>dh0--!=LA&ImeF93>8t(#(Hd!nU-8`Y4Yx`T6Z_!XcrT(H^DMngfrQW&d4mNy(eRAw)CfDn<(d2<=Ci9TdlhPrfZCr zDVv#dj_tta7~@&(lzud!#=@`_z`xc;W4T1EVAm4TYncpW9E?Nl?JTWw$;1tZrJZwa zYxUf*KzkXFH{C(geG6bW<~YNuntYjEQj3)-_T|{7T^4H)eLVWu7C4F}?yEzH+0Q4h zyupBWp-Y+%KLF26?A6GXBa#j$;@2um^BWPkOh26@z9toRPR*ABU-8q_Z*})wT1U7q z`jSu?w3L1p^d8?Qi^B(!PY&e*!^TW-gcs9C%)oJPqLP{R*_ZkV%KSAKR6r?BZh(@dRm!b-9ChdI{!O>a!-QVf6 zyrrb;>P~7nn<;wJOkC}!=vly(CW6pzuG>)8qe8VN;u(2s*C*3L4kJ26uM zl_?CG;=5Yq1;(BT4J9dIoWE~8i2To7*L zf29gos49sxImTBlYI$CPxZN{gOI&L;<6g7iOWFBqz6dZ(_Jk+Pbr>r3WK0fOzD+^G zP&nF_odJ9>V{llcS|`&b-L%@k5UBWRHal+x!ZlpYz*a`dFgr%Ia+SOue}J_dZ9W#C z+uacWPhS{9C!%4$VwJQf%(5~6%0gHgtSH&{g}_SI=fPsitmAfCLp$k^lcp0lRmg|Y zjAb8S7Dds)dOlob;#Ufwwbungxmz&MrVIwJ7WvMc(GvNl8a6b54sLf@FFtr#e{gH~ zOROjJ9Aj2tF0<%c=WnOgN)NBI>zcOA0^X4DzR*2h>CXlU*BS}r(yG*FJmR@@S91si z&81qG;>3`NIQIsQkVBYDI7~8mnXs$f0fWHyJq&HG?>=Kxzt>Ftk)yR|=)S5<3wdJ~03V{vfq)do3)JFn^15=q}G^|$a>Wd61Qw+@kSz>T`nO$vP zQY%M8tg(Mc&+~|USuzze{UseM?-E3^9sXb&u#16Od6eHQNKT3tDkJF_)14q887T+Z z433w!e=cazFyT?-&8koq3#gu)=^u8s%<*!U;L!RkwV2SqfBDxDa(gf!-R(Df@G(@t zZe~{jI;Ra3v4SDu78dyi$L}Pf4hMVET|@$YPPvh&tiASW(jv_iKk+fdeAuB)Qw>cpGdUoBooS*sAWF=nQhL#-KL6ZMw)3?j?e+xL0%2JWQT!wFFH?cYjBmtrISso8Ng9lHf&vR1F5iI zT4;)_%$6(lcX8?73p12S>hVw+H$?0GezG*=sa>6a2D%t8zU6cvDWAIsp#Nn*ICa@( zErThCD%Cg-ZPMveft^+D_)QGV>JVWuPwF!C&VRaw!2~*4+_||X|2&;d=s^NKK z!H&jiz@;2;ayK_ZE`gP^%?5D|?ggBbP-o?Y(wdsTFg26|n=yRr`vR>&=;~R?{~Gy# zNRUT0l){kOB(B9%=!v_wu}HVPKa?F^`r~UYq@*;h0{=o3 z3*1}|mC37TV;MOf&GAxt0x8{URyr%Zlx!qGe)F(CW+7KOs#v$CUlfQlYy}JTfVSNa z?Nsv)_LC`IR>T5a^~&A!S(Kyk+u1@pM)GUVpx++<8AU7-s1qftCMYAQ0%Wf{MV{9^ zZkrFe?MjX*l%6++G{IHyezo)-(&C@CVOniyenMelPp%$dy?~F+1RZ^qT-$qRp|W&s z7rm!ts`G~IJ|X~%bqH_y7C<>> zN}*pgslz{dwLW_Q{Ck(&5U|SlP(F*1i+B#Upha9zbX^3b%cf3?GX35c(*E+!iyCz$0!N z7YYVV7XN0df-#(Y8eQqLocex3f+JJ@goc4ov^ydL1ccURdGaXst)X{i7*BRP^FmfT zE|$Mqz?)?}BVm-k(w+5^ROsK}hiS-vifgh4lfxTo{@rZIcLH0j@RN}zHh6Yd^D?8R_vDwzoO&na}FbMEVo(8{Bb4_yDk(b6Dub26Ut-qM=lHh+;kxo(0 ztRF9oSg{kLY_`~!(#btu#!CIs%MQ35QihAs!%=!|3 z3b%wb74Al{vyz#Vqrh$_RdhSJwTN!vm5VG{-YJH~MD2KS=&;XKb|9!D%UOX#9}t0Y zS+MfhL2p6va=a)-8jlF!IsfeNQT`+JwZ$javBJz>+2V+4$A!LQ38>flqr%Xr=@xA5 zD~A(@5(y-6IUGyabA+YH7&S)K{rr`S(W^4fHh#nycTVxBK~9ms$g%~mZjS;CYLTRA z3$@6QW&QT{UtE5o(S5rdcc033;`-85(-CD|au7K5`1po7Li|M{1JnIu@m##s@%UXS zh1m`le@tYV>_6g93Z|Q`v9KEzAaeN3V)q6GA-PCc;dTzjm9EIZyvd0Lcn@ zbXxn3hOtENbEd1Ms<*R9{0;g&+$D-o2xk5A25o3Y-KQ3(KkL%|7R5?Ib7LtMP2hF6;Zz*ts#Z^0F9Rn)6sV#pz;cBItB8Sv+spw&J=Emjt zq0u+uz`Y7EUsvEPIb(Q%26u1`>YaC8-|XUu#8j6H1Z9RA4=qnCagw}}0H&C3lgTAr zvbx|&>#uF!_W)_1IuV{ADnHC5bG5x%VS|lIGDb}7tr+++1j-vIl^WFJUCBD76juI97)2q&Chv3=b7;ZVM8(Z0PL&1Ey57B$fp3xK~(Uz=~3f(VLv9rVT@f;?nGWpe5^Yrj}#llrW?~ z(A{vY&JZ*!gmkmXcGGNQ%DvXs=D;#mIt=$8U%dek^=tgXB(|v267}e^03C{k6&JU) zPeqLnb?)w|&_dRpzDx-?o~!PRhsEXA;O;YT%2uzdcKe&gpM?-3;7KU?7+&Z~GlDJ2 zvT#zs5Y=JcU_?&e!A?>0CUz(@fzy z%#`;=zz=$$tG`10lFa13-T^GCwTk|4ti(*M`mdB?N;#s8kF)!J&* zUKLeJYZftDqjsrTRaJZM*tGUm)M)L!x7x(k)}{mzF=~@gBZ$aN+wZ-<`?!DKJo1o7 zl3o9f+++}S-N=pWN#L2f0O2H|foqVuWr*3B~s}bZLXuL(3@(w2A z6;QI?q|-1<=Oyry(|mLGv1K{)EMYy44n1_^LTgRw)9+}RSK3A`@-sLo%IKNO4)2UL z$cI84D*vZ4ohOAYk3v7=>E>tNqqjq)Klj@5W15DXT`LVzNVrP6#|Uxe)=M%xndkYk zMAlI!{nte*taPg}iAU5w!fEOLgi!ycLZ`4$PYO%&0;yuu}B(&#tIa;RH- z8VjQkpB;cW><+8c2+a)k=&UP`Gd{G8(_$?Jy2vtLzA@gfry_i6JAE{Xvur(N@Hz_0 zdb=i-9J$p=tED=Fie28uNThJ>a1F)Gil5rdHk@+W0E=2fZ~hp&v%M|8z~g+ zS$9}RWnRk_-!&il4HXiddT&jGFD1=rQd+>(Wjgj6iISpAU&t*KT4p@q%wD-;P_$P- zwzVR%*iu>c(Yu+HHLhJ2bEPL#g&TYCe(<1AKF8ddX}D+EEM63=7JJ5U5kT;onRS;% zYQ4~oMypV%){RhCeb9+Lf}dcRu;c!@5#NgMV#Pg-W1WwgWIe!D;dQ|y9Imb;`_m-} zmUQ3e1pIuO`mz07AxO63@_iSnOBP^jV0xkaaje(7kUs(3#&K~YxF<@wNg9PVM9eKa zRZM*YS;ZT9XM!QnHfQqMJ*#k4D5Z9LpfhiP{!8INT?Ws~wmO%wfS~_@{CTrR2fB zRzRzGwMpK|&5*=xVX1~S(wpp)4CEv%p9j3SjQahFOyD%t$OEa^zBC(fuhSH9x5j3_LhgdLw)n^-+rXB?J;Bopp)CD;7y2(9 zpG(5&Bz9j-$(k|<@)67%e9?Q$HuE8Wh+B|f33GQIj^bi#Omk0$h-qoMOvT=QuzY(V ze^2%9C4^o-Qk(Q<}au%Cf5JTA4Yy))7HgnsH z`Zk1{TvM>b5d|&itsKYMnt;RaIHlO18b#rI`~;)z^V^uSi-_v|Utz4%w~;)6Hk zD9?V+eAkU69XSv#Ecsv!CXxbsfa-&y=kTUS?|k;p{ASx(OKR|KQYw}DfIaI)CMxT` zi#_->FtDseszQsdH+_aDe$B=66-S2a}m z$(`T*ZCy^lB4H%?GOnG|L2$$+{&5*8`rZ=>#AymAj3dN{xKM*P(vhH>1Q=u37ySL^ zRR$N#hTues@s0md5aNpvYOzk+`$iDw57(1!Azs0J;SSjs;xM;2S(X~nPn!z2a~?J~ zqzxpC3Ent*7UExme7CI({Sv$0eqN-@5F#a=)q#EfoxTrl{L%s5M0h+($nNc8%iQ+c z2AQQcrsP2NOvv57%D~wy78dl`(jbln@W|yOk8N%8yrG zII7~%t^xGD^t+n?;$!g;j?%xw&s%ToPFpwgMsvSI>On5=4S(vYxb_57{!O~%v)Ico zA4S(u>&zcs_q|+{JUUnbZtkY9TX56_j5?uLOHYc2 z_%=!_SuVRYUdMv9i%z3u#pWD`>SHQ8h%Wk_s#2Dp|)W= zG0W8q<3+@3MdfAHz3YtIG_zgV@%rj&!1Lxw*pERWt{vzVH8rCPp=--U^XE7Pc z>z$t>{N0@`2-aR*ea?%2wV0tuHrBIN>o7X2Xc-Se**rYd3K|?K#q5@pEN?o=`p_iMG!{#K>CA4MYS>IX|oP2{9H|y5% z@@@J&tJLDMcvshrm!Ip;KT;%DKk&A3EXE%c;h_pFbAMeMZOeqHqO(gP_jX#I{V>W$ zYl7D!E_(|I8$jW;beeffEJ0>iZhE1Pau>EBC0~WUAInzWOk?AMWIt|rGzP)HlleB; zth`&nfc`OUSqCpTqX!=zwRIbD%}R1twc4o9ztPbspK>9Cc-pyZPu)FJ0nuUCIye|& zj^g4^F?m7q(KzVV~1&u38aK!hVyliF1A5f>_U7vHS3 z<9!4w(fY{;4LH-ZLT(5%^~c3d2H_oMN_r|B5{E%_aSFgV;-EQ>wf&c1(W6E7AEg8L z^}X&Nohp}M`)wRBr|$Rv{BW3+em#xS@3#|CE!z-|P2!Mc`j3Bs`{3hxD!`A3LPP3? zcHCkp|L3UpeR=LUTCM{oCXM=D0Ke8?ht+`&(2j!%vk@&D3Krc z?64UONzjpdNF6PQ$M8qgb@08_2ltTA6aN8so;3Cs0DL|a?|#Zw7C;*P;jYAd5Y8QU z^LlrN+YcC*@!R?(_K(KGArV6DR`}re;}Bt7G5Og3Y)KF3wW(b+EHrqomEy>8|I+o) z;+VI%1b58J;T8I1)g8=6@$Sk|U!2rEsQ!Wc-U~7QNqQ?pn=7$kSVzngy>c~n{IAUK ziF*qE^abbn1x}S%FRZ5B#4u<9hq;;qI-1z9iw6}d;10#Te%<^_8hRsQ0tTZ0fEPaA z>9Ze#85uSUn1)F+{7v(X;ICS6r;_PcoOAx5 z&~~3K{$A1C^Z!~d0tZQ2Hs!SltFj8j5y@u%%2oUt{o}EDyJg~qf=>OG@A1Vn@6>;g zptme)t7cpbw#QGSx-&cW7N_7<6s34Mm)L{cEJRPs_fnv!#Us zG#r=I^%In*;(Y$7{KyY%nBtx7+)AW?DB~-ogf$|?U#1-~GaYsx2KX5c9DgARV+%uv>uJ9<} zWXfn=k$#!X%)w>+g_R|wmR`qc*d+tB<)_Fc zZ|iTZ|8>^ipTk4$H*Jz$m+i`NJ^i2bd9Kd|ezy|Ww1dOU$N$|0{`~mjc?Qp%*MKVz zdf=6|(<%P{+(z)=<}ua2VUmXs-TcyWAtO?j)|0P)wTJ)9)TLZ-sUQaDoPe{3%ZSIn z1j!~`+qVys7XbbP82w3iwV~QX?eXmSmk&;f-*{&YTqgb!$Ny!)Ct$z2-u!>(`Cr!m z&&P9S-hcm*FE5*QnPQ;ho;<)yiSv1uimX*z>(O zGk}kL4bDaTrX?~So~JX3ywKe^UTL#~Ce>nkFTJIPrwjNoO>W5%#c1hifl50rXJ|~5 zZ_(alyvr$BQGd=;Tzh;#d#8Mfwe|UTdQXqC>*0ZtA5yb4OsomlbPm68aUpgrA>Y4T zj0T#_238kWY%I>iu-Mc~)6}*`zn=Lnm+9#9%m&oQmAHJ@rC>f@>(@eH8yeuY;JU>g zc}pi^(2mza`|Zrrku&)X8^k3&{UiM5^wn)3-r&ZGg(K zwJ@h2M|GMmZ9*zx1WWl?DQhyz#k2ZE^-Q1OOvsm`2>&U~p1n(nmSVfOh4Y_?^dc^G z*Xyf+**pF+vh;uxQJXkXxpp`HDXTg0XC-Sf8dC6lt5c6noZL7}QYpHucE->aT)FyK z53?BCI^Krdeu*8-Mia3Lf0>Cg9=aT48}9wW8Erc<-P2wGStUGvVpz~zUArLeqDL;l z4&Ac6GqsQNbW_z{H1liDX}gtm-Y$tl)Fr|mje;N|?x$7RE1f--^PDH0&3@@weBVrT zy|U*@B=c=j{_AplDK}oU=Qt@a7&bPM(BbrpthwqzmV zOGck>WGX6qvT-#m$GbA(OLCuEyqfodtl7oFUBF2eXxLc0ZZ9Ge0NxABc`$oR%;aE| zw*&W>DG0PotFklO?M|qX$<*4un6nY}kp5nDz0yewv$bBJ+YLuROnqx{Wsw8Hj~?CY zN$rotB`bt&As&ud0fIVD4~J@J@{Y@O9sbnqWc1?WRbcahIzjbwuB2hNc~an(Ddw_e z*xA1Z*XTx%o(7-e43iUOl0xl!wUk<|(pzUfuVyT=4Uz&q9?ZHMX6IgLI<yHjna9@oeZ*4ZF@ zWmeM@+ZXjCbEyvfq(CrIv67h5g-1HbaKirbg3ZN6wi!*?p^E7t^aY#O z8!Llmv+906Mtjxl2PNt<_<(4mxjbqTBv5{4iKSx`r`;4aIY4J*F7r@@jXY4@)ASDD zr1g3d=P|He06GNToZcvQ3DjmEZ$EH)*Z2;4_M`Bt87vAeVVY}dF9Pl#(27nm7TgihAsbZioh z&S{EAju7k9@NYN1YPc7>bHz7!iJZ4*YU~{ev3~)l+NSrsE0$=S$ec*G#p5Td(;Gjz z4lC1to_t(^Bf#A8NFbuh(MCSDdE?q%^s5#a;(hqraxTqFm?+v0KAU zdYtO)_jPj22zZTha@kH0U<mr_z{w3Gr0wskX;a9T9m4myX60 z{U|;?Gc%rlbtqwZkV5_VPW(*LFAs$mCIH4(H~+;s5!R}Dd!lIFBre zrFHW!0+Ib0_h?r`VxBjiK4O^u3@;<6>Re3S+7-nUd0S8$#VWaw>!SeyG{K!FHdgJD zgHATC<>rbWy>&KUmL&|ZTBZN$xL3!i6Z=53pv-*G3*B#RSe4@p5%g1=9bw8)YkqLL zFDrX^1~EYXH&dP=OVMy_xbH}e3y8QG(x%#$=tUb11PsyNo3?6xmEXc$|G*+op9sY3x(C&TJNVp zUqMcRKZrQXaTIETj7M}JXbXWujut}M@9lnfDq`Y*%I{~zaLg4xa&)NpI+Gw!=)akO zG%q$(f_m(eh;W|MKvjmly8|u8Ip0g74qTBbn;0bYS=f9rSkb~#+>icEY@(Ha%Ir#O zGTV%$MbJ;$HsZF&cc$WA9>9@~2xM464^Gdo(bPBvWG>d_72K@RvAhS2pI9H~j%L<2 z>wO+xtFXaounYh*HejYM(n!u*dBBJuce5riAe+2pudCEkpc=kUY* ztqt%A@N`;g>vH@ag(CCUUpP$wkNP%H!2)Js7m^L#h3FljWp!38R7_( zP1G~}hFg2mL4-Tt-p-bU9o8DWZ>5vgQsN-u;mi+|R@Xl3WD_h@_*Pu%Q~J&mKm48Yc!1r2dEwL=*6EcZ}?NHl(s$<@76-7^b6aj@g zuXY-fdZr|pKN3ajr#I*gl(L?9znqp-ql9R8%_2o8U9w)w8U?$gjQKqJlqKn!-S7Cq zpFMUbuu35yCk(`kSv!eJT3<3dPv+TaY3v&_m9wh>&}v0;R@f{$pE(i`Z#f6`Etzp- zy~xEYiHmdqlsCINWLIUkloZgX*D^wL;CqeMwG6>c=cgsq=xXWG#4DP=D->{w|&# z)-PrD61%VqQ7sM{QbU`FW+?7-&J^&Qz2-^kF3|l%?r`HMnaDC@&C)mk)1e$oFvRxd^_>Ru8|t)^*xfJG44$F=-<4N7!Cbu#!JPi~+XU4Z_ZuUNX zE+p}#p79g!mvWG6kgK0(@x7~RK5Ok3Crv6^9HJCLM6{e!ZxZ^t;KV4Gx+EfMVg>{J z+SRS{ZSHa(kg#_X?_2ohqJ-DpUC8&zs6Vr)ZV=|LND!{_ygwY1IePzETr1ALZ+u`+ zmVc39G*k~Ws?U8vL8jrtN)!^bH<{UTj$3($w5oQ!=shSrO);ZW2iM-n8cO6;KgM<8 z_{$r5hwgIoa+0-nvnU@iw}6}sKCOvExJ4xrrx2xHA$p^>xj5S*N!^IEM}Sj-0)(*Q)&j=ZBK zmXLFoff3Qz9RG98mU8{^4&0R@*55kfWG9k>22iXq|MAfaS613KMa>SQS&$+}UJ=m~ zGkgA`P=nYq7rY{lH`EP^_`=kp9ck-XUvZnPr-mvtdMY%2ERo@3P_553)Wod)>?}CN zm|h&M|G%}z%qT7_gYqh__3jmJ{cxXpXpZ>q+6d#3ZrDaUt?+K3oJFhhU02)d4y{Ie z$oh#&CVb8ZW zdOPtM6Ke%-8iYienqd4F8QPo=gWYN|gSj>2a5%Z(35#i-7aC)_ec@^2TsZMSU%BCN+rHDJESlUHNptlBuYt_CDPEFOUe3!QIP zX|568SA4Hvz=4_ZO^m-qkK4{&`W5n!EGAW?oTKN@4gtNKgz&-D!-PnMtg#R`Vi%2J z!m3qq0L#=VZtu~_**Mu*J(ovs=ex3c&faJ1t_QfPcD`EQZSot~(lL*4(q7#jxf5h4 za;|>k!xkKo;yN;hPkZyzI3rHKPr9znGPjt#>tXI9=0-2^ud)57R0K|-T)S*;?yao1 z=z_AJBPZaBh=s2~(Giw+V%ansQ8CMIA>jGbVfzK{nwPwD9BxdLnxT=JqJ-T423p@c~y7B43x_pNL8I_pNVq(>RTwa@{#;F{XU^+_n3%K`%CmF zQOICRHji7-r1x|Gb0bC77ZS~(rzY6?%-5#Hk6z#pWltw(-V?;_(|JQOyqr&niML#2 zasKgoYsl!OUA>-4)aB^I^!8;puPou0#kr?<5(*@N>A|ZlSUOB57G8QM#;_|jM|=S* z;Tw_m@`S9m_KEqRlMD4`H3#6`hph?y?4D=tdXFMP{X4ELrV6z3&h$gId`g=`#WtwOy|wH(yHN;7!z`zOxs{>y6)ynj+mo=3;G^W zT8&oNJ3FlW2-jm_s^2`XKTj)bY$W@))(@YB;+$@=gVDwWN^D*V?p*x?A$Tf}ygXN> zia8WyDsP0*P6x>|ee=DZd*NScVhTrT7IW{P#BLN>&8=Z_Th|vxe#fp$0&x|~ZM)p; zZp_tB{Whe1`vsgRAF}cOVxdQhxTh$CTms~@+gbLl5crzsur;w zAmBkGmKB9g9Y?Ad*53wg!O`c5?HE%DmTmtI6!|rX*wyC9widRNAsw{l)DO1_f=7E{ z)&y8hyBPLi3oekaGhD745Ht#cI5>AIsDXRKt*TS~F$vhquCo1Tkl8bggF1g;Xb3SF zyqD48%WvY0twk+iw-^_4#pTEG1$Z5 zrD(dz`z^LM9Huew% ztBh9Jsoav;&JgUB=F0p_HL~sjJ#b z2jIqUH1sQ&VC-I3uo;Sy!nDua(;IDgvWQg)&bd5*w{z2SnO-Jy3P*4GA+fs~?Czxno?lUQV!O*V2=Hla7YckLr-G6}1Vk!sVebL^Rka+v196?ro?PD4 zf@WPrw5(U2_XbP74R%&(QJ-tQ>y}T%2gwFwguWY)A)PO>5fGs4xzL{i z1z*$_JYZl5@Xn=c-JQg+p|O48!Af$KH*=$jGNA{#p%IL4FqGY0-mq#xIIqQ--4?xA zX4#!k_oz!w)NkSue@J#N_z-ZyGSNy;TcZL(NkR!olPDivo9NNc>rG5%d`NIOHTlUh zEym|yRIM2{)YtI(V(0Qup)_(87tS$4yw~!jhCBeO5AXj((Wt|*CB&WuZp)f)7&c(G zTm-^5g=yzEwiClm3(-_bP^L0zHFB_x3@=2fE8nZk)NUvD>%6ap(9*qtbhXfnOhm5L zjqt4vI3~sDUViSCT(IwG4lk>H{-BoT!EjJJr~qd@*jyy_^F8vN9*HzOOw6uPvsII+|)vpf(Xu7Zc}Td4*sa$Ag+J zP>=SU-#)EN=A za?u5Ktxk$cM8QnyPw~;594nNR@gsh%ALRX_{PEIU24 zgLTRKqa=<%)_-zF`ej;TnVY;927@6gOL9Tg;%~8(xD#vYqP)#oV8fG%OsCGM{-3a} zguqWh-Y8$P#o*}6bd22Y*TNW?gvP)i6L$ZEyEl?-knM0-kRPI!i}GOKJNF`xZOJA5 z7J#1DO|V?&`_!EcEsndsYa(R!3yt?gkwG3xUdCK%4r^sz*v=syH4qE9eO0Ryb!tO# zlPP}UnZM<#?mP1cegCiiuTBPzg6y?EZF1y!xMXR;5Dr#1<+VDWO#wHH;h1i(zQjPB zfRRw|Ix@&CYg>2k>$*Dl7zl!frUyX}P}0u1@Qa+V#O$#%7`A2U8oY6K;RM4a_h?!8 zoYquXO?jr@a$N@LZl0!ZF)f|U+OUB1rD^(aR<1|M{4jc&STG0t<3b78K>NB{vq(_A(=g2v=WFL5qP1f-) zJQ}E6VeNmfwLrd{T{xkBRcL-cO!~`B(`XX)&N8;H$9m}WZ(R3YGbIT6@?ByqA@cA1 z<#z?{1MGcQX+U)(@cM7J-!K=TcTboFbQ~KepT^M!5g&uvyiA8J16P_)iQRr;Rhx8k zJGWL3Th<(SWi(3ZTJA2)UTgkFzEy`R>ty?h+E!>)ue{nB%08Qb&idfJ`sxei=W@hO z+u?@>!xs`-?ob6h>5-VAOW`PIad#tmoR5amgJ6e*a z;1*mw3#i*K{N?)E)(yJArt^tNnxn3o4oyn_HkO|N*5Rk4$wAZhXdbtf%Rv(*vsb!Y zK&FG@L1o&=y^_1ommtBU#mL&TKARHEpdshV859e>eaFv*PCIM$`U<0u(L0%)w*Cm+r=rcZYb@SXT6ktV!w~(ay{JPUQ2d;Ma^jI5@ z4@O@LMMsQGwc4WaYDGTm;Z!+S<)n9r+ETqx`HdfXj#a?!I2*&e{Z^gA62oAe1{LDf zSWqtMlJ;wrfmI$ry<~l+&qs?wbL}gjjO%0>?FC5Y-8LDMZ3cWfhbaQBOxx_kHBV{E z(Fo-tJvh%f@bxc%mm=6>&F+dWcp-;rvk`FkX&0tD@3_jLXZC(Sr~~GW952i9M&w?h zyo0p3f+KYHbRdt&@6mXjrPPojUBPW0A)h^?sO}iO4%4Y7RY5>Qk!)quHLEq>ht7(; zo5Y__-4tN6hxR@@>+w=(f8Zx$J{e4;;p^Gpu$Ji2>8%$XP8|Kod_)B5r}EnSUi%e6 zc!3XgGC04{W$BvO{62qE!X#sx!t;hp$X6G|E&9H|C2DCoAy%vK7MqHGji1uQ)StlQ zvAG=ql0^GVPXrEr=Ox_?wagwOmZ+FCt$k@DC3e5p@riDRq-^B5J_b`%$@-yi>@0~0 zB6mJRCK*_5(&2pGi7EcR82qdHYhdE~bI+f#Q;1+Ze$mgZP$bQjVjq7m28A^Ks@TkI zVRphTZjXhg26jv=%DDz_BDnUVvZnIM7r~8C5PHi7xP7L~knSks{V-YV87q3&##0*5m1^xN8 zB#^tLvZHdZumcHpfw>!h0}ZfuH)s0)s1xLqIHQ{$PT(rY37pTegqpt}&)$87Fvz{t zsTyhcCRNgZQsJAazn(1M`giXA&|4oQz8sY^TiKP3GB5L125+7VJiA+6=Jp+0tbgHg zt@x;JF3#{>2B-EK9Yxvb?FlMzl+3gPGn*rTn6=jrNQw4U zK4^;>_sdUz^xUIb zrqF%x9cHaJfiX*!d+dq)-i~&RMO^1DTw3uEf0whexoAL}2tY5tkEL9F#ka6@e5%cH zyJ@M^M3(rTiI0=he_1z%X21_uManopJ{wwcS@PwRHVID(>JzO9((87Q`w`vBak0w3Rh=M6y1k_PO&!N9=2=BGWZakz4K z;5@$uuhneld3(WOeI0ZmI9hMn_u zbnN;q^v`*!BZf}RbqH^ecRbsit!_@JVc=aUN74l?w(y~Di~Jz^5rZaG`B_;hzNzgN ze+b;iqlES$cYdEkK7RV47!NpdC+jl_jSbt)`Fw8r=IrU*GJ$&5`;OY_WNMNfkq(jB z#>hOb$CS3SfvpA$b zN?|e0Zts5B0wq-FjlEHZ;YP7(+oL% za=nqyrhe`SZ1U93;KX<8#{Alh&31-fc-03aG@q`PVn9ky$IZUgVYp8s-=ru59GCJY z18SY?yNn}ggg75Zv5W1E49CuSbWz3XQDQ&nYB+UVFGy*8wy5-$C>tGTs4hKrn7r^E z1V4UynO?b<*ZNS^WOz-S(ENg;O`Oy7q4-{@gnR}+_wY9LO$DkyZ*zCoD#n}0$?_*T z{EK%76l9}Q=?=+_Gkq~ zHuC@u#pN?arVT*8LU(WT>AR#zoko_L`*Cq3!2i0)wMU0VdNB4nMwb3rT-Q(a8`Ocz zqVHL|@M%NBBI%7k!7rydjcRlGH9kJO1=*?0JsMelQkl>7Dp@ODo2vfEYPK4`h5wFq z^5lLfW$$BRIoS!55l)|}!(z@O1WO?6X>sGSOw#32I>o34LEUX`uBVCr4Gk7oerpNK z=R>b?=xdtrgf!p0RA97 zvCCH{(LgOr69MK5OO%aYWL>Jx2h&$)Y!tiq^!P@}04#2v>Yoh)m5mhcOaj=(yJ}H2 zW4SP0rGu0CMY#)yB&L;i8#BGtDHKO%y@oVn<9{V0A?S<9i6wOJP4V0@^h2>Rwu*wA z=>RW>85xFNceHLF@JGu)o+`2}x~>mD8fIBc1h(uNXx?w7*!aB4smAbJV{^=y2rTiE(y>Kt$h zGeM@%`-FezV*B5Hc}O&=FnHt$3fQT7U--arA6B@*FUSDv3x=dHiUuV`mge%B7ZSb9 zyDQ}Kk&X#Ht&?-k5;7S5qG~v7*dk(Nba3AsnRC0zRoIHcpyT=8wLIJqJ|pCdNk;mF zGEK>RrW7Rb{WX5f;6-CJ_cuu-uRNGBe`;bP_RC?ojqF`MNh52Nm z9Udh!L-qfZuqOgizYFfxxm8Q#+;CG(Fv%_b%BAN5M83P2cvMcLahpd+)fZ^sIEiy{ zg_tk#)Q8lZCnr${e7IS9nvMS~a2ULRMY)ScU17=PUm7W`nC!Ll~R{qNR9;zBZZ?#)tZb4xj+ zTx|U%K7QWtFsPFy1=zl$9+@}Mf76wX_Dy2B&We$urD8Av!Ef+6&(4bj;(Ry1fWElL zG3hT7*`~v#T%HE7g|)CiltUvicZ+iNk4JFc?Jn+quS!Z3rA-d%W}J%}vLvN`-PuPLZC@n?BfXRG3VMXJ zV7yU$M`+{O+K>nxknPj;6V=xE=JNR8b!v=qA_wR`AL@LSzjql0sv9In?`<`9G$mt! zwm%`lS~@q}i1EQ}Lz51n!E*v({coNv#c)Jzh{SP545!_tI^)+R8g_bqEMu5RHMU?K z;J4lRpb0B}wUbB9Gs>rSSM)28Oy;YmpGC z_Zm^0^`To&+Mbgh;LB<1N4q66ZfaY#2-75;#W-!EXcoGq%G6te1Vs1PJ^_w?2#OaC z>(n^&865y*+~3_vid5}%T}c4giZT7%qmm!opKd@o?YN2!l=hQ}jWnaQHWKEinD6a5 z9c~-$W*dB#plwMv?@CZEw%EU*Yo>)?kmDvj2V<$UMTMsBfmZXNA1~dV3UddVileU~DWk7RL%0F=^i~?MRT7bUJ8z@_WxZ@M5~m z?TOF4KDc2OzWDuP8OOZO{>yRIoE?_ZM@^PyZP!n>fI50LlZ~>}UG@`4uyyF^#;XU{ zo&xDPnO&g5728m z?({hCFi%9gof2Q_*>&Ytwm{dV5)+fJ9_n$OK1qdTRZav`zRE>U_%B_sgojSP0Xm)B sO^0yO4yG`^Q}arh8+nd-!$^rGUK&j!ES#43>x8PRq@`H%^lixh0vp@8W&i*H literal 0 HcmV?d00001