From 0b71f5ea657ffaa55177448dcb6afa4be5a43727 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 10 Feb 2026 20:24:24 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- BusinessCard/Models/AppTab.swift | 1 - BusinessCard/Resources/Localizable.xcstrings | 13 +++ BusinessCard/Views/RootTabView.swift | 4 - BusinessCard/Views/SettingsView.swift | 10 ++ BusinessCard/Views/WidgetsView.swift | 112 ++++++++++++++----- 5 files changed, 104 insertions(+), 36 deletions(-) diff --git a/BusinessCard/Models/AppTab.swift b/BusinessCard/Models/AppTab.swift index 1794e88..99322d8 100644 --- a/BusinessCard/Models/AppTab.swift +++ b/BusinessCard/Models/AppTab.swift @@ -3,7 +3,6 @@ import Foundation enum AppTab: String, CaseIterable, Hashable, Identifiable { case cards case contacts - case widgets case settings var id: String { rawValue } diff --git a/BusinessCard/Resources/Localizable.xcstrings b/BusinessCard/Resources/Localizable.xcstrings index 5753701..e12a51f 100644 --- a/BusinessCard/Resources/Localizable.xcstrings +++ b/BusinessCard/Resources/Localizable.xcstrings @@ -590,6 +590,10 @@ }, "Scheduling" : { + }, + "Select a business card from the Cards tab to preview widget layouts." : { + "comment" : "A message displayed when no business card is selected to preview widget layouts.", + "isCommentAutoGenerated" : true }, "Select a color" : { @@ -621,6 +625,7 @@ } }, "Share using widgets on your phone or watch" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -665,6 +670,10 @@ } } }, + "Share your card quickly from your home screen and your watch face." : { + "comment" : "A description of how users can share their business cards on their iPhone and watch faces.", + "isCommentAutoGenerated" : true + }, "Shared With" : { }, @@ -916,6 +925,10 @@ }, "Website URL" : { + }, + "Widgets on iPhone and Watch" : { + "comment" : "A title for a view that showcases widgets for iPhone and watch faces.", + "isCommentAutoGenerated" : true }, "Work" : { diff --git a/BusinessCard/Views/RootTabView.swift b/BusinessCard/Views/RootTabView.swift index a93e4b7..58685a2 100644 --- a/BusinessCard/Views/RootTabView.swift +++ b/BusinessCard/Views/RootTabView.swift @@ -28,10 +28,6 @@ struct RootTabView: View { ContactsView() } - Tab(String.localized("Widgets"), systemImage: "square.grid.2x2", value: AppTab.widgets) { - WidgetsView() - } - Tab(String.localized("Settings"), systemImage: "gearshape", value: AppTab.settings) { SettingsView() } diff --git a/BusinessCard/Views/SettingsView.swift b/BusinessCard/Views/SettingsView.swift index e535c6b..13512fc 100644 --- a/BusinessCard/Views/SettingsView.swift +++ b/BusinessCard/Views/SettingsView.swift @@ -128,6 +128,16 @@ struct SettingsView: View { .foregroundStyle(Color.AppText.secondary) } } + + SettingsDivider(color: AppBorder.subtle) + + SettingsNavigationRow( + title: String.localized("Widgets"), + subtitle: "Phone and watch preview", + backgroundColor: .clear + ) { + WidgetsView() + } } } } diff --git a/BusinessCard/Views/WidgetsView.swift b/BusinessCard/Views/WidgetsView.swift index 61c41f4..0e82629 100644 --- a/BusinessCard/Views/WidgetsView.swift +++ b/BusinessCard/Views/WidgetsView.swift @@ -6,24 +6,31 @@ struct WidgetsView: View { @Environment(AppState.self) private var appState var body: some View { - NavigationStack { + ZStack { + LinearGradient( + colors: [Color.AppBackground.base, Color.AppBackground.accent], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .ignoresSafeArea() + ScrollView { VStack(spacing: Design.Spacing.large) { - Text("Share using widgets on your phone or watch") - .typography(.title2) - .bold() - .foregroundStyle(Color.Text.primary) + WidgetsHeroCard() if let card = appState.cardStore.selectedCard { WidgetPreviewCardView(card: card) + } else { + WidgetsEmptyStateCard() } } .padding(.horizontal, Design.Spacing.large) .padding(.vertical, Design.Spacing.xLarge) } - .background(Color.AppBackground.base) - .navigationTitle(String.localized("Widgets")) + .scrollIndicators(.hidden) } + .navigationTitle(String.localized("Widgets")) + .navigationBarTitleDisplayMode(.inline) } } @@ -38,36 +45,56 @@ private struct WidgetPreviewCardView: View { } } +private struct WidgetsHeroCard: View { + var body: some View { + WidgetSurfaceCard { + Label("Widgets on iPhone and Watch", systemImage: "square.grid.2x2.fill") + .styled(.headingEmphasis) + + Text("Share your card quickly from your home screen and your watch face.") + .styled(.subheading, emphasis: .secondary) + } + } +} + +private struct WidgetsEmptyStateCard: View { + var body: some View { + WidgetSurfaceCard { + Label("No card selected", systemImage: "person.crop.rectangle") + .styled(.headingEmphasis) + + Text("Select a business card from the Cards tab to preview widget layouts.") + .styled(.subheading, emphasis: .secondary) + } + } +} + private struct PhoneWidgetPreview: View { let card: BusinessCard var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.medium) { - Text("Phone Widget") - .typography(.heading) - .bold() - .foregroundStyle(Color.Text.primary) + WidgetSurfaceCard { + Label("Phone Widget", systemImage: "iphone") + .styled(.headingEmphasis) HStack(spacing: Design.Spacing.medium) { QRCodeView(payload: card.vCardPayload) .frame(width: Design.CardSize.widgetPhoneHeight, height: Design.CardSize.widgetPhoneHeight) + .clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { Text(card.fullName) - .typography(.heading) - .foregroundStyle(Color.Text.primary) + .styled(.subheadingEmphasis) + .lineLimit(2) Text(card.role) - .typography(.subheading) - .foregroundStyle(Color.Text.secondary) + .styled(.caption, emphasis: .secondary) + .lineLimit(1) Text("Tap to share") - .typography(.caption) - .foregroundStyle(Color.Text.secondary) + .styled(.caption2, emphasis: .tertiary) } + .frame(maxWidth: .infinity, alignment: .leading) } } - .padding(Design.Spacing.large) - .background(Color.AppBackground.elevated) - .clipShape(.rect(cornerRadius: Design.CornerRadius.large)) } } @@ -75,29 +102,52 @@ private struct WatchWidgetPreview: View { let card: BusinessCard var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.medium) { - Text("Watch Widget") - .typography(.heading) - .bold() - .foregroundStyle(Color.Text.primary) + WidgetSurfaceCard { + Label("Watch Widget", systemImage: "applewatch") + .styled(.headingEmphasis) HStack(spacing: Design.Spacing.medium) { QRCodeView(payload: card.vCardPayload) .frame(width: Design.CardSize.widgetWatchSize, height: Design.CardSize.widgetWatchSize) + .clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { Text("Ready to scan") - .typography(.subheading) - .foregroundStyle(Color.Text.secondary) + .styled(.subheadingEmphasis, emphasis: .secondary) Text("Open on Apple Watch") - .typography(.caption) - .foregroundStyle(Color.Text.secondary) + .styled(.caption, emphasis: .tertiary) } + .frame(maxWidth: .infinity, alignment: .leading) } } + } +} + +private struct WidgetSurfaceCard: View { + let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + VStack(alignment: .leading, spacing: Design.Spacing.medium) { + content + } .padding(Design.Spacing.large) - .background(Color.AppBackground.elevated) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.AppBackground.secondary.opacity(Design.Opacity.almostFull)) .clipShape(.rect(cornerRadius: Design.CornerRadius.large)) + .overlay { + RoundedRectangle(cornerRadius: Design.CornerRadius.large) + .strokeBorder(AppBorder.subtle, lineWidth: Design.LineWidth.thin) + } + .shadow( + color: Color.AppText.tertiary.opacity(Design.Opacity.subtle), + radius: Design.Shadow.radiusSmall, + x: Design.Shadow.offsetNone, + y: Design.Shadow.offsetSmall + ) } }