From f03062b97937176e6540667064c4e351e885427c Mon Sep 17 00:00:00 2001 From: Ahmed-Naguib93 Date: Fri, 20 Dec 2024 15:07:07 +0200 Subject: [PATCH 1/2] feat: update dashboard & add LearningObjectCard card component [ignore-commit-lint] --- .../Horizon/Resources/Localizable.xcstrings | 6 +- .../Dashboard/View/DashboardView.swift | 106 +++++------- .../Dashboard/View/DashboardViewModel.swift | 4 + .../Features/HorizonTabBarController.swift | 4 +- ...rizonUI.LearningObjectCard.Storybook.swift | 78 +++++++++ .../HorizonUI.LearningObjectCard.swift | 157 ++++++++++++++++++ .../Components/Pill/HorizonUI.Pill.swift | 45 +++-- .../Spaces/HorizonUI.Spaces.Primitivies.swift | 18 +- .../Foundation/Spaces/HorizonUI.Spaces.swift | 2 +- .../HorizonUI.Typography.Storybook.swift | 2 +- .../Typography/HorizonUI.Typography.swift | 7 +- .../Sources/HorizonUI/Sources/Storybook.swift | 4 +- 12 files changed, 333 insertions(+), 100 deletions(-) create mode 100644 packages/HorizonUI/Sources/HorizonUI/Sources/Components/LearningObjectCard/HorizonUI.LearningObjectCard.Storybook.swift create mode 100644 packages/HorizonUI/Sources/HorizonUI/Sources/Components/LearningObjectCard/HorizonUI.LearningObjectCard.swift diff --git a/Horizon/Horizon/Resources/Localizable.xcstrings b/Horizon/Horizon/Resources/Localizable.xcstrings index b329ddc64e..d92119c0b4 100644 --- a/Horizon/Horizon/Resources/Localizable.xcstrings +++ b/Horizon/Horizon/Resources/Localizable.xcstrings @@ -46,9 +46,6 @@ }, "Confusing" : { - }, - "Continue learning" : { - }, "Due" : { @@ -91,6 +88,9 @@ }, "My Progress" : { + }, + "Next Up" : { + }, "Note" : { diff --git a/Horizon/Horizon/Sources/Features/Dashboard/View/DashboardView.swift b/Horizon/Horizon/Sources/Features/Dashboard/View/DashboardView.swift index 556ccfbea5..0849286c16 100644 --- a/Horizon/Horizon/Sources/Features/Dashboard/View/DashboardView.swift +++ b/Horizon/Horizon/Sources/Features/Dashboard/View/DashboardView.swift @@ -18,6 +18,7 @@ import Core import SwiftUI +import HorizonUI struct DashboardView: View { @ObservedObject private var viewModel: DashboardViewModel @@ -30,30 +31,55 @@ struct DashboardView: View { var body: some View { InstUI.BaseScreen( state: viewModel.state, - config: .init(refreshable: true) + config: .init(refreshable: true, loaderBackgroundColor: .huiColors.surface.pagePrimary) ) { _ in - VStack(spacing: 0) { + LazyVStack(spacing: .zero) { ForEach(viewModel.courses) { course in if course.currentModuleItem != nil, !course.upcomingModuleItems.isEmpty { - VStack(alignment: .leading, spacing: 12) { - Size24BoldTextDarkestTitle(title: course.name) - .padding(.top, 16) - CertificateProgressBar( + VStack(alignment: .leading, spacing: .zero) { + Text(course.name) + .huiTypography(.h1) + .foregroundStyle(Color.huiColors.text.title) + .padding(.top, .huiSpaces.primitives.medium) + .padding(.bottom, .huiSpaces.primitives.mediumSmall) + + HorizonUI.ProgressBar( progress: course.progress, - progressString: course.progressString + size: .medium, + numberPosition: .outside ) - moduleView(course: course) + if let module = course.currentModule, let moduleItem = course.currentModuleItem { + Text("Next Up", bundle: .horizon) + .huiTypography(.h3) + .foregroundStyle(Color.huiColors.text.title) + .padding(.top, .huiSpaces.primitives.large) + .padding(.bottom, .huiSpaces.primitives.small) + .frame(maxWidth: .infinity, alignment: .leading) + + HorizonUI.LearningObjectCard( + status: "Default", + moduleTitle: module.name, + learningObjectName: moduleItem.title, + duration: "20 Mins", + type: moduleItem.type?.label, + dueDate: moduleItem.dueAt?.relativeShortDateOnlyString + ) { + if let url = moduleItem.htmlURL { + viewModel.navigateToCourseDetails(url: url, viewController: viewController) + } + } + } } - .padding(.horizontal, 16) - .background() + .padding(.horizontal, .huiSpaces.primitives.medium) } } } + .padding(.bottom, .huiSpaces.primitives.mediumSmall) } .navigationBarItems(leading: nameLabel) .navigationBarItems(trailing: navBarIcons) .scrollIndicators(.hidden, axes: .vertical) - .background(Color.backgroundLight) + .background(Color.huiColors.surface.pagePrimary) } private var nameLabel: some View { @@ -97,64 +123,6 @@ struct DashboardView: View { } } - @ViewBuilder - private func moduleView(course: HCourse) -> some View { - if let module = course.currentModule, let moduleItem = course.currentModuleItem { - VStack(spacing: 0) { - GeometryReader { proxy in - AsyncImage(url: course.imageURL) { image in - image.image?.resizable().scaledToFill() - } - .frame(width: proxy.size.width) - .cornerRadius(8) - } - .frame(height: 200) - .padding(.vertical, 24) - - Size24RegularTextDarkestTitle(title: module.name) - .padding(.bottom, 8) - HStack(spacing: 0) { - HStack(spacing: 4) { - Image(systemName: "document") - .resizable() - .renderingMode(.template) - .aspectRatio(contentMode: .fit) - .foregroundStyle(Color.textDark) - .frame(width: 18, height: 18) - Size12RegularTextDarkTitle(title: moduleItem.title) - .lineLimit(2) - } - Spacer() - HStack(spacing: 4) { - Image(systemName: "timer") - .resizable() - .renderingMode(.template) - .foregroundStyle(Color.textDark) - .frame(width: 14, height: 14) - Size12RegularTextDarkTitle(title: "20 Mins") - } - } - Button { - if let url = moduleItem.htmlURL { - AppEnvironment.shared.router.route(to: url, from: viewController) - } - } label: { - Text("Continue learning") - .font(.regular14) - .frame(height: 36) - .frame(maxWidth: .infinity) - .background(Color.backgroundLight) - .foregroundColor(Color.textDark) - .cornerRadius(8) - .padding(.vertical, 16) - } - } - .padding(.horizontal, 16) - .background(Color.backgroundLightest) - .cornerRadius(8) - .padding(.vertical, 16) - } - } } #if DEBUG diff --git a/Horizon/Horizon/Sources/Features/Dashboard/View/DashboardViewModel.swift b/Horizon/Horizon/Sources/Features/Dashboard/View/DashboardViewModel.swift index 72fc8886e4..5ef118c24f 100644 --- a/Horizon/Horizon/Sources/Features/Dashboard/View/DashboardViewModel.swift +++ b/Horizon/Horizon/Sources/Features/Dashboard/View/DashboardViewModel.swift @@ -66,4 +66,8 @@ final class DashboardViewModel: ObservableObject { func notificationsDidTap() {} func profileDidTap() {} + + func navigateToCourseDetails(url: URL, viewController: WeakViewController) { + router.route(to: url, from: viewController) + } } diff --git a/Horizon/Horizon/Sources/Features/HorizonTabBarController.swift b/Horizon/Horizon/Sources/Features/HorizonTabBarController.swift index 59ef214c5e..2ef67b247e 100644 --- a/Horizon/Horizon/Sources/Features/HorizonTabBarController.swift +++ b/Horizon/Horizon/Sources/Features/HorizonTabBarController.swift @@ -19,6 +19,7 @@ import Core import HorizonUI import UIKit +import SwiftUI final class HorizonTabBarController: UITabBarController, UITabBarControllerDelegate { // MARK: - Properties @@ -63,7 +64,8 @@ final class HorizonTabBarController: UITabBarController, UITabBarControllerDeleg ) let appearance = UINavigationBarAppearance() appearance.configureWithOpaqueBackground() - appearance.backgroundColor = .backgroundLight + + appearance.backgroundColor = UIColor(Color.huiColors.surface.pagePrimary) appearance.shadowImage = UIImage() appearance.shadowColor = nil diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/LearningObjectCard/HorizonUI.LearningObjectCard.Storybook.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/LearningObjectCard/HorizonUI.LearningObjectCard.Storybook.swift new file mode 100644 index 0000000000..54a9ce341c --- /dev/null +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/LearningObjectCard/HorizonUI.LearningObjectCard.Storybook.swift @@ -0,0 +1,78 @@ +// +// This file is part of Canvas. +// Copyright (C) 2024-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import SwiftUI + +extension HorizonUI.LearningObjectCard { + struct Storybook: View { + var body: some View { + ScrollView { + VStack(spacing: 20) { + HorizonUI.LearningObjectCard( + status: "Default", + moduleTitle: "Module Title Lorem Ipsum Dolor Sit Amet Adipiscing Elit So Do", + learningObjectName: "Learning Object Name Lorem Ipsum Dolor", + duration: "20 Mins", + type: "Learning Object Type", + dueDate: "10/10/2015" + ) + + HorizonUI.LearningObjectCard( + status: "Default", + moduleTitle: "Module Title Lorem Ipsum Dolor Sit Amet Adipiscing Elit So Do", + learningObjectName: "Learning Object Name Lorem Ipsum Dolor", + type: "Learning Object Type", + dueDate: "10/10/2015" + ) + + HorizonUI.LearningObjectCard( + status: "Default", + moduleTitle: "Module Title Lorem Ipsum Dolor Sit Amet Adipiscing Elit So Do", + learningObjectName: "Learning Object Name Lorem Ipsum Dolor", + duration: "20 Mins", + dueDate: "10/10/2015" + ) + + HorizonUI.LearningObjectCard( + status: "Default", + moduleTitle: "Module Title Lorem Ipsum Dolor Sit Amet Adipiscing Elit So Do", + learningObjectName: "Learning Object Name Lorem Ipsum Dolor", + duration: "20 Mins" + ) + + HorizonUI.LearningObjectCard( + status: "Default", + moduleTitle: "Module Title Lorem Ipsum Dolor Sit Amet Adipiscing Elit So Do", + learningObjectName: "Learning Object Name Lorem Ipsum Dolor" + ) + + HorizonUI.LearningObjectCard( + moduleTitle: "Module Title Lorem Ipsum Dolor Sit Amet Adipiscing Elit So Do", + learningObjectName: "Learning Object Name Lorem Ipsum Dolor" + ) + } + .padding(.horizontal, .huiSpaces.primitives.medium) + } + .navigationTitle("LearningObjectCard") + } + } +} + +#Preview { + HorizonUI.LearningObjectCard.Storybook() +} diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/LearningObjectCard/HorizonUI.LearningObjectCard.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/LearningObjectCard/HorizonUI.LearningObjectCard.swift new file mode 100644 index 0000000000..4ca0b8968c --- /dev/null +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/LearningObjectCard/HorizonUI.LearningObjectCard.swift @@ -0,0 +1,157 @@ +// +// This file is part of Canvas. +// Copyright (C) 2024-present Instructure, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +import SwiftUI + +public extension HorizonUI { + struct LearningObjectCard: View { + // MARK: - Dependencies + + private let status: String? + private let moduleTitle: String + private let learningObjectName: String + private let duration: String? + private let type: String? + private let dueDate: String? + private let onTapButton: () -> Void + + // MARK: - Init + + public init( + status: String? = nil, + moduleTitle: String, + learningObjectName: String, + duration: String? = nil, + type: String? = nil, + dueDate: String? = nil, + onTapButton: @escaping () -> Void = { } + ) { + self.status = status + self.moduleTitle = moduleTitle + self.learningObjectName = learningObjectName + self.duration = duration + self.type = type + self.dueDate = dueDate + self.onTapButton = onTapButton + } + + + public var body: some View { + VStack(alignment: .leading, spacing: .zero) { + if let status { + HorizonUI.Pill( + title: status, + style: .outline(.default), + isSmall: false, + isUppercased: true, + icon: nil + ) + } + + Text(moduleTitle) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundStyle(Color.huiColors.text.body) + .huiTypography(.p2) + .padding(.top, .huiSpaces.primitives.mediumSmall) + + Text(learningObjectName) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundStyle(Color.huiColors.surface.institution) + .huiTypography(.h3) + .padding(.top, .huiSpaces.primitives.smallMedium) + + courseInfoView() + .padding(.top, .huiSpaces.primitives.xLarge) + } + .frame(maxWidth: .infinity) + .padding(.huiSpaces.primitives.large) + .background(Color.huiColors.surface.cardPrimary) + .huiCornerRadius(level: .level2) + .huiElevation(level: .level4) + } + + private func courseInfoView() -> some View { + HStack(alignment: .bottom) { + setCoursePropertiesView() + Spacer() + iconButton + } + } + + private func setCoursePropertiesView() -> some View { + VStack(alignment: .leading, spacing: .huiSpaces.primitives.xSmall) { + if let duration { + HorizonUI.Pill( + title: duration, + style: .inline(.init(textColor: Color.huiColors.surface.institution)), + isSmall: true, + isUppercased: false, + icon: Image.huiIcons.schedule + ) + } + + if let type { + HorizonUI.Pill( + title: type, + style: .inline(.init(textColor: Color.huiColors.surface.institution)), + isSmall: true, + isUppercased: false, + icon: Image.huiIcons.textSnippet + ) + } + + if let dueDate { + HorizonUI.Pill( + title: "Due \(dueDate)", + style: .inline(.init(textColor: Color.huiColors.surface.institution)), + isSmall: true, + isUppercased: false, + icon: Image.huiIcons.calendarToday + ) + } + } + } + + // TODO: will reuse iconButton component + private var iconButton: some View { + Button { + onTapButton() + } label: { + Rectangle() + .fill(Color.huiColors.surface.institution) + .frame(width: 44, height: 44) + .huiCornerRadius(level: .level6) + .overlay { + Image.huiIcons.arrowForward + .foregroundStyle(Color.huiColors.icon.surfaceColored) + } + } + } + } +} + +#Preview { + HorizonUI.LearningObjectCard( + status: "Default", + moduleTitle: "Module Title Lorem Ipsum Dolor Sit Amet Adipiscing Elit So Do", + learningObjectName: "Learning Object Name Lorem Ipsum Dolor", + duration: "20 Mins", + type: "Learning Object Type", + dueDate: "10/10/2015" + ) +} diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Pill/HorizonUI.Pill.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Pill/HorizonUI.Pill.swift index 7b726ab0c5..83e3615374 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Pill/HorizonUI.Pill.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Pill/HorizonUI.Pill.swift @@ -57,6 +57,27 @@ public extension HorizonUI { return inline.textColor } } + + var drawBorder: Bool { + switch self { + case .outline: return true + default: return false + } + } + + var verticalSpaceForSmallSize: CGFloat { + switch self { + case .inline: return 0 + default: return .huiSpaces.primitives.xxSmall + } + } + + var horizontalSpaceForSmallSize: CGFloat { + switch self { + case .inline: return 0 + default: return .huiSpaces.primitives.xSmall + } + } } private let title: String @@ -65,8 +86,7 @@ public extension HorizonUI { private let isUppercased: Bool private let icon: Image? private let cornerRadius: CornerRadius = .level4 - private let drawBorder: Bool - +// private let drawBorder: Bool public init( title: String, style: Pill.Style = .outline(Style.Outline.default), @@ -79,12 +99,12 @@ public extension HorizonUI { self.isSmall = isSmall self.isUppercased = isUppercased self.icon = icon - - if case .outline = style { - drawBorder = true - } else { - drawBorder = false - } + +// if case .outline = style { +// drawBorder = true +// } else { +// drawBorder = false +// } } public var body: some View { @@ -99,16 +119,17 @@ public extension HorizonUI { .huiTypography(isUppercased ? .tag : .labelSmall) .foregroundStyle(style.textColor) } - .padding(.horizontal, isSmall ? .huiSpaces.primitives.xSmall : .huiSpaces.primitives.small) - .padding(.vertical, isSmall ? .huiSpaces.primitives.xxSmall : .huiSpaces.primitives.xSmall) + // TODO: Need to check with Szabolcs + .padding(.horizontal, isSmall ? style.horizontalSpaceForSmallSize : .huiSpaces.primitives.small) + .padding(.vertical, isSmall ? style.verticalSpaceForSmallSize : .huiSpaces.primitives.xSmall) .background(style.backgroundColor) .huiCornerRadius(level: cornerRadius) .huiBorder( - level: drawBorder ? .level1 : nil, + level: style.drawBorder ? .level1 : nil, color: style.borderColor, radius: cornerRadius.attributes.radius ) - .frame(minHeight: isSmall ? 25 : 33) +// .frame(minHeight: isSmall ? 25 : 33) } } } diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Spaces/HorizonUI.Spaces.Primitivies.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Spaces/HorizonUI.Spaces.Primitivies.swift index f25ccc5bc0..70895940e5 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Spaces/HorizonUI.Spaces.Primitivies.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Spaces/HorizonUI.Spaces.Primitivies.swift @@ -20,14 +20,14 @@ import Foundation public extension HorizonUI.Spaces { struct Primitives: Sendable { - let xxxSmall: CGFloat = 2 - let xxSmall: CGFloat = 4 - let xSmall: CGFloat = 8 - let smallMedium: CGFloat = 10 - let small: CGFloat = 12 - let mediumSmall: CGFloat = 16 - let medium: CGFloat = 24 - let large: CGFloat = 36 - let xLarge: CGFloat = 48 + public let xxxSmall: CGFloat = 2 + public let xxSmall: CGFloat = 4 + public let xSmall: CGFloat = 8 + public let smallMedium: CGFloat = 10 + public let small: CGFloat = 12 + public let mediumSmall: CGFloat = 16 + public let medium: CGFloat = 24 + public let large: CGFloat = 36 + public let xLarge: CGFloat = 48 } } diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Spaces/HorizonUI.Spaces.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Spaces/HorizonUI.Spaces.swift index a277607738..f979204f32 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Spaces/HorizonUI.Spaces.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Spaces/HorizonUI.Spaces.swift @@ -21,7 +21,7 @@ import Foundation public extension HorizonUI { struct Spaces: Sendable { fileprivate init() {} - let primitives = Primitives() + public let primitives = Primitives() } static let spaces = HorizonUI.Spaces() diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Typography/HorizonUI.Typography.Storybook.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Typography/HorizonUI.Typography.Storybook.swift index 5332edb256..80bb2496db 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Typography/HorizonUI.Typography.Storybook.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Typography/HorizonUI.Typography.Storybook.swift @@ -45,7 +45,7 @@ public extension HorizonUI.Typography { } extension HorizonUI.Typography.Name: Identifiable { - var id: Self { self } + public var id: Self { self } } #Preview { diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Typography/HorizonUI.Typography.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Typography/HorizonUI.Typography.swift index c0ea25b0fe..0f1a7df759 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Typography/HorizonUI.Typography.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Foundation/Typography/HorizonUI.Typography.swift @@ -20,7 +20,7 @@ import SwiftUI public extension HorizonUI { struct Typography: ViewModifier { - enum Name: CaseIterable { + public enum Name: CaseIterable { case h1 case h2 case h3 @@ -100,13 +100,14 @@ public extension HorizonUI { public func body(content: Content) -> some View{ content .font(name.font) - .lineSpacing(name.lineSpacing) + // TODO: Need to check with Szabolcs +// .lineSpacing(name.lineSpacing) .tracking(name.letterSpacing) } } } -extension View { +public extension View { func huiTypography(_ name: HorizonUI.Typography.Name) -> some View { modifier(HorizonUI.Typography(name)) } diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift index 05f0c0af18..da8e65a4d5 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Storybook.swift @@ -101,7 +101,9 @@ public struct Storybook: View { NavigationLink {} label: { Text("Inputs and Interactive Fields").tint(Color.black) } - NavigationLink {} label: { + NavigationLink { + HorizonUI.LearningObjectCard.Storybook() + } label: { Text("Cards").tint(Color.black) } NavigationLink {} label: { From 33e338daa8978618b3cacd3d0642d73af91484cc Mon Sep 17 00:00:00 2001 From: Ahmed-Naguib93 Date: Fri, 20 Dec 2024 15:14:12 +0200 Subject: [PATCH 2/2] Fix code style --- .../Sources/Features/Dashboard/View/DashboardView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Horizon/Horizon/Sources/Features/Dashboard/View/DashboardView.swift b/Horizon/Horizon/Sources/Features/Dashboard/View/DashboardView.swift index 0849286c16..65fe2992bb 100644 --- a/Horizon/Horizon/Sources/Features/Dashboard/View/DashboardView.swift +++ b/Horizon/Horizon/Sources/Features/Dashboard/View/DashboardView.swift @@ -31,7 +31,10 @@ struct DashboardView: View { var body: some View { InstUI.BaseScreen( state: viewModel.state, - config: .init(refreshable: true, loaderBackgroundColor: .huiColors.surface.pagePrimary) + config: .init( + refreshable: true, + loaderBackgroundColor: .huiColors.surface.pagePrimary + ) ) { _ in LazyVStack(spacing: .zero) { ForEach(viewModel.courses) { course in