From 1acd47c6c50a77a915f873c4d60e38a4a3f58e31 Mon Sep 17 00:00:00 2001 From: zzchao-1999 <149659707+zzchao-1999@users.noreply.github.com> Date: Thu, 5 Sep 2024 07:57:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20[JIRA:HCPSDKFIORIUIKIT-1?= =?UTF-8?q?934]SwiftUI=20TimelinePreviewView=20(#780)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-1934]SwiftUI TimelinePreviewView * feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-1934]Address bot checks * feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-1934]Address the comments * feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-1934]Address new comments * feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-1934]Added a new base component * feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-1934]Updated model to protocol * feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-1934]Added Docs * feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-1934]Corrected Docs issue --------- Co-authored-by: Bill Zhou --- .../Examples.xcodeproj/project.pbxproj | 29 ++- .../CustomTimelinePreviewExample.swift | 57 ++++++ .../Timeline/SimpleTimelineExample.swift | 2 +- .../SimpleTimelinePreviewExample.swift | 66 +++++++ .../Timeline/TimelineExample.swift | 13 +- .../Timeline/TimelinePreviewExample.swift | 77 ++++++++ .../TimelinePreviewItemProtocol.swift | 32 ++++ .../BaseComponentProtocols.swift | 7 + .../CompositeComponentProtocols.swift | 55 ++++++ .../OptionalTitleStyle.fiori.swift | 19 ++ .../TimelineNowIndicatorStyle.fiori.swift | 6 +- .../TimelinePreviewItemStyle.fiori.swift | 102 ++++++++++ .../TimelinePreviewStyle.fiori.swift | 177 ++++++++++++++++++ .../OptionalTitle.generated.swift | 63 +++++++ .../OptionalTitleStyle.generated.swift | 28 +++ .../TimelinePreview.generated.swift | 114 +++++++++++ .../TimelinePreviewStyle.generated.swift | 39 ++++ .../TimelinePreviewItem.generated.swift | 93 +++++++++ .../TimelinePreviewItemStyle.generated.swift | 46 +++++ ...entStyleProtocol+Extension.generated.swift | 168 +++++++++++++++++ .../EnvironmentVariables.generated.swift | 63 +++++++ .../ModifiedStyle.generated.swift | 84 +++++++++ .../ResolvedStyle.generated.swift | 48 +++++ .../View+Extension_.generated.swift | 51 +++++ ...iewEmptyChecking+Extension.generated.swift | 22 +++ .../en.lproj/FioriSwiftUICore.strings | 9 + 26 files changed, 1454 insertions(+), 16 deletions(-) create mode 100644 Apps/Examples/Examples/FioriSwiftUICore/Timeline/CustomTimelinePreviewExample.swift create mode 100644 Apps/Examples/Examples/FioriSwiftUICore/Timeline/SimpleTimelinePreviewExample.swift create mode 100644 Apps/Examples/Examples/FioriSwiftUICore/Timeline/TimelinePreviewExample.swift create mode 100644 Sources/FioriSwiftUICore/Components/TimelinePreviewItemProtocol.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/OptionalTitleStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/TimelinePreviewItemStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/TimelinePreviewStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/OptionalTitle/OptionalTitle.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/OptionalTitle/OptionalTitleStyle.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreview/TimelinePreview.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreview/TimelinePreviewStyle.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreviewItem/TimelinePreviewItem.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreviewItem/TimelinePreviewItemStyle.generated.swift diff --git a/Apps/Examples/Examples.xcodeproj/project.pbxproj b/Apps/Examples/Examples.xcodeproj/project.pbxproj index 1ca14cab4..8b109406c 100644 --- a/Apps/Examples/Examples.xcodeproj/project.pbxproj +++ b/Apps/Examples/Examples.xcodeproj/project.pbxproj @@ -31,10 +31,6 @@ 691DE21925F2A30B00094D4A /* KPIViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691DE21825F2A30B00094D4A /* KPIViewExample.swift */; }; 692F338B26556A6A009B98DA /* SideBarExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 692F338A26556A6A009B98DA /* SideBarExample.swift */; }; 69B2B5D9268A333C009AC6B3 /* KPIProgressViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B2B5D8268A333C009AC6B3 /* KPIProgressViewExample.swift */; }; - 8732C2C52C350957002110E9 /* TimelineExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732C2C42C350957002110E9 /* TimelineExample.swift */; }; - 8732C2C72C3524B6002110E9 /* TimelineItemsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732C2C62C3524B6002110E9 /* TimelineItemsExample.swift */; }; - 8732C2C92C3524C9002110E9 /* SimpleTimelineExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732C2C82C3524C9002110E9 /* SimpleTimelineExample.swift */; }; - 8732C2CB2C3524D9002110E9 /* CustomTimelineExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732C2CA2C3524D9002110E9 /* CustomTimelineExample.swift */; }; 6D6E86252C50D42000EDB6F4 /* FioriButtonInListExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6E86242C50D42000EDB6F4 /* FioriButtonInListExample.swift */; }; 6D6E86292C50E5F900EDB6F4 /* FioriButtonInListMultipleLineExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6E86282C50E5F900EDB6F4 /* FioriButtonInListMultipleLineExample.swift */; }; 6D6E86672C50FDBE00EDB6F4 /* FioriButtonInCollectionExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6E86662C50FDBE00EDB6F4 /* FioriButtonInCollectionExample.swift */; }; @@ -49,7 +45,14 @@ 6DEC32002C48FB010084DD20 /* LoadingButtonSingleStatusExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEC31FF2C48FB010084DD20 /* LoadingButtonSingleStatusExample.swift */; }; 6DEC32022C4A4DC70084DD20 /* CardFullWidthSingleButtonExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEC32012C4A4DC70084DD20 /* CardFullWidthSingleButtonExample.swift */; }; 6DEC32042C4E49C70084DD20 /* CardFixedWidthButtonsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEC32032C4E49C70084DD20 /* CardFixedWidthButtonsExample.swift */; }; + 8732C2C52C350957002110E9 /* TimelineExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732C2C42C350957002110E9 /* TimelineExample.swift */; }; + 8732C2C72C3524B6002110E9 /* TimelineItemsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732C2C62C3524B6002110E9 /* TimelineItemsExample.swift */; }; + 8732C2C92C3524C9002110E9 /* SimpleTimelineExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732C2C82C3524C9002110E9 /* SimpleTimelineExample.swift */; }; + 8732C2CB2C3524D9002110E9 /* CustomTimelineExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732C2CA2C3524D9002110E9 /* CustomTimelineExample.swift */; }; 878219C42BEE128E002FDFBC /* StepperViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878219C32BEE128E002FDFBC /* StepperViewExample.swift */; }; + 87F492312C73AD99002B8703 /* CustomTimelinePreviewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F492302C73AD99002B8703 /* CustomTimelinePreviewExample.swift */; }; + 87F492332C73ADA1002B8703 /* SimpleTimelinePreviewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F492322C73ADA1002B8703 /* SimpleTimelinePreviewExample.swift */; }; + 87F492352C73ADAA002B8703 /* TimelinePreviewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F492342C73ADAA002B8703 /* TimelinePreviewExample.swift */; }; 8A55795724C1286E0098003A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A55795624C1286E0098003A /* AppDelegate.swift */; }; 8A55795924C1286E0098003A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A55795824C1286E0098003A /* SceneDelegate.swift */; }; 8A55795B24C1286E0098003A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A55795A24C1286E0098003A /* ContentView.swift */; }; @@ -234,10 +237,6 @@ 691DE21825F2A30B00094D4A /* KPIViewExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KPIViewExample.swift; sourceTree = ""; }; 692F338A26556A6A009B98DA /* SideBarExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideBarExample.swift; sourceTree = ""; }; 69B2B5D8268A333C009AC6B3 /* KPIProgressViewExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KPIProgressViewExample.swift; sourceTree = ""; }; - 8732C2C42C350957002110E9 /* TimelineExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineExample.swift; sourceTree = ""; }; - 8732C2C62C3524B6002110E9 /* TimelineItemsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemsExample.swift; sourceTree = ""; }; - 8732C2C82C3524C9002110E9 /* SimpleTimelineExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleTimelineExample.swift; sourceTree = ""; }; - 8732C2CA2C3524D9002110E9 /* CustomTimelineExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimelineExample.swift; sourceTree = ""; }; 6D6E86242C50D42000EDB6F4 /* FioriButtonInListExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FioriButtonInListExample.swift; sourceTree = ""; }; 6D6E86282C50E5F900EDB6F4 /* FioriButtonInListMultipleLineExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FioriButtonInListMultipleLineExample.swift; sourceTree = ""; }; 6D6E86662C50FDBE00EDB6F4 /* FioriButtonInCollectionExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FioriButtonInCollectionExample.swift; sourceTree = ""; }; @@ -252,7 +251,14 @@ 6DEC31FF2C48FB010084DD20 /* LoadingButtonSingleStatusExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonSingleStatusExample.swift; sourceTree = ""; }; 6DEC32012C4A4DC70084DD20 /* CardFullWidthSingleButtonExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFullWidthSingleButtonExample.swift; sourceTree = ""; }; 6DEC32032C4E49C70084DD20 /* CardFixedWidthButtonsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFixedWidthButtonsExample.swift; sourceTree = ""; }; + 8732C2C42C350957002110E9 /* TimelineExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineExample.swift; sourceTree = ""; }; + 8732C2C62C3524B6002110E9 /* TimelineItemsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemsExample.swift; sourceTree = ""; }; + 8732C2C82C3524C9002110E9 /* SimpleTimelineExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleTimelineExample.swift; sourceTree = ""; }; + 8732C2CA2C3524D9002110E9 /* CustomTimelineExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimelineExample.swift; sourceTree = ""; }; 878219C32BEE128E002FDFBC /* StepperViewExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepperViewExample.swift; sourceTree = ""; }; + 87F492302C73AD99002B8703 /* CustomTimelinePreviewExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomTimelinePreviewExample.swift; sourceTree = ""; }; + 87F492322C73ADA1002B8703 /* SimpleTimelinePreviewExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleTimelinePreviewExample.swift; sourceTree = ""; }; + 87F492342C73ADAA002B8703 /* TimelinePreviewExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelinePreviewExample.swift; sourceTree = ""; }; 8A1E99AD24D59C8000ED8A39 /* cloud-sdk-ios-fiori */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "cloud-sdk-ios-fiori"; path = ../..; sourceTree = ""; }; 8A55795324C1286E0098003A /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; 8A55795624C1286E0098003A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -518,6 +524,9 @@ 8732C2C32C35092D002110E9 /* Timeline */ = { isa = PBXGroup; children = ( + 87F492342C73ADAA002B8703 /* TimelinePreviewExample.swift */, + 87F492322C73ADA1002B8703 /* SimpleTimelinePreviewExample.swift */, + 87F492302C73AD99002B8703 /* CustomTimelinePreviewExample.swift */, 8732C2C42C350957002110E9 /* TimelineExample.swift */, 8732C2C62C3524B6002110E9 /* TimelineItemsExample.swift */, 8732C2C82C3524C9002110E9 /* SimpleTimelineExample.swift */, @@ -1069,7 +1078,6 @@ C18868D12B32535100F865F7 /* SearchFontAndColor.swift in Sources */, 9D0B26092B9BA5C0004278A5 /* KeyValueFormViewExample.swift in Sources */, 8732C2C52C350957002110E9 /* TimelineExample.swift in Sources */, - 9D057DAB2C2F260200F5331C /* RatingControlExample.swift in Sources */, 8A557A1A24C12C820098003A /* ChartsContentView.swift in Sources */, 8A5579CE24C1293C0098003A /* SettingColor.swift in Sources */, 1F55FEF32AC941FF00D7A1BE /* View+Extensions.swift in Sources */, @@ -1078,6 +1086,7 @@ 8AD9DFB225D49967007448EC /* StylingModifierExample.swift in Sources */, 9D0B260A2B9BA5C0004278A5 /* NoteFormViewExample.swift in Sources */, 9DEC27B72C3F3DE70070B571 /* OtherViewExamples.swift in Sources */, + 87F492332C73ADA1002B8703 /* SimpleTimelinePreviewExample.swift in Sources */, 8A5579D124C1293C0098003A /* Settings.swift in Sources */, 8AD9DFB025D49967007448EC /* ContactItemActionItemsExample.swift in Sources */, 8A6D64B125AE658300D2D76C /* ExpHeaderView.swift in Sources */, @@ -1139,8 +1148,10 @@ 692F338B26556A6A009B98DA /* SideBarExample.swift in Sources */, 8A5579D324C1293C0098003A /* SettingsPoint.swift in Sources */, B80DA9BA260BBF8600C0B2E9 /* SingleActionProfiles.swift in Sources */, + 87F492352C73ADAA002B8703 /* TimelinePreviewExample.swift in Sources */, B18D593C2B0C52C700ABB1AD /* TabViewExample.swift in Sources */, 8A5579D424C1293C0098003A /* SettingsBaseline.swift in Sources */, + 87F492312C73AD99002B8703 /* CustomTimelinePreviewExample.swift in Sources */, 1F90888C261A59820015A84D /* FioriButtonExample.swift in Sources */, 6D6E86252C50D42000EDB6F4 /* FioriButtonInListExample.swift in Sources */, B8D4377125F983730024EE7D /* ObjectCell_Rules_Alignment.swift in Sources */, diff --git a/Apps/Examples/Examples/FioriSwiftUICore/Timeline/CustomTimelinePreviewExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/Timeline/CustomTimelinePreviewExample.swift new file mode 100644 index 000000000..f3dbbfac1 --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/Timeline/CustomTimelinePreviewExample.swift @@ -0,0 +1,57 @@ +import FioriSwiftUICore +import SwiftUI + +struct CustomTimelinePreviewExample: View { + @State private var items0: [TimelinePreviewItemModelImplementation] = [ + TimelinePreviewItemModelImplementation(title: "open", icon: Image(systemName: "a.square"), timelineNode: TimelineNodeType.inProgress, due: ISO8601DateFormatter().date(from: "2024-09-21T12:00:00Z")!, dateFormat: "dd/MM/yyyy"), + TimelinePreviewItemModelImplementation(title: "Before start", icon: Image(systemName: "a.square"), timelineNode: TimelineNodeType.start, due: ISO8601DateFormatter().date(from: "2024-07-23T12:00:00Z")!, dateFormat: "dd/MM/yyyy"), + TimelinePreviewItemModelImplementation(title: "start", timelineNode: TimelineNodeType.open, due: ISO8601DateFormatter().date(from: "2024-08-19T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "open", icon: Image(systemName: "o.square"), timelineNode: TimelineNodeType.inProgress, due: ISO8601DateFormatter().date(from: "2024-08-17T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "open", timelineNode: TimelineNodeType.open, due: ISO8601DateFormatter().date(from: "2024-09-27T12:00:00Z")!) + ] + + var body: some View { + VStack { + TimelinePreview( + optionalTitle: { Text("Timeline") }, + action: { + FioriButton( + label: { _ in + Label("See All (\(self.items0.count))", systemImage: "arrowtriangle.right") + .labelStyle(MyLabelStyle()) + } + ) + }, + items: .constant(self.items0.map { $0 as any TimelinePreviewItemModel }) + ) + .timelinePreviewItemStyle(content: { config in + TimelinePreviewItem(config) + .titleStyle(content: { titleConfig in + titleConfig.title.foregroundColor(.yellow) + }) + .timestampStyle(content: { timestampConfig in + timestampConfig.timestamp.foregroundColor(.red) + }) + }) + .optionalTitleStyle(content: { config in + config.optionalTitle.foregroundColor(.purple) + }) + Spacer() + } + } +} + +struct MyLabelStyle: LabelStyle { + func makeBody(configuration: Configuration) -> some View { + HStack(alignment: .center) { + configuration.title + .foregroundColor(.purple) + configuration.icon + .foregroundColor(.green) + } + } +} + +#Preview { + CustomTimelinePreviewExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/Timeline/SimpleTimelineExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/Timeline/SimpleTimelineExample.swift index f60f9730f..94a7fb5f5 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/Timeline/SimpleTimelineExample.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/Timeline/SimpleTimelineExample.swift @@ -35,7 +35,7 @@ struct SimpleTimelineExample: View { } } .listStyle(.plain) - .environment(\.defaultMinListRowHeight, 7) + .environment(\.defaultMinListRowHeight, 5) } } diff --git a/Apps/Examples/Examples/FioriSwiftUICore/Timeline/SimpleTimelinePreviewExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/Timeline/SimpleTimelinePreviewExample.swift new file mode 100644 index 000000000..17663f1c5 --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/Timeline/SimpleTimelinePreviewExample.swift @@ -0,0 +1,66 @@ +import FioriSwiftUICore +import SwiftUI + +struct SimpleTimelinePreviewExample: View { + @State private var items0: [TimelinePreviewItemModelImplementation] = [ + TimelinePreviewItemModelImplementation(title: "POC", timelineNode: TimelineNodeType.beforeStart, due: ISO8601DateFormatter().date(from: "2024-06-03T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Project Phase 3", timelineNode: TimelineNodeType.open, due: ISO8601DateFormatter().date(from: "2024-06-25T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Project Phase 2", timelineNode: TimelineNodeType.inProgress, due: ISO8601DateFormatter().date(from: "2024-06-20T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Project Phase 1", timelineNode: TimelineNodeType.complete, due: ISO8601DateFormatter().date(from: "2024-06-12T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Project Start", timelineNode: TimelineNodeType.start, due: ISO8601DateFormatter().date(from: "2024-06-05T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Project End", timelineNode: TimelineNodeType.end, due: ISO8601DateFormatter().date(from: "2024-07-25T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Accept Test", timelineNode: TimelineNodeType.beforeEnd, due: ISO8601DateFormatter().date(from: "2024-07-15T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Project Phase 4", timelineNode: TimelineNodeType.open, due: ISO8601DateFormatter().date(from: "2024-07-05T12:00:00Z")!) + ] + + var body: some View { + VStack { + NavigationLink(destination: TimelineView()) { + TimelinePreview(optionalTitle: { Text("Timeline Preview") }, items: .constant(self.items0.map { $0 as any TimelinePreviewItemModel })) + } + Spacer() + } + } +} + +struct TimelineView: View { + var body: some View { + List { + Section(header: Text("Simple Timeline Example")) { + TimelineMarker(timestamp: "06/03/24", secondaryTimestamp: .icon(Image(systemName: "sun.max")), timelineNode: .beforeStart, title: "POC", isPast: true, showUpperVerticalLine: false) + .modifier(CustomListRowModifier()) + .secondaryTimestampStyle(content: { config in + config.secondaryTimestamp.foregroundColor(.yellow) + }) + TimelineMarker(timestamp: "06/05/24", secondaryTimestamp: .text("Sunny"), timelineNode: .start, title: "Project Start", isPast: true) + .modifier(CustomListRowModifier()) + Timeline(timestamp: "06/12/24", secondaryTimestamp: .icon(Image(systemName: "sun.max")), timelineNode: .complete, title: "Project Phase 1", attribute: "xx features implementation done", status: .text("Done"), isPast: true) + .modifier(CustomListRowModifier()) + .secondaryTimestampStyle(content: { config in + config.secondaryTimestamp.foregroundColor(.yellow) + }) + Timeline(timestamp: "06/20/24", timelineNode: .inProgress, title: "Project Phase 2", subtitle: "Integration test", status: .text("ongoing"), isPresent: true) + .modifier(CustomListRowModifier()) + TimelineNowIndicator() + .modifier(CustomListRowModifier()) + Timeline(timestamp: "06/25/24", timelineNode: .open, title: "Project Phase 3", attribute: "feature list: xx, xx, xx", status: .text("pending")) + .modifier(CustomListRowModifier()) + Timeline(timestamp: "06/28/24", timelineNode: .open, title: "Project Phase 4", attribute: "feature list: xx, xx, xx", status: .text("pending")) + .modifier(CustomListRowModifier()) + TimelineMarker(timestamp: "07/05/24", timelineNode: .beforeEnd, title: "Accept Test") + .modifier(CustomListRowModifier()) + TimelineMarker(timestamp: "07/09/24", secondaryTimestamp: .icon(Image(systemName: "sun.max")), timelineNode: .end, title: "Project End", showLowerVerticalLine: false) + .modifier(CustomListRowModifier()) + .secondaryTimestampStyle(content: { config in + config.secondaryTimestamp.foregroundColor(.yellow) + }) + } + } + .listStyle(.plain) + .environment(\.defaultMinListRowHeight, 7) + } +} + +#Preview { + SimpleTimelinePreviewExample() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/Timeline/TimelineExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/Timeline/TimelineExample.swift index 4b1bac39b..b6397b13f 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/Timeline/TimelineExample.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/Timeline/TimelineExample.swift @@ -3,9 +3,16 @@ import SwiftUI struct TimelineExample: View { var body: some View { List { - NavigationLink("TimelineItems", destination: TimelineItemsExample()) - NavigationLink("SimpleTimelineExample", destination: SimpleTimelineExample()) - NavigationLink("CustomTimelineExample", destination: CustomTimelineExample()) + Section(header: Text("TimelinePreview")) { + NavigationLink("Simple TimelinePreview Example", destination: TimelinePreviewExample()) + NavigationLink("Simple TimelinePreview Use Case", destination: SimpleTimelinePreviewExample()) + NavigationLink("Custom TimelinePreview Example", destination: CustomTimelinePreviewExample()) + } + Section(header: Text("Timeline")) { + NavigationLink("Timeline Items", destination: TimelineItemsExample()) + NavigationLink("Simple Timeline Example", destination: SimpleTimelineExample()) + NavigationLink("Custom Timeline Example", destination: CustomTimelineExample()) + } } } } diff --git a/Apps/Examples/Examples/FioriSwiftUICore/Timeline/TimelinePreviewExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/Timeline/TimelinePreviewExample.swift new file mode 100644 index 000000000..234d04f8a --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/Timeline/TimelinePreviewExample.swift @@ -0,0 +1,77 @@ +import FioriSwiftUICore +import SwiftUI + +struct TimelinePreviewItemModelImplementation: TimelinePreviewItemModel { + var id: UUID + var title: String + var icon: Image? + var timelineNode: FioriSwiftUICore.TimelineNodeType + var due: Date + var formatter: DateFormatter? + var isFuture: Bool? + var isCurrent: Bool? + + init(id: UUID = UUID(), title: String, icon: Image? = nil, timelineNode: FioriSwiftUICore.TimelineNodeType, due: Date, dateFormat: String? = nil, isFuture: Bool? = nil, isCurrent: Bool? = nil) { + self.id = id + self.title = title + self.icon = icon + self.timelineNode = timelineNode + self.due = due + self.formatter = DateFormatter() + if let dateFormat { + self.formatter?.dateFormat = dateFormat + } else { + self.formatter?.dateFormat = "MMMM dd yyyy" + } + self.isFuture = isFuture + self.isCurrent = isCurrent + } +} + +struct TimelinePreviewExample: View { + @State private var items0: [TimelinePreviewItemModelImplementation] = [ + TimelinePreviewItemModelImplementation(title: "Open", icon: Image(systemName: "a.square"), timelineNode: TimelineNodeType.inProgress, due: ISO8601DateFormatter().date(from: "2025-09-21T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Before start", timelineNode: TimelineNodeType.start, due: ISO8601DateFormatter().date(from: "2025-07-23T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Start", timelineNode: TimelineNodeType.open, due: ISO8601DateFormatter().date(from: "2025-08-16T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Open", timelineNode: TimelineNodeType.inProgress, due: ISO8601DateFormatter().date(from: "2025-08-15T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Open", timelineNode: TimelineNodeType.open, due: ISO8601DateFormatter().date(from: "2025-09-27T12:00:00Z")!) + ] + @State private var items1: [TimelinePreviewItemModelImplementation] = [ + TimelinePreviewItemModelImplementation(title: "Open", timelineNode: TimelineNodeType.inProgress, due: ISO8601DateFormatter().date(from: "2024-09-21T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "LooooooooooooooooooooooongTitle", timelineNode: TimelineNodeType.start, due: ISO8601DateFormatter().date(from: "2024-07-23T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Start", timelineNode: TimelineNodeType.start, due: ISO8601DateFormatter().date(from: "2024-08-29T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Open", timelineNode: TimelineNodeType.open, due: ISO8601DateFormatter().date(from: "2024-08-27T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Open", timelineNode: TimelineNodeType.open, due: ISO8601DateFormatter().date(from: "2024-09-27T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Open", timelineNode: TimelineNodeType.open, due: ISO8601DateFormatter().date(from: "2024-09-07T12:00:00Z")!) + ] + @State private var items2: [TimelinePreviewItemModelImplementation] = [ + TimelinePreviewItemModelImplementation(title: "Open", timelineNode: TimelineNodeType.inProgress, due: ISO8601DateFormatter().date(from: "2023-09-21T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Start", timelineNode: TimelineNodeType.start, due: ISO8601DateFormatter().date(from: "2023-08-10T12:00:00Z")!, dateFormat: "EEEE, MMMM dd, yyyy h:mm a"), + TimelinePreviewItemModelImplementation(title: "Open", timelineNode: TimelineNodeType.open, due: ISO8601DateFormatter().date(from: "2023-08-17T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "Open", timelineNode: TimelineNodeType.end, due: ISO8601DateFormatter().date(from: "2023-09-27T12:00:00Z")!) + ] + @State private var items3: [TimelinePreviewItemModelImplementation] = [ + TimelinePreviewItemModelImplementation(title: "Inprogress", timelineNode: TimelineNodeType.inProgress, due: ISO8601DateFormatter().date(from: "2024-07-21T12:00:00Z")!), + TimelinePreviewItemModelImplementation(title: "End", timelineNode: TimelineNodeType.end, due: ISO8601DateFormatter().date(from: "2024-08-26T12:00:00Z")!) + ] + + var body: some View { + List { + Text("TimelinePreview: Future") + TimelinePreview(optionalTitle: { Text("Timeline") }, items: .constant(self.items0.map { $0 as any TimelinePreviewItemModel })) + Text("TimelinePreview: Present") + TimelinePreview(optionalTitle: { Text("Timeline") }, items: .constant(self.items1.map { $0 as any TimelinePreviewItemModel })) + Text("TimelinePreview: Past") + TimelinePreview(optionalTitle: { Text("Timeline") }, items: .constant(self.items2.map { $0 as any TimelinePreviewItemModel })) + Text("TimelinePreview: No Header") + TimelinePreview(items: .constant(self.items2.map { $0 as any TimelinePreviewItemModel })) + Text("TimelinePreview: End") + TimelinePreview(optionalTitle: { Text("Timeline") }, items: .constant(self.items3.map { $0 as any TimelinePreviewItemModel })) + } + .listStyle(.plain) + } +} + +#Preview { + TimelinePreviewExample() +} diff --git a/Sources/FioriSwiftUICore/Components/TimelinePreviewItemProtocol.swift b/Sources/FioriSwiftUICore/Components/TimelinePreviewItemProtocol.swift new file mode 100644 index 000000000..bf4e4de99 --- /dev/null +++ b/Sources/FioriSwiftUICore/Components/TimelinePreviewItemProtocol.swift @@ -0,0 +1,32 @@ +import Foundation +import SwiftUI + +/// Protocol for a timelinePreviewItem +public protocol TimelinePreviewItemModel: Identifiable { + var id: UUID { get } + var title: String { get } + var icon: Image? { get } + var timelineNode: TimelineNodeType { get } + var due: Date { get } + var formatter: DateFormatter? { get } + var isFuture: Bool? { get set } + var isCurrent: Bool? { get set } +} + +/// Extension to provide a default date formatter for the `TimelinePreviewItemModel`. +extension TimelinePreviewItemModel { + var formatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "MMMM dd yyyy" + return formatter + } +} + +/// Extension to provide an initializer for `TimelinePreviewItem` from a `TimelinePreviewItemModel`. +public extension TimelinePreviewItem { + /// Initialize a `TimelinePreviewItem` from a `TimelinePreviewItemModel`. + init(model: any TimelinePreviewItemModel) { + // Initialize the `TimelinePreviewItem` with values + self.init(title: AttributedString(model.title), icon: model.icon, timelineNode: model.timelineNode, timestamp: AttributedString(model.formatter.string(from: model.due)), isFuture: model.isFuture ?? false, nodeType: model.timelineNode) + } +} diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift index bb422e118..02bc108d5 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift @@ -320,3 +320,10 @@ protocol _NowIndicatorNodeComponent { protocol _OptionsComponent { var options: [AttributedString] { get } } + +// sourcery: BaseComponent +protocol _OptionalTitleComponent { + // sourcery: @ViewBuilder + // sourcery: defaultValue = "" + var optionalTitle: AttributedString? { get } +} diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift index 5e9e9df23..8a6fa9a2b 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift @@ -424,6 +424,61 @@ protocol _SegmentedControlPickerComponent: _OptionsComponent { var selectedIndex: Int { get } } +/// 'TimelinePreviewItem' is an item specialized for placement in TimelinePreview. +// sourcery: CompositeComponent +protocol _TimelinePreviewItemComponent: _TitleComponent, _IconComponent, _TimelineNodeComponent, _TimestampComponent { + // sourcery: defaultValue = false + /// The node is in future or not. Default is not in future. + var isFuture: Bool { get } + /// Timeline node type + var nodeType: TimelineNodeType { get } +} + +/// `TimelinePreview` is an view for showing a collection of tasks. It comes with a header and a collection view which uses `TimelinePreviewItem` to represent data items within it. +/// +/// ## Usage +/// ```swift +/// Create a struct that conforms to the protocol: TimelinePreviewItemModel, providing implementation for the required properties and methods: +/// struct TimelinePreviewItemModelImplementation: TimelinePreviewItemModel { +/// var id: UUID +/// var title: AttributedString +/// var icon: Image? +/// var timelineNode: FioriSwiftUICore.TimelineNodeType +/// var due: Date +/// var formatter: DateFormatter? +/// var isFuture: Bool? +/// var isCurrent: Bool? +/// +/// init(id: UUID = UUID(), title: AttributedString, icon: Image? = nil, timelineNode: FioriSwiftUICore.TimelineNodeType, due: Date, dateFormat: String? = nil, isFuture: Bool? = nil, isCurrent: Bool? = nil) { +/// self.id = id +/// self.title = title +/// self.icon = icon +/// self.timelineNode = timelineNode +/// self.due = due +/// self.formatter = DateFormatter() +/// if let dateFormat { +/// self.formatter.dateFormat = dateFormat +/// } else { +/// self.formatter.dateFormat = "MMMM dd yyyy" +/// } +/// self.isFuture = isFuture +/// self.isCurrent = isCurrent +/// } +/// } +/// +/// Create a Protocol Instance array with Initial Value +/// @State private var items: [TimelinePreviewItemModelImplementation] = [TimelinePreviewItemModelImplementation(title: "Complete", timelineNode: TimelineNodeType.complete, due: ISO8601DateFormatter().date(from: "2023-07-21T12:00:00Z")!),TimelinePreviewItemModelImplementation(title: "End", timelineNode: TimelineNodeType.end, due: ISO8601DateFormatter().date(from: "2023-08-10T12:00:00Z")!)] +/// +/// Create TimelinePreview with the array +/// TimelinePreview(optionalTitle: { Text("Timeline") }, data: .constant(items.map { $0 as any TimelinePreviewItemModel })) +/// ``` +// sourcery: CompositeComponent +protocol _TimelinePreviewComponent: _OptionalTitleComponent, _ActionComponent { + // sourcery: @Binding + /// The data for all timelinePreviewItems + var items: [any TimelinePreviewItemModel] { get } +} + /// `SwitchView`provides a Fiori style title and`Toggle`. /// /// ## Usage diff --git a/Sources/FioriSwiftUICore/_FioriStyles/OptionalTitleStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/OptionalTitleStyle.fiori.swift new file mode 100644 index 000000000..f2c389abd --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/OptionalTitleStyle.fiori.swift @@ -0,0 +1,19 @@ +import FioriThemeManager +import Foundation +import SwiftUI + +// Base Layout style +public struct OptionalTitleBaseStyle: OptionalTitleStyle { + @ViewBuilder + public func makeBody(_ configuration: OptionalTitleConfiguration) -> some View { + configuration.optionalTitle + } +} + +// Default fiori styles +public struct OptionalTitleFioriStyle: OptionalTitleStyle { + @ViewBuilder + public func makeBody(_ configuration: OptionalTitleConfiguration) -> some View { + OptionalTitle(configuration) + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/TimelineNowIndicatorStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/TimelineNowIndicatorStyle.fiori.swift index 10ddce151..eea8d35a3 100644 --- a/Sources/FioriSwiftUICore/_FioriStyles/TimelineNowIndicatorStyle.fiori.swift +++ b/Sources/FioriSwiftUICore/_FioriStyles/TimelineNowIndicatorStyle.fiori.swift @@ -8,7 +8,7 @@ public struct TimelineNowIndicatorBaseStyle: TimelineNowIndicatorStyle { HStack(alignment: .center, spacing: 0) { configuration.nowIndicatorNode Rectangle() - .frame(height: 2) + .frame(height: 1) .foregroundColor(Color.preferredColor(.tintColor)) .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } @@ -20,7 +20,7 @@ extension TimelineNowIndicatorFioriStyle { struct ContentFioriStyle: TimelineNowIndicatorStyle { func makeBody(_ configuration: TimelineNowIndicatorConfiguration) -> some View { TimelineNowIndicator(configuration) - .padding(EdgeInsets(top: 0, leading: 88, bottom: 0, trailing: 0)) + .padding(EdgeInsets(top: 0, leading: 90, bottom: 0, trailing: 0)) } } @@ -29,7 +29,7 @@ extension TimelineNowIndicatorFioriStyle { func makeBody(_ configuration: NowIndicatorNodeConfiguration) -> some View { NowIndicatorNode(configuration) - .font(.system(size: 7)) + .font(.system(size: 5)) .foregroundColor(.preferredColor(.tintColor)) .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } diff --git a/Sources/FioriSwiftUICore/_FioriStyles/TimelinePreviewItemStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/TimelinePreviewItemStyle.fiori.swift new file mode 100644 index 000000000..0d5a8acb4 --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/TimelinePreviewItemStyle.fiori.swift @@ -0,0 +1,102 @@ +import FioriThemeManager +import Foundation +import SwiftUI + +// Base Layout style +public struct TimelinePreviewItemBaseStyle: TimelinePreviewItemStyle { + public func makeBody(_ configuration: TimelinePreviewItemConfiguration) -> some View { + VStack(alignment: .leading, spacing: 0) { + configuration.timestamp + HStack(alignment: .center, spacing: 0) { + if configuration.icon.isEmpty { + configuration.timelineNode + } else { + configuration.icon + } + if configuration.nodeType != TimelineNodeType.end { + Rectangle() + .frame(height: 2) + .foregroundColor(Color.preferredColor(configuration.isFuture ? .separatorOpaque : .tintColor)) + .padding(.trailing, -3) + } else { + Spacer() + } + } + .frame(minHeight: 15) + .padding(EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 0)) + configuration.title + } + } +} + +// Default fiori styles +extension TimelinePreviewItemFioriStyle { + struct ContentFioriStyle: TimelinePreviewItemStyle { + func makeBody(_ configuration: TimelinePreviewItemConfiguration) -> some View { + TimelinePreviewItem(configuration) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + + struct TitleFioriStyle: TitleStyle { + let timelinePreviewItemConfiguration: TimelinePreviewItemConfiguration + + func makeBody(_ configuration: TitleConfiguration) -> some View { + Title(configuration) + .lineLimit(/*@START_MENU_TOKEN@*/2/*@END_MENU_TOKEN@*/) + .foregroundColor(Color.preferredColor(.primaryLabel)) + .font(.fiori(forTextStyle: .subheadline)) + .alignmentGuide(.timelinePreviewAlignmentGuide) { context in + context[.firstTextBaseline] + } + .multilineTextAlignment(.leading) + } + } + + struct IconFioriStyle: IconStyle { + let timelinePreviewItemConfiguration: TimelinePreviewItemConfiguration + + func makeBody(_ configuration: IconConfiguration) -> some View { + Icon(configuration) + .font(TimelineStyleHelpers.getFontSize(for: self.timelinePreviewItemConfiguration)) + .foregroundColor(Color.preferredColor(self.timelinePreviewItemConfiguration.isFuture ? .separatorOpaque : .tintColor)) + } + } + + struct TimelineNodeFioriStyle: TimelineNodeStyle { + let timelinePreviewItemConfiguration: TimelinePreviewItemConfiguration + + func makeBody(_ configuration: TimelineNodeConfiguration) -> some View { + TimelineNode(configuration) + .font(TimelineStyleHelpers.getFontSize(for: self.timelinePreviewItemConfiguration)) + .fontWeight(.bold) + .foregroundColor(Color.preferredColor(self.timelinePreviewItemConfiguration.isFuture ? .separatorOpaque : .tintColor)) + } + } + + struct TimestampFioriStyle: TimestampStyle { + let timelinePreviewItemConfiguration: TimelinePreviewItemConfiguration + + func makeBody(_ configuration: TimestampConfiguration) -> some View { + Timestamp(configuration) + .lineLimit(/*@START_MENU_TOKEN@*/2/*@END_MENU_TOKEN@*/) + .foregroundColor(Color.preferredColor(.tertiaryLabel)) + .font(.fiori(forTextStyle: .footnote)) + .alignmentGuide(.timelinePreviewAlignmentGuide) { context in + context[.lastTextBaseline] + } + .multilineTextAlignment(/*@START_MENU_TOKEN@*/ .leading/*@END_MENU_TOKEN@*/) + } + } + + enum TimelineStyleHelpers { + static func getFontSize(for configuration: TimelinePreviewItemConfiguration) -> Font { + switch configuration.nodeType { + case .beforeStart, .start, .beforeEnd, .end: + return .fiori(forTextStyle: .caption2) + default: + return .fiori(forTextStyle: .subheadline) + } + } + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/TimelinePreviewStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/TimelinePreviewStyle.fiori.swift new file mode 100644 index 000000000..b55afbb93 --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/TimelinePreviewStyle.fiori.swift @@ -0,0 +1,177 @@ +import FioriThemeManager +import Foundation +import SwiftUI + +// Base Layout style +public struct TimelinePreviewBaseStyle: TimelinePreviewStyle { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @State private var itemCount = 0 + @State private var VSize: CGSize = .zero + + public func makeBody(_ configuration: TimelinePreviewConfiguration) -> some View { + VStack(alignment: .leading, spacing: 0) { + if !configuration.optionalTitle.isEmpty || !configuration.action.isEmpty { + BuildHeader(configuration: configuration, itemCount: configuration.items.count) + } + BuildTimelinePreviewItem(configuration: configuration, displayItems: self.getDisplayItemCount(VSWidth: self.VSize.width)) + }.readSize { newSize in + self.VSize = newSize + } + } + + func getDisplayItemCount(VSWidth: CGFloat) -> Int { + let itemCount: Int + switch self.horizontalSizeClass { + case .regular: + itemCount = VSWidth > 672 ? 4 : 3 + default: + itemCount = 2 + } + return itemCount + } +} + +struct BuildHeader: View { + let configuration: TimelinePreviewConfiguration + let itemCount: Int + + var body: some View { + HStack { + self.configuration.optionalTitle + Spacer() + self.configuration.action + .actionStyle(content: { actionConfig in + if actionConfig.action.isEmpty { + let labelFormat = NSLocalizedString("See All (%d)", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") + let labelString = String(format: labelFormat, self.itemCount) + FioriButton(label: { _ in Label(labelString, systemImage: "chevron.forward").labelStyle(SeeAllActionLabelStyle()) }) + .fioriButtonStyle(FioriPlainButtonStyle()) + } else { + self.configuration.action + } + }) + } + .padding(EdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0)) + } +} + +struct BuildTimelinePreviewItem: View { + let configuration: TimelinePreviewConfiguration + let displayItems: Int + + var body: some View { + HStack(alignment: .timelinePreviewAlignmentGuide, spacing: 4) { + ForEach(self.filterItems(itemsData: self.configuration.items), id: \.id) { item in + let itemLabelFormat = NSLocalizedString("Item %d of %d", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") + let itemLabelString = String(format: itemLabelFormat, item.formatter.string(from: item.due), item.title) + let dateString = item.formatter.string(from: item.due) + let timestampFormat = NSLocalizedString("Today, %d", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") + let timestampString = String(format: timestampFormat, dateString) + let dateAttributedString = AttributedString(Date.compareTwoDates(first: item.due, second: Date()) == .orderedSame ? timestampString : dateString) + TimelinePreviewItem(title: AttributedString(item.title), icon: item.icon, timelineNode: item.timelineNode, timestamp: dateAttributedString, isFuture: item.isFuture ?? false, nodeType: item.timelineNode) + .accessibilityElement(children: .ignore) + .accessibilityLabel(itemLabelString) + } + } + } + + func filterItems(itemsData: [any TimelinePreviewItemModel]) -> [any TimelinePreviewItemModel] { + // flag item with isFuture and isCurrent + let updatedItems = itemsData.map { item in + var mutableItem = item + mutableItem.isFuture = Date.compareTwoDates(first: mutableItem.due, second: Date()) == .orderedDescending + mutableItem.isCurrent = Date.compareTwoDates(first: mutableItem.due, second: Date()) == .orderedSame + return mutableItem + } + // sort the data by due and filter data by display item count + let filteredItems = Array(updatedItems.sorted(by: { $0.due < $1.due }).prefix(self.displayItems)) + + return filteredItems + } +} + +// Default fiori styles +extension TimelinePreviewFioriStyle { + struct ContentFioriStyle: TimelinePreviewStyle { + func makeBody(_ configuration: TimelinePreviewConfiguration) -> some View { + TimelinePreview(configuration) + .fixedSize(horizontal: false, vertical: true) + .contentShape(.rect) + .padding(EdgeInsets(top: 11, leading: 16, bottom: 16, trailing: 16)) + } + } + + struct OptionalTitleFioriStyle: OptionalTitleStyle { + let timelinePreviewConfiguration: TimelinePreviewConfiguration + + func makeBody(_ configuration: OptionalTitleConfiguration) -> some View { + OptionalTitle(configuration) + .font(.fiori(forTextStyle: .subheadline)) + .foregroundColor(Color.preferredColor(.secondaryLabel)) + .multilineTextAlignment(.leading) + } + } + + struct ActionFioriStyle: ActionStyle { + let timelinePreviewConfiguration: TimelinePreviewConfiguration + + func makeBody(_ configuration: ActionConfiguration) -> some View { + Action(configuration) + .font(.fiori(forTextStyle: .subheadline)) + .fioriButtonStyle(FioriPlainButtonStyle()) + } + } +} + +struct SeeAllActionLabelStyle: LabelStyle { + func makeBody(configuration: Configuration) -> some View { + HStack(alignment: .center) { + configuration.title + .font(.fiori(forTextStyle: .subheadline)) + .foregroundColor(Color.preferredColor(.tintColor)) + configuration.icon + .font(.fiori(forTextStyle: .subheadline)) + .foregroundColor(Color.preferredColor(.secondaryLabel)) + } + } +} + +extension Date { + static func compareTwoDates(first: Date, second: Date) -> ComparisonResult { + let calendar = Calendar.current + let components: Set = [Calendar.Component.day, .month, .year, .hour, .minute, .second] + let date1Components = calendar.dateComponents(components, from: first) + let date2Components = calendar.dateComponents(components, from: second) + let date1 = calendar.date(from: date1Components) + let date2 = calendar.date(from: date2Components) + + guard let date1, let date2 else { + fatalError("Two dates in comparison must have the same component") + } + + return date1.compare(date2) + } +} + +extension VerticalAlignment { + private struct TimelinePreviewAlignment: AlignmentID { + static func defaultValue(in context: ViewDimensions) -> CGFloat { + context[VerticalAlignment.bottom] + } + } + + static let timelinePreviewAlignmentGuide = VerticalAlignment( + TimelinePreviewAlignment.self + ) +} + +extension View { + func readSize(onChange: @escaping (CGSize) -> Void) -> some View { + background( + GeometryReader { geometryProxy in + Color.clear.preference(key: SizePreferenceKey.self, value: geometryProxy.size) + } + ) + .onPreferenceChange(SizePreferenceKey.self, perform: onChange) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/OptionalTitle/OptionalTitle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/OptionalTitle/OptionalTitle.generated.swift new file mode 100644 index 000000000..61fe127d6 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/OptionalTitle/OptionalTitle.generated.swift @@ -0,0 +1,63 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct OptionalTitle { + let optionalTitle: any View + + @Environment(\.optionalTitleStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder optionalTitle: () -> any View = { EmptyView() }) { + self.optionalTitle = optionalTitle() + } +} + +public extension OptionalTitle { + init(optionalTitle: AttributedString?) { + self.init(optionalTitle: { OptionalText(optionalTitle) }) + } +} + +public extension OptionalTitle { + init(_ configuration: OptionalTitleConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: OptionalTitleConfiguration, shouldApplyDefaultStyle: Bool) { + self.optionalTitle = configuration.optionalTitle + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension OptionalTitle: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(optionalTitle: .init(self.optionalTitle))).typeErased + .transformEnvironment(\.optionalTitleStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension OptionalTitle { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + OptionalTitle(optionalTitle: { self.optionalTitle }) + .shouldApplyDefaultStyle(false) + .optionalTitleStyle(.fiori) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/OptionalTitle/OptionalTitleStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/OptionalTitle/OptionalTitleStyle.generated.swift new file mode 100644 index 000000000..96985c000 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/OptionalTitle/OptionalTitleStyle.generated.swift @@ -0,0 +1,28 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol OptionalTitleStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: OptionalTitleConfiguration) -> Body +} + +struct AnyOptionalTitleStyle: OptionalTitleStyle { + let content: (OptionalTitleConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (OptionalTitleConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: OptionalTitleConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct OptionalTitleConfiguration { + public let optionalTitle: OptionalTitle + + public typealias OptionalTitle = ConfigurationViewWrapper +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreview/TimelinePreview.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreview/TimelinePreview.generated.swift new file mode 100644 index 000000000..d5ec8bcd9 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreview/TimelinePreview.generated.swift @@ -0,0 +1,114 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/// `TimelinePreview` is an view for showing a collection of tasks. It comes with a header and a collection view which uses `TimelinePreviewItem` to represent data items within it. +/// +/// ## Usage +/// ```swift +/// Create a struct that conforms to the protocol: TimelinePreviewItemModel, providing implementation for the required properties and methods: +/// struct TimelinePreviewItemModelImplementation: TimelinePreviewItemModel { +/// var id: UUID +/// var title: AttributedString +/// var icon: Image? +/// var timelineNode: FioriSwiftUICore.TimelineNodeType +/// var due: Date +/// var formatter: DateFormatter? +/// var isFuture: Bool? +/// var isCurrent: Bool? +/// +/// init(id: UUID = UUID(), title: AttributedString, icon: Image? = nil, timelineNode: FioriSwiftUICore.TimelineNodeType, due: Date, dateFormat: String? = nil, isFuture: Bool? = nil, isCurrent: Bool? = nil) { +/// self.id = id +/// self.title = title +/// self.icon = icon +/// self.timelineNode = timelineNode +/// self.due = due +/// self.formatter = DateFormatter() +/// if let dateFormat { +/// self.formatter.dateFormat = dateFormat +/// } else { +/// self.formatter.dateFormat = "MMMM dd yyyy" +/// } +/// self.isFuture = isFuture +/// self.isCurrent = isCurrent +/// } +/// } +/// +/// Create a Protocol Instance array with Initial Value +/// @State private var items: [TimelinePreviewItemModelImplementation] = [TimelinePreviewItemModelImplementation(title: "Complete", timelineNode: TimelineNodeType.complete, due: ISO8601DateFormatter().date(from: "2023-07-21T12:00:00Z")!),TimelinePreviewItemModelImplementation(title: "End", timelineNode: TimelineNodeType.end, due: ISO8601DateFormatter().date(from: "2023-08-10T12:00:00Z")!)] +/// +/// Create TimelinePreview with the array +/// TimelinePreview(optionalTitle: { Text("Timeline") }, data: .constant(items.map { $0 as any TimelinePreviewItemModel })) +/// ``` +public struct TimelinePreview { + let optionalTitle: any View + let action: any View + /// The data for all timelinePreviewItems + @Binding var items: [any TimelinePreviewItemModel] + + @Environment(\.timelinePreviewStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder optionalTitle: () -> any View = { EmptyView() }, + @ViewBuilder action: () -> any View = { EmptyView() }, + items: Binding<[any TimelinePreviewItemModel]>) + { + self.optionalTitle = OptionalTitle { optionalTitle() } + self.action = Action { action() } + self._items = items + } +} + +public extension TimelinePreview { + init(optionalTitle: AttributedString?, + action: FioriButton? = nil, + items: Binding<[any TimelinePreviewItemModel]>) + { + self.init(optionalTitle: { OptionalText(optionalTitle) }, action: { action }, items: items) + } +} + +public extension TimelinePreview { + init(_ configuration: TimelinePreviewConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: TimelinePreviewConfiguration, shouldApplyDefaultStyle: Bool) { + self.optionalTitle = configuration.optionalTitle + self.action = configuration.action + self._items = configuration.$items + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension TimelinePreview: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(optionalTitle: .init(self.optionalTitle), action: .init(self.action), items: self.$items)).typeErased + .transformEnvironment(\.timelinePreviewStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension TimelinePreview { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + TimelinePreview(.init(optionalTitle: .init(self.optionalTitle), action: .init(self.action), items: self.$items)) + .shouldApplyDefaultStyle(false) + .timelinePreviewStyle(TimelinePreviewFioriStyle.ContentFioriStyle()) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreview/TimelinePreviewStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreview/TimelinePreviewStyle.generated.swift new file mode 100644 index 000000000..b547169b0 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreview/TimelinePreviewStyle.generated.swift @@ -0,0 +1,39 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol TimelinePreviewStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: TimelinePreviewConfiguration) -> Body +} + +struct AnyTimelinePreviewStyle: TimelinePreviewStyle { + let content: (TimelinePreviewConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (TimelinePreviewConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: TimelinePreviewConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct TimelinePreviewConfiguration { + public let optionalTitle: OptionalTitle + public let action: Action + @Binding public var items: [any TimelinePreviewItemModel] + + public typealias OptionalTitle = ConfigurationViewWrapper + public typealias Action = ConfigurationViewWrapper +} + +public struct TimelinePreviewFioriStyle: TimelinePreviewStyle { + public func makeBody(_ configuration: TimelinePreviewConfiguration) -> some View { + TimelinePreview(configuration) + .optionalTitleStyle(OptionalTitleFioriStyle(timelinePreviewConfiguration: configuration)) + .actionStyle(ActionFioriStyle(timelinePreviewConfiguration: configuration)) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreviewItem/TimelinePreviewItem.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreviewItem/TimelinePreviewItem.generated.swift new file mode 100644 index 000000000..47138f117 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreviewItem/TimelinePreviewItem.generated.swift @@ -0,0 +1,93 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/// 'TimelinePreviewItem' is an item specialized for placement in TimelinePreview. +public struct TimelinePreviewItem { + let title: any View + let icon: any View + let timelineNode: any View + let timestamp: any View + /// The node is in future or not. Default is not in future. + let isFuture: Bool + /// Timeline node type + let nodeType: TimelineNodeType + + @Environment(\.timelinePreviewItemStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder title: () -> any View, + @ViewBuilder icon: () -> any View = { EmptyView() }, + @ViewBuilder timelineNode: () -> any View, + @ViewBuilder timestamp: () -> any View = { EmptyView() }, + isFuture: Bool = false, + nodeType: TimelineNodeType) + { + self.title = Title { title() } + self.icon = Icon { icon() } + self.timelineNode = TimelineNode { timelineNode() } + self.timestamp = Timestamp { timestamp() } + self.isFuture = isFuture + self.nodeType = nodeType + } +} + +public extension TimelinePreviewItem { + init(title: AttributedString, + icon: Image? = nil, + timelineNode: TimelineNodeType, + timestamp: AttributedString? = nil, + isFuture: Bool = false, + nodeType: TimelineNodeType) + { + self.init(title: { Text(title) }, icon: { icon }, timelineNode: { TimelineNodeView(timelineNode) }, timestamp: { OptionalText(timestamp) }, isFuture: isFuture, nodeType: nodeType) + } +} + +public extension TimelinePreviewItem { + init(_ configuration: TimelinePreviewItemConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: TimelinePreviewItemConfiguration, shouldApplyDefaultStyle: Bool) { + self.title = configuration.title + self.icon = configuration.icon + self.timelineNode = configuration.timelineNode + self.timestamp = configuration.timestamp + self.isFuture = configuration.isFuture + self.nodeType = configuration.nodeType + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension TimelinePreviewItem: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(title: .init(self.title), icon: .init(self.icon), timelineNode: .init(self.timelineNode), timestamp: .init(self.timestamp), isFuture: self.isFuture, nodeType: self.nodeType)).typeErased + .transformEnvironment(\.timelinePreviewItemStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension TimelinePreviewItem { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + TimelinePreviewItem(.init(title: .init(self.title), icon: .init(self.icon), timelineNode: .init(self.timelineNode), timestamp: .init(self.timestamp), isFuture: self.isFuture, nodeType: self.nodeType)) + .shouldApplyDefaultStyle(false) + .timelinePreviewItemStyle(TimelinePreviewItemFioriStyle.ContentFioriStyle()) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreviewItem/TimelinePreviewItemStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreviewItem/TimelinePreviewItemStyle.generated.swift new file mode 100644 index 000000000..ac90d1198 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/TimelinePreviewItem/TimelinePreviewItemStyle.generated.swift @@ -0,0 +1,46 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol TimelinePreviewItemStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: TimelinePreviewItemConfiguration) -> Body +} + +struct AnyTimelinePreviewItemStyle: TimelinePreviewItemStyle { + let content: (TimelinePreviewItemConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (TimelinePreviewItemConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: TimelinePreviewItemConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct TimelinePreviewItemConfiguration { + public let title: Title + public let icon: Icon + public let timelineNode: TimelineNode + public let timestamp: Timestamp + public let isFuture: Bool + public let nodeType: TimelineNodeType + + public typealias Title = ConfigurationViewWrapper + public typealias Icon = ConfigurationViewWrapper + public typealias TimelineNode = ConfigurationViewWrapper + public typealias Timestamp = ConfigurationViewWrapper +} + +public struct TimelinePreviewItemFioriStyle: TimelinePreviewItemStyle { + public func makeBody(_ configuration: TimelinePreviewItemConfiguration) -> some View { + TimelinePreviewItem(configuration) + .titleStyle(TitleFioriStyle(timelinePreviewItemConfiguration: configuration)) + .iconStyle(IconFioriStyle(timelinePreviewItemConfiguration: configuration)) + .timelineNodeStyle(TimelineNodeFioriStyle(timelinePreviewItemConfiguration: configuration)) + .timestampStyle(TimestampFioriStyle(timelinePreviewItemConfiguration: configuration)) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift index 628f967ee..f721a3fdd 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift @@ -2845,6 +2845,20 @@ public extension ObjectItemStyle where Self == ObjectItemActionStyle { } } +// MARK: OptionalTitleStyle + +public extension OptionalTitleStyle where Self == OptionalTitleBaseStyle { + static var base: OptionalTitleBaseStyle { + OptionalTitleBaseStyle() + } +} + +public extension OptionalTitleStyle where Self == OptionalTitleFioriStyle { + static var fiori: OptionalTitleFioriStyle { + OptionalTitleFioriStyle() + } +} + // MARK: OptionsStyle public extension OptionsStyle where Self == OptionsBaseStyle { @@ -4518,6 +4532,160 @@ public extension TimelineNowIndicatorStyle where Self == TimelineNowIndicatorNow } } +// MARK: TimelinePreviewStyle + +public extension TimelinePreviewStyle where Self == TimelinePreviewBaseStyle { + static var base: TimelinePreviewBaseStyle { + TimelinePreviewBaseStyle() + } +} + +public extension TimelinePreviewStyle where Self == TimelinePreviewFioriStyle { + static var fiori: TimelinePreviewFioriStyle { + TimelinePreviewFioriStyle() + } +} + +public struct TimelinePreviewOptionalTitleStyle: TimelinePreviewStyle { + let style: any OptionalTitleStyle + + public func makeBody(_ configuration: TimelinePreviewConfiguration) -> some View { + TimelinePreview(configuration) + .optionalTitleStyle(self.style) + .typeErased + } +} + +public extension TimelinePreviewStyle where Self == TimelinePreviewOptionalTitleStyle { + static func optionalTitleStyle(_ style: some OptionalTitleStyle) -> TimelinePreviewOptionalTitleStyle { + TimelinePreviewOptionalTitleStyle(style: style) + } + + static func optionalTitleStyle(@ViewBuilder content: @escaping (OptionalTitleConfiguration) -> some View) -> TimelinePreviewOptionalTitleStyle { + let style = AnyOptionalTitleStyle(content) + return TimelinePreviewOptionalTitleStyle(style: style) + } +} + +public struct TimelinePreviewActionStyle: TimelinePreviewStyle { + let style: any ActionStyle + + public func makeBody(_ configuration: TimelinePreviewConfiguration) -> some View { + TimelinePreview(configuration) + .actionStyle(self.style) + .typeErased + } +} + +public extension TimelinePreviewStyle where Self == TimelinePreviewActionStyle { + static func actionStyle(_ style: some ActionStyle) -> TimelinePreviewActionStyle { + TimelinePreviewActionStyle(style: style) + } + + static func actionStyle(@ViewBuilder content: @escaping (ActionConfiguration) -> some View) -> TimelinePreviewActionStyle { + let style = AnyActionStyle(content) + return TimelinePreviewActionStyle(style: style) + } +} + +// MARK: TimelinePreviewItemStyle + +public extension TimelinePreviewItemStyle where Self == TimelinePreviewItemBaseStyle { + static var base: TimelinePreviewItemBaseStyle { + TimelinePreviewItemBaseStyle() + } +} + +public extension TimelinePreviewItemStyle where Self == TimelinePreviewItemFioriStyle { + static var fiori: TimelinePreviewItemFioriStyle { + TimelinePreviewItemFioriStyle() + } +} + +public struct TimelinePreviewItemTitleStyle: TimelinePreviewItemStyle { + let style: any TitleStyle + + public func makeBody(_ configuration: TimelinePreviewItemConfiguration) -> some View { + TimelinePreviewItem(configuration) + .titleStyle(self.style) + .typeErased + } +} + +public extension TimelinePreviewItemStyle where Self == TimelinePreviewItemTitleStyle { + static func titleStyle(_ style: some TitleStyle) -> TimelinePreviewItemTitleStyle { + TimelinePreviewItemTitleStyle(style: style) + } + + static func titleStyle(@ViewBuilder content: @escaping (TitleConfiguration) -> some View) -> TimelinePreviewItemTitleStyle { + let style = AnyTitleStyle(content) + return TimelinePreviewItemTitleStyle(style: style) + } +} + +public struct TimelinePreviewItemIconStyle: TimelinePreviewItemStyle { + let style: any IconStyle + + public func makeBody(_ configuration: TimelinePreviewItemConfiguration) -> some View { + TimelinePreviewItem(configuration) + .iconStyle(self.style) + .typeErased + } +} + +public extension TimelinePreviewItemStyle where Self == TimelinePreviewItemIconStyle { + static func iconStyle(_ style: some IconStyle) -> TimelinePreviewItemIconStyle { + TimelinePreviewItemIconStyle(style: style) + } + + static func iconStyle(@ViewBuilder content: @escaping (IconConfiguration) -> some View) -> TimelinePreviewItemIconStyle { + let style = AnyIconStyle(content) + return TimelinePreviewItemIconStyle(style: style) + } +} + +public struct TimelinePreviewItemTimelineNodeStyle: TimelinePreviewItemStyle { + let style: any TimelineNodeStyle + + public func makeBody(_ configuration: TimelinePreviewItemConfiguration) -> some View { + TimelinePreviewItem(configuration) + .timelineNodeStyle(self.style) + .typeErased + } +} + +public extension TimelinePreviewItemStyle where Self == TimelinePreviewItemTimelineNodeStyle { + static func timelineNodeStyle(_ style: some TimelineNodeStyle) -> TimelinePreviewItemTimelineNodeStyle { + TimelinePreviewItemTimelineNodeStyle(style: style) + } + + static func timelineNodeStyle(@ViewBuilder content: @escaping (TimelineNodeConfiguration) -> some View) -> TimelinePreviewItemTimelineNodeStyle { + let style = AnyTimelineNodeStyle(content) + return TimelinePreviewItemTimelineNodeStyle(style: style) + } +} + +public struct TimelinePreviewItemTimestampStyle: TimelinePreviewItemStyle { + let style: any TimestampStyle + + public func makeBody(_ configuration: TimelinePreviewItemConfiguration) -> some View { + TimelinePreviewItem(configuration) + .timestampStyle(self.style) + .typeErased + } +} + +public extension TimelinePreviewItemStyle where Self == TimelinePreviewItemTimestampStyle { + static func timestampStyle(_ style: some TimestampStyle) -> TimelinePreviewItemTimestampStyle { + TimelinePreviewItemTimestampStyle(style: style) + } + + static func timestampStyle(@ViewBuilder content: @escaping (TimestampConfiguration) -> some View) -> TimelinePreviewItemTimestampStyle { + let style = AnyTimestampStyle(content) + return TimelinePreviewItemTimestampStyle(style: style) + } +} + // MARK: TimestampStyle public extension TimestampStyle where Self == TimestampBaseStyle { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift index 93458187f..e44679694 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift @@ -990,6 +990,27 @@ extension EnvironmentValues { } } +// MARK: OptionalTitleStyle + +struct OptionalTitleStyleStackKey: EnvironmentKey { + static let defaultValue: [any OptionalTitleStyle] = [] +} + +extension EnvironmentValues { + var optionalTitleStyle: any OptionalTitleStyle { + self.optionalTitleStyleStack.last ?? .base + } + + var optionalTitleStyleStack: [any OptionalTitleStyle] { + get { + self[OptionalTitleStyleStackKey.self] + } + set { + self[OptionalTitleStyleStackKey.self] = newValue + } + } +} + // MARK: OptionsStyle struct OptionsStyleStackKey: EnvironmentKey { @@ -1704,6 +1725,48 @@ extension EnvironmentValues { } } +// MARK: TimelinePreviewStyle + +struct TimelinePreviewStyleStackKey: EnvironmentKey { + static let defaultValue: [any TimelinePreviewStyle] = [] +} + +extension EnvironmentValues { + var timelinePreviewStyle: any TimelinePreviewStyle { + self.timelinePreviewStyleStack.last ?? .base.concat(.fiori) + } + + var timelinePreviewStyleStack: [any TimelinePreviewStyle] { + get { + self[TimelinePreviewStyleStackKey.self] + } + set { + self[TimelinePreviewStyleStackKey.self] = newValue + } + } +} + +// MARK: TimelinePreviewItemStyle + +struct TimelinePreviewItemStyleStackKey: EnvironmentKey { + static let defaultValue: [any TimelinePreviewItemStyle] = [] +} + +extension EnvironmentValues { + var timelinePreviewItemStyle: any TimelinePreviewItemStyle { + self.timelinePreviewItemStyleStack.last ?? .base.concat(.fiori) + } + + var timelinePreviewItemStyleStack: [any TimelinePreviewItemStyle] { + get { + self[TimelinePreviewItemStyleStackKey.self] + } + set { + self[TimelinePreviewItemStyleStackKey.self] = newValue + } + } +} + // MARK: TimestampStyle struct TimestampStyleStackKey: EnvironmentKey { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift index 6e3e6cf6e..a67669b8b 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift @@ -1324,6 +1324,34 @@ public extension ObjectItemStyle { } } +// MARK: OptionalTitleStyle + +extension ModifiedStyle: OptionalTitleStyle where Style: OptionalTitleStyle { + public func makeBody(_ configuration: OptionalTitleConfiguration) -> some View { + OptionalTitle(configuration) + .optionalTitleStyle(self.style) + .modifier(self.modifier) + } +} + +public struct OptionalTitleStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.optionalTitleStyle(self.style) + } +} + +public extension OptionalTitleStyle { + func modifier(_ modifier: some ViewModifier) -> some OptionalTitleStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some OptionalTitleStyle) -> some OptionalTitleStyle { + style.modifier(OptionalTitleStyleModifier(style: self)) + } +} + // MARK: OptionsStyle extension ModifiedStyle: OptionsStyle where Style: OptionsStyle { @@ -2276,6 +2304,62 @@ public extension TimelineNowIndicatorStyle { } } +// MARK: TimelinePreviewStyle + +extension ModifiedStyle: TimelinePreviewStyle where Style: TimelinePreviewStyle { + public func makeBody(_ configuration: TimelinePreviewConfiguration) -> some View { + TimelinePreview(configuration) + .timelinePreviewStyle(self.style) + .modifier(self.modifier) + } +} + +public struct TimelinePreviewStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.timelinePreviewStyle(self.style) + } +} + +public extension TimelinePreviewStyle { + func modifier(_ modifier: some ViewModifier) -> some TimelinePreviewStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some TimelinePreviewStyle) -> some TimelinePreviewStyle { + style.modifier(TimelinePreviewStyleModifier(style: self)) + } +} + +// MARK: TimelinePreviewItemStyle + +extension ModifiedStyle: TimelinePreviewItemStyle where Style: TimelinePreviewItemStyle { + public func makeBody(_ configuration: TimelinePreviewItemConfiguration) -> some View { + TimelinePreviewItem(configuration) + .timelinePreviewItemStyle(self.style) + .modifier(self.modifier) + } +} + +public struct TimelinePreviewItemStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.timelinePreviewItemStyle(self.style) + } +} + +public extension TimelinePreviewItemStyle { + func modifier(_ modifier: some ViewModifier) -> some TimelinePreviewItemStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some TimelinePreviewItemStyle) -> some TimelinePreviewItemStyle { + style.modifier(TimelinePreviewItemStyleModifier(style: self)) + } +} + // MARK: TimestampStyle extension ModifiedStyle: TimestampStyle where Style: TimestampStyle { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift index ce2a30855..8f2cbba9d 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift @@ -755,6 +755,22 @@ extension ObjectItemStyle { } } +// MARK: OptionalTitleStyle + +struct ResolvedOptionalTitleStyle: View { + let style: Style + let configuration: OptionalTitleConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension OptionalTitleStyle { + func resolve(configuration: OptionalTitleConfiguration) -> some View { + ResolvedOptionalTitleStyle(style: self, configuration: configuration) + } +} + // MARK: OptionsStyle struct ResolvedOptionsStyle: View { @@ -1299,6 +1315,38 @@ extension TimelineNowIndicatorStyle { } } +// MARK: TimelinePreviewStyle + +struct ResolvedTimelinePreviewStyle: View { + let style: Style + let configuration: TimelinePreviewConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension TimelinePreviewStyle { + func resolve(configuration: TimelinePreviewConfiguration) -> some View { + ResolvedTimelinePreviewStyle(style: self, configuration: configuration) + } +} + +// MARK: TimelinePreviewItemStyle + +struct ResolvedTimelinePreviewItemStyle: View { + let style: Style + let configuration: TimelinePreviewItemConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension TimelinePreviewItemStyle { + func resolve(configuration: TimelinePreviewItemConfiguration) -> some View { + ResolvedTimelinePreviewItemStyle(style: self, configuration: configuration) + } +} + // MARK: TimestampStyle struct ResolvedTimestampStyle: View { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift index be8fd660a..3af5d1b6f 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift @@ -802,6 +802,23 @@ public extension View { } } +// MARK: OptionalTitleStyle + +public extension View { + func optionalTitleStyle(_ style: some OptionalTitleStyle) -> some View { + self.transformEnvironment(\.optionalTitleStyleStack) { stack in + stack.append(style) + } + } + + func optionalTitleStyle(@ViewBuilder content: @escaping (OptionalTitleConfiguration) -> some View) -> some View { + self.transformEnvironment(\.optionalTitleStyleStack) { stack in + let style = AnyOptionalTitleStyle(content) + stack.append(style) + } + } +} + // MARK: OptionsStyle public extension View { @@ -1380,6 +1397,40 @@ public extension View { } } +// MARK: TimelinePreviewStyle + +public extension View { + func timelinePreviewStyle(_ style: some TimelinePreviewStyle) -> some View { + self.transformEnvironment(\.timelinePreviewStyleStack) { stack in + stack.append(style) + } + } + + func timelinePreviewStyle(@ViewBuilder content: @escaping (TimelinePreviewConfiguration) -> some View) -> some View { + self.transformEnvironment(\.timelinePreviewStyleStack) { stack in + let style = AnyTimelinePreviewStyle(content) + stack.append(style) + } + } +} + +// MARK: TimelinePreviewItemStyle + +public extension View { + func timelinePreviewItemStyle(_ style: some TimelinePreviewItemStyle) -> some View { + self.transformEnvironment(\.timelinePreviewItemStyleStack) { stack in + stack.append(style) + } + } + + func timelinePreviewItemStyle(@ViewBuilder content: @escaping (TimelinePreviewItemConfiguration) -> some View) -> some View { + self.transformEnvironment(\.timelinePreviewItemStyleStack) { stack in + let style = AnyTimelinePreviewItemStyle(content) + stack.append(style) + } + } +} + // MARK: TimestampStyle public extension View { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift index b9c915603..6a10a842f 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift @@ -360,6 +360,12 @@ extension ObjectItem: _ViewEmptyChecking { } } +extension OptionalTitle: _ViewEmptyChecking { + public var isEmpty: Bool { + optionalTitle.isEmpty + } +} + extension Options: _ViewEmptyChecking { public var isEmpty: Bool { false @@ -596,6 +602,22 @@ extension TimelineNowIndicator: _ViewEmptyChecking { } } +extension TimelinePreview: _ViewEmptyChecking { + public var isEmpty: Bool { + optionalTitle.isEmpty && + action.isEmpty + } +} + +extension TimelinePreviewItem: _ViewEmptyChecking { + public var isEmpty: Bool { + title.isEmpty && + icon.isEmpty && + timelineNode.isEmpty && + timestamp.isEmpty + } +} + extension Timestamp: _ViewEmptyChecking { public var isEmpty: Bool { timestamp.isEmpty diff --git a/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings b/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings index 767669327..2fe276f9c 100644 --- a/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings +++ b/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings @@ -183,3 +183,12 @@ /* XACT: RatingControl accessibility label */ "%d out of %d stars" = "%d out of %d stars"; + +/* XBUT: timeline preview component see all label */ +"See All (%d)" = "See All (%d)"; + +/* XACT: Timeline preview item accessibility label */ +"Item %d of %d" = "Item %d of %d"; + +/* XBUT: timeline preview component timestamp label */ +"Today, %d" = "Today, %d";