mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Give the Mac app Apple's Liquid Glass chrome with a system sidebar and glass cards.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
df89cd1984
commit
1569df7e13
5 changed files with 192 additions and 120 deletions
|
|
@ -49,12 +49,12 @@ struct PrimaryButton: View {
|
|||
.font(.body.weight(.semibold))
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
#if os(iOS)
|
||||
.buttonStyle(.glassProminent)
|
||||
#if os(macOS)
|
||||
.controlSize(.extraLarge)
|
||||
#else
|
||||
.buttonStyle(.borderedProminent)
|
||||
#endif
|
||||
.controlSize(.large)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ struct GlassCard<Content: View>: View {
|
|||
content()
|
||||
.padding(16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 22, style: .continuous))
|
||||
.glassEffect(.regular, in: .rect(cornerRadius: 26, style: .continuous))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -288,6 +288,33 @@ extension View {
|
|||
.padding()
|
||||
#endif
|
||||
}
|
||||
|
||||
/// macOS content lists use the inset style so they sit under the floating
|
||||
/// glass sidebar instead of looking like a second source list.
|
||||
@ViewBuilder
|
||||
func appListStyle() -> some View {
|
||||
#if os(macOS)
|
||||
listStyle(.inset)
|
||||
#else
|
||||
self
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func appFormStyle() -> some View {
|
||||
#if os(macOS)
|
||||
formStyle(.grouped)
|
||||
.scenePadding()
|
||||
#else
|
||||
self
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Pin a primary action above scrolling content using the system scroll
|
||||
/// edge effect instead of an opaque bar.
|
||||
func glassBottomBar<Bar: View>(@ViewBuilder _ bar: () -> Bar) -> some View {
|
||||
safeAreaBar(edge: .bottom, spacing: 8, content: bar)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bottom action area used by exercises and lesson slides.
|
||||
|
|
@ -304,7 +331,6 @@ struct BottomAction<Extra: View>: View {
|
|||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 4)
|
||||
.background(.bar)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -322,19 +348,20 @@ func subjectSymbol(_ icon: String) -> String {
|
|||
struct SubjectIcon: View {
|
||||
let icon: String
|
||||
var open = true
|
||||
var size: CGFloat = 26
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch icon {
|
||||
case "de": GermanFlag(size: 26)
|
||||
case "cz": CzechFlag(size: 26)
|
||||
case "de": GermanFlag(size: size)
|
||||
case "cz": CzechFlag(size: size)
|
||||
default:
|
||||
Image(systemName: subjectSymbol(icon))
|
||||
.font(.title3)
|
||||
.font(.system(size: size * 0.72, weight: .semibold))
|
||||
.foregroundStyle(open ? AnyShapeStyle(Color.accentColor) : AnyShapeStyle(.tertiary))
|
||||
}
|
||||
}
|
||||
.frame(width: 28, height: 28)
|
||||
.frame(width: size, height: size)
|
||||
.opacity(open ? 1 : 0.6)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ struct ExerciseScaffold<Content: View>: View {
|
|||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.detailScreen(title, subtitle: sub)
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
.glassBottomBar {
|
||||
BottomAction(label: action, action: onAction) {
|
||||
if !feedback.isEmpty {
|
||||
FeedbackLine(text: feedback, kind: kind, palette: p)
|
||||
|
|
@ -601,7 +601,7 @@ private struct VocabEx: View {
|
|||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.detailScreen(title, subtitle: sub)
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
.glassBottomBar {
|
||||
if index < cards.count {
|
||||
BottomAction(label: vm.tr("check"), action: { check(cards[index]) }) { EmptyView() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -741,12 +741,12 @@ private struct VlsmEx: View {
|
|||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.detailScreen(vm.tr("net_ex_title"))
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
.glassBottomBar {
|
||||
if !fb.0.isEmpty {
|
||||
FeedbackLine(text: fb.0, kind: fb.1, palette: p)
|
||||
.padding()
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(.bar)
|
||||
}
|
||||
}
|
||||
.animation(.default, value: fb.0)
|
||||
|
|
@ -842,7 +842,7 @@ struct LitQuizScreen: View {
|
|||
.exerciseContent()
|
||||
}
|
||||
.detailScreen(vm.tr(book.str("quizTitle")), subtitle: vm.tr(book.str("quizSub")))
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
.glassBottomBar {
|
||||
if finished {
|
||||
BottomAction(label: vm.tr("lit_again"), action: {
|
||||
idx = 0; score = 0; answered = false; finished = false; picked = -1
|
||||
|
|
|
|||
|
|
@ -4,6 +4,100 @@ struct PracticeHome: View {
|
|||
@ObservedObject var vm: AppModel
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
macHome
|
||||
#else
|
||||
iosHome
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private var macHome: some View {
|
||||
let sum = summarize(vm.content, vm.progress)
|
||||
let pct = sum.totalEx == 0 ? 0 : Double(sum.doneEx) / Double(sum.totalEx)
|
||||
return ScrollView {
|
||||
VStack(alignment: .leading, spacing: 28) {
|
||||
if vm.showChangelog {
|
||||
ChangelogCard(vm: vm)
|
||||
}
|
||||
GlassCard {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(vm.tr("welcome_title"))
|
||||
.font(.largeTitle.bold())
|
||||
Text(vm.tr(Self.welcomeBodyKey))
|
||||
.font(.title3)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
ProgressView(value: pct)
|
||||
Text(vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(vm.tr("subjects_title"))
|
||||
.font(.title2.weight(.semibold))
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 220), spacing: 16)], spacing: 16) {
|
||||
ForEach(Array(vm.content.subjects.enumerated()), id: \.offset) { _, s in
|
||||
subjectTile(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(28)
|
||||
.frame(maxWidth: 920, alignment: .leading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.backgroundExtensionEffect()
|
||||
.navigationTitle("Maturita")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func subjectTile(_ s: J) -> some View {
|
||||
let open = s.bool("open")
|
||||
let dest = s.strOrNull("target").flatMap(routeFromPage)
|
||||
let part = subjectSum(vm, s)
|
||||
let progress = open && part.totalEx > 0 ? Double(part.doneEx) / Double(part.totalEx) : 0
|
||||
Group {
|
||||
if open, let dest, dest != .subjects, dest != .welcome {
|
||||
NavigationLink(value: dest) {
|
||||
subjectTileBody(s, open: true, progress: progress, part: part)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else {
|
||||
subjectTileBody(s, open: false, progress: 0, part: part)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func subjectTileBody(_ s: J, open: Bool, progress: Double, part: ProgressSum) -> some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
SubjectIcon(icon: s.str("icon"), open: open, size: 36)
|
||||
Text(vm.tr(s.str("key")))
|
||||
.font(.headline)
|
||||
.foregroundStyle(open ? .primary : .secondary)
|
||||
.multilineTextAlignment(.leading)
|
||||
Spacer(minLength: 0)
|
||||
if open && part.totalEx > 0 {
|
||||
ProgressView(value: progress)
|
||||
Text(vm.fmt("stats_ex_fmt", part.doneEx, part.totalEx))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.monospacedDigit()
|
||||
} else if !open {
|
||||
Text(vm.tr("stats_locked"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(18)
|
||||
.frame(maxWidth: .infinity, minHeight: 148, alignment: .topLeading)
|
||||
.glassEffect(.regular, in: .rect(cornerRadius: 24, style: .continuous))
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
private var iosHome: some View {
|
||||
let sum = summarize(vm.content, vm.progress)
|
||||
let pct = sum.totalEx == 0 ? 0 : Double(sum.doneEx) / Double(sum.totalEx)
|
||||
List {
|
||||
|
|
@ -50,17 +144,7 @@ struct PracticeHome: View {
|
|||
}
|
||||
}
|
||||
.navigationTitle("Maturita")
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
#endif
|
||||
}
|
||||
|
||||
private static var welcomeBodyKey: String {
|
||||
#if os(macOS)
|
||||
"welcome_body_macos"
|
||||
#else
|
||||
"welcome_body_ios"
|
||||
#endif
|
||||
}
|
||||
|
||||
private func subjectLabel(_ s: J) -> some View {
|
||||
|
|
@ -75,6 +159,15 @@ struct PracticeHome: View {
|
|||
SubjectIcon(icon: s.str("icon"), open: s.bool("open"))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private static var welcomeBodyKey: String {
|
||||
#if os(macOS)
|
||||
"welcome_body_macos"
|
||||
#else
|
||||
"welcome_body_ios"
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
struct ChangelogCard: View {
|
||||
|
|
@ -99,7 +192,7 @@ struct ChangelogCard: View {
|
|||
ChangelogLines(index: index, lines: changelogLines(block, langKey))
|
||||
}
|
||||
Button(vm.tr("changelog_dismiss")) { vm.dismissChangelog() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.buttonStyle(.glassProminent)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
|
|
@ -171,6 +264,7 @@ struct SettingsScreen: View {
|
|||
}
|
||||
}
|
||||
.navigationTitle(vm.tr("settings_title"))
|
||||
.appFormStyle()
|
||||
}
|
||||
|
||||
private var modeBinding: Binding<ColorMode> {
|
||||
|
|
@ -237,6 +331,7 @@ struct StatsScreen: View {
|
|||
}
|
||||
}
|
||||
.navigationTitle(vm.tr("stats_title"))
|
||||
.appListStyle()
|
||||
}
|
||||
|
||||
private func stat(_ label: String, _ value: String) -> some View {
|
||||
|
|
@ -296,8 +391,9 @@ struct SearchScreen: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(vm.tr("search_placeholder"))
|
||||
.navigationTitle(vm.tr("search"))
|
||||
.searchable(text: $vm.searchQuery, prompt: vm.tr("search_placeholder"))
|
||||
.appListStyle()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -372,6 +468,7 @@ struct RoadmapScreen: View {
|
|||
}
|
||||
.navigationTitle(vm.tr("roadmap_title"))
|
||||
.navigationSubtitle(vm.tr("roadmap_sub"))
|
||||
.appListStyle()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -400,6 +497,7 @@ struct UnitMapScreen: View {
|
|||
}
|
||||
.navigationTitle(u.str("title"))
|
||||
.navigationSubtitle(vm.tr(u.str("sub")))
|
||||
.appListStyle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -429,6 +527,7 @@ struct NetYearsScreen: View {
|
|||
}
|
||||
.navigationTitle(vm.tr("net_years_title"))
|
||||
.navigationSubtitle(vm.tr("net_years_sub"))
|
||||
.appListStyle()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -456,6 +555,7 @@ struct LessonListScreen: View {
|
|||
}
|
||||
.navigationTitle(title)
|
||||
.navigationSubtitle(subtitle)
|
||||
.appListStyle()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -483,7 +583,7 @@ struct SlidesScreen: View {
|
|||
#endif
|
||||
}
|
||||
.detailScreen(title, subtitle: subtitle)
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
.glassBottomBar {
|
||||
VStack(spacing: 10) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(slides.indices, id: \.self) { i in
|
||||
|
|
@ -501,11 +601,7 @@ struct SlidesScreen: View {
|
|||
Image(systemName: "chevron.left")
|
||||
.frame(minWidth: 24)
|
||||
}
|
||||
#if os(iOS)
|
||||
.buttonStyle(.glass)
|
||||
#else
|
||||
.buttonStyle(.bordered)
|
||||
#endif
|
||||
.controlSize(.large)
|
||||
}
|
||||
if idx < slides.count - 1 {
|
||||
|
|
@ -522,7 +618,6 @@ struct SlidesScreen: View {
|
|||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 4)
|
||||
.background(.bar)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -568,6 +663,7 @@ struct CzechMapScreen: View {
|
|||
}
|
||||
.navigationTitle(vm.tr("Český jazyk a literatura"))
|
||||
.navigationSubtitle(vm.tr("czech_sub"))
|
||||
.appListStyle()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -591,6 +687,7 @@ struct BookListScreen: View {
|
|||
}
|
||||
.navigationTitle(vm.tr("Maturitní četba"))
|
||||
.navigationSubtitle(vm.tr("reading_sub"))
|
||||
.appListStyle()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -631,6 +728,7 @@ struct BookScreen: View {
|
|||
}
|
||||
.navigationTitle(b.str("title"))
|
||||
.navigationSubtitle(vm.tr(b.str("subKey")))
|
||||
.appListStyle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ struct MaturitaApp: App {
|
|||
}
|
||||
.defaultSize(width: 1100, height: 720)
|
||||
.windowToolbarStyle(.unified)
|
||||
.windowStyle(.automatic)
|
||||
.commands {
|
||||
CommandGroup(replacing: .newItem) {}
|
||||
CommandGroup(after: .sidebar) {
|
||||
|
|
@ -35,6 +36,7 @@ struct MaturitaApp: App {
|
|||
|
||||
Settings {
|
||||
SettingsScreen(vm: vm)
|
||||
.formStyle(.grouped)
|
||||
.frame(minWidth: 420, minHeight: 460)
|
||||
.tint(vm.palette.tint)
|
||||
.preferredColorScheme(vm.mode == .dark ? .dark : .light)
|
||||
|
|
@ -46,61 +48,53 @@ struct MacRoot: View {
|
|||
@ObservedObject var vm: AppModel
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
MacSidebar(vm: vm)
|
||||
.navigationSplitViewColumnWidth(min: 200, ideal: 240, max: 320)
|
||||
} detail: {
|
||||
MacDetail(vm: vm)
|
||||
}
|
||||
.navigationSplitViewStyle(.balanced)
|
||||
.tint(vm.palette.tint)
|
||||
.preferredColorScheme(vm.mode == .dark ? .dark : .light)
|
||||
.overlay(alignment: .top) {
|
||||
if showsUpdateAccessory {
|
||||
UpdateBanner(vm: vm)
|
||||
.padding(12)
|
||||
tabs
|
||||
.tint(vm.palette.tint)
|
||||
.preferredColorScheme(vm.mode == .dark ? .dark : .light)
|
||||
.overlay(alignment: .top) {
|
||||
if showsUpdateAccessory {
|
||||
UpdateBanner(vm: vm)
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear { vm.checkUpdate(false) }
|
||||
.frame(minWidth: 880, minHeight: 540)
|
||||
.onAppear { vm.checkUpdate(false) }
|
||||
.frame(minWidth: 880, minHeight: 540)
|
||||
}
|
||||
|
||||
private var showsUpdateAccessory: Bool {
|
||||
vm.update.canInstall || vm.update.status == "downloading" || vm.update.status == "staged"
|
||||
}
|
||||
}
|
||||
|
||||
private struct MacDetail: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch vm.tab {
|
||||
case .practice:
|
||||
private var tabs: some View {
|
||||
TabView(selection: $vm.tab) {
|
||||
Tab(vm.tr("search_home"), systemImage: "square.grid.2x2.fill", value: AppTab.practice) {
|
||||
NavigationStack(path: $vm.practicePath) {
|
||||
PracticeHome(vm: vm)
|
||||
.navigationDestination(for: Route.self) { route in
|
||||
RouteDestination(vm: vm, route: route)
|
||||
}
|
||||
}
|
||||
case .progress:
|
||||
}
|
||||
Tab(vm.tr("stats"), systemImage: "chart.bar.fill", value: AppTab.progress) {
|
||||
NavigationStack {
|
||||
StatsScreen(vm: vm)
|
||||
}
|
||||
case .search:
|
||||
NavigationStack {
|
||||
SearchScreen(vm: vm)
|
||||
}
|
||||
case .settings:
|
||||
}
|
||||
Tab(vm.tr("settings"), systemImage: "gearshape.fill", value: AppTab.settings) {
|
||||
NavigationStack {
|
||||
SettingsScreen(vm: vm)
|
||||
}
|
||||
}
|
||||
Tab(vm.tr("search"), systemImage: "magnifyingglass", value: AppTab.search, role: .search) {
|
||||
NavigationStack {
|
||||
SearchScreen(vm: vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.sidebarAdaptable)
|
||||
.toolbar {
|
||||
MacWindowToolbar(vm: vm)
|
||||
}
|
||||
.toolbarBackgroundVisibility(.visible, for: .windowToolbar)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +116,11 @@ private struct MacWindowToolbar: ToolbarContent {
|
|||
Label(vm.tr("stats"), systemImage: "chart.bar.fill")
|
||||
}
|
||||
.help(vm.tr("stats"))
|
||||
}
|
||||
|
||||
ToolbarSpacer(.fixed, placement: .primaryAction)
|
||||
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
vm.tab = .settings
|
||||
} label: {
|
||||
|
|
@ -133,58 +131,6 @@ private struct MacWindowToolbar: ToolbarContent {
|
|||
}
|
||||
}
|
||||
|
||||
private struct MacSidebar: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
|
||||
var body: some View {
|
||||
List(selection: $vm.tab) {
|
||||
Section {
|
||||
Label(vm.tr("search_home"), systemImage: "square.grid.2x2.fill")
|
||||
.tag(AppTab.practice)
|
||||
Label(vm.tr("stats"), systemImage: "chart.bar.fill")
|
||||
.tag(AppTab.progress)
|
||||
Label(vm.tr("search_placeholder"), systemImage: "magnifyingglass")
|
||||
.tag(AppTab.search)
|
||||
Label(vm.tr("settings"), systemImage: "gearshape.fill")
|
||||
.tag(AppTab.settings)
|
||||
}
|
||||
|
||||
Section(vm.tr("subjects_title")) {
|
||||
ForEach(Array(vm.content.subjects.enumerated()), id: \.offset) { _, s in
|
||||
let open = s.bool("open")
|
||||
let dest = s.strOrNull("target").flatMap(routeFromPage)
|
||||
if open, let dest, dest != .subjects, dest != .welcome {
|
||||
Button {
|
||||
vm.openFromSearch(dest.page)
|
||||
} label: {
|
||||
subjectLabel(s)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else {
|
||||
subjectLabel(s)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
.navigationTitle("maturita.c")
|
||||
}
|
||||
|
||||
private func subjectLabel(_ s: J) -> some View {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(vm.tr(s.str("key")))
|
||||
if !s.bool("open") {
|
||||
Text(vm.tr("stats_locked")).font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
} icon: {
|
||||
SubjectIcon(icon: s.str("icon"), open: s.bool("open"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct UpdateBanner: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
|
||||
|
|
@ -196,13 +142,14 @@ private struct UpdateBanner: View {
|
|||
Spacer()
|
||||
if vm.update.canInstall {
|
||||
Button(vm.tr("update_install")) { vm.installUpdate() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.buttonStyle(.glassProminent)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.shadow(color: .black.opacity(0.12), radius: 8, y: 2)
|
||||
.frame(maxWidth: 640)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.glassEffect(.regular, in: .rect(cornerRadius: 18, style: .continuous))
|
||||
.frame(maxWidth: 560)
|
||||
}
|
||||
|
||||
private var bannerText: String {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue