Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-02-10 20:24:24 -06:00
parent 79d69a5495
commit 0b71f5ea65
5 changed files with 104 additions and 36 deletions

View File

@ -3,7 +3,6 @@ import Foundation
enum AppTab: String, CaseIterable, Hashable, Identifiable { enum AppTab: String, CaseIterable, Hashable, Identifiable {
case cards case cards
case contacts case contacts
case widgets
case settings case settings
var id: String { rawValue } var id: String { rawValue }

View File

@ -590,6 +590,10 @@
}, },
"Scheduling" : { "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" : { "Select a color" : {
@ -621,6 +625,7 @@
} }
}, },
"Share using widgets on your phone or watch" : { "Share using widgets on your phone or watch" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "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" : { "Shared With" : {
}, },
@ -916,6 +925,10 @@
}, },
"Website URL" : { "Website URL" : {
},
"Widgets on iPhone and Watch" : {
"comment" : "A title for a view that showcases widgets for iPhone and watch faces.",
"isCommentAutoGenerated" : true
}, },
"Work" : { "Work" : {

View File

@ -28,10 +28,6 @@ struct RootTabView: View {
ContactsView() ContactsView()
} }
Tab(String.localized("Widgets"), systemImage: "square.grid.2x2", value: AppTab.widgets) {
WidgetsView()
}
Tab(String.localized("Settings"), systemImage: "gearshape", value: AppTab.settings) { Tab(String.localized("Settings"), systemImage: "gearshape", value: AppTab.settings) {
SettingsView() SettingsView()
} }

View File

@ -128,6 +128,16 @@ struct SettingsView: View {
.foregroundStyle(Color.AppText.secondary) .foregroundStyle(Color.AppText.secondary)
} }
} }
SettingsDivider(color: AppBorder.subtle)
SettingsNavigationRow(
title: String.localized("Widgets"),
subtitle: "Phone and watch preview",
backgroundColor: .clear
) {
WidgetsView()
}
} }
} }
} }

View File

@ -6,24 +6,31 @@ struct WidgetsView: View {
@Environment(AppState.self) private var appState @Environment(AppState.self) private var appState
var body: some View { var body: some View {
NavigationStack { ZStack {
LinearGradient(
colors: [Color.AppBackground.base, Color.AppBackground.accent],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
ScrollView { ScrollView {
VStack(spacing: Design.Spacing.large) { VStack(spacing: Design.Spacing.large) {
Text("Share using widgets on your phone or watch") WidgetsHeroCard()
.typography(.title2)
.bold()
.foregroundStyle(Color.Text.primary)
if let card = appState.cardStore.selectedCard { if let card = appState.cardStore.selectedCard {
WidgetPreviewCardView(card: card) WidgetPreviewCardView(card: card)
} else {
WidgetsEmptyStateCard()
} }
} }
.padding(.horizontal, Design.Spacing.large) .padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.xLarge) .padding(.vertical, Design.Spacing.xLarge)
} }
.background(Color.AppBackground.base) .scrollIndicators(.hidden)
.navigationTitle(String.localized("Widgets"))
} }
.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 { private struct PhoneWidgetPreview: View {
let card: BusinessCard let card: BusinessCard
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) { WidgetSurfaceCard {
Text("Phone Widget") Label("Phone Widget", systemImage: "iphone")
.typography(.heading) .styled(.headingEmphasis)
.bold()
.foregroundStyle(Color.Text.primary)
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
QRCodeView(payload: card.vCardPayload) QRCodeView(payload: card.vCardPayload)
.frame(width: Design.CardSize.widgetPhoneHeight, height: Design.CardSize.widgetPhoneHeight) .frame(width: Design.CardSize.widgetPhoneHeight, height: Design.CardSize.widgetPhoneHeight)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text(card.fullName) Text(card.fullName)
.typography(.heading) .styled(.subheadingEmphasis)
.foregroundStyle(Color.Text.primary) .lineLimit(2)
Text(card.role) Text(card.role)
.typography(.subheading) .styled(.caption, emphasis: .secondary)
.foregroundStyle(Color.Text.secondary) .lineLimit(1)
Text("Tap to share") Text("Tap to share")
.typography(.caption) .styled(.caption2, emphasis: .tertiary)
.foregroundStyle(Color.Text.secondary)
} }
.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 let card: BusinessCard
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) { WidgetSurfaceCard {
Text("Watch Widget") Label("Watch Widget", systemImage: "applewatch")
.typography(.heading) .styled(.headingEmphasis)
.bold()
.foregroundStyle(Color.Text.primary)
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
QRCodeView(payload: card.vCardPayload) QRCodeView(payload: card.vCardPayload)
.frame(width: Design.CardSize.widgetWatchSize, height: Design.CardSize.widgetWatchSize) .frame(width: Design.CardSize.widgetWatchSize, height: Design.CardSize.widgetWatchSize)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text("Ready to scan") Text("Ready to scan")
.typography(.subheading) .styled(.subheadingEmphasis, emphasis: .secondary)
.foregroundStyle(Color.Text.secondary)
Text("Open on Apple Watch") Text("Open on Apple Watch")
.typography(.caption) .styled(.caption, emphasis: .tertiary)
.foregroundStyle(Color.Text.secondary)
} }
.frame(maxWidth: .infinity, alignment: .leading)
} }
} }
}
}
private struct WidgetSurfaceCard<Content: View>: 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) .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)) .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
)
} }
} }