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..65fe2992bb 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,58 @@ 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 +126,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: {