Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
4bec90cc31
commit
3807ba38dd
@ -20,7 +20,7 @@ extension Design {
|
||||
static let avatarLarge: CGFloat = 80
|
||||
static let avatarOverlap: CGFloat = 40
|
||||
static let logoSize: CGFloat = 64
|
||||
static let bannerHeight: CGFloat = 140
|
||||
static let bannerHeight: CGFloat = 240
|
||||
static let qrSize: CGFloat = 200
|
||||
static let qrSizeLarge: CGFloat = 260
|
||||
static let colorSwatchSize: CGFloat = 40
|
||||
|
||||
@ -49,6 +49,7 @@ private struct CardBannerView: View {
|
||||
}
|
||||
}
|
||||
.frame(height: Design.CardSize.bannerHeight)
|
||||
.clipped()
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,27 +59,30 @@ private struct ProfileBannerContent: View {
|
||||
let card: BusinessCard
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let photoData = card.photoData, let uiImage = UIImage(data: photoData) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.clipped()
|
||||
} else {
|
||||
LinearGradient(
|
||||
colors: [card.theme.primaryColor, card.theme.secondaryColor],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
if let photoData = card.photoData, let uiImage = UIImage(data: photoData) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
.clipped()
|
||||
} else {
|
||||
LinearGradient(
|
||||
colors: [card.theme.primaryColor, card.theme.secondaryColor],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
|
||||
VStack(spacing: Design.Spacing.xSmall) {
|
||||
Image(systemName: "person.fill")
|
||||
.font(.system(size: Design.BaseFontSize.display, weight: .bold))
|
||||
Text("Profile")
|
||||
.font(.title3)
|
||||
.bold()
|
||||
VStack(spacing: Design.Spacing.xSmall) {
|
||||
Image(systemName: "person.fill")
|
||||
.font(.system(size: Design.BaseFontSize.display, weight: .bold))
|
||||
Text("Profile")
|
||||
.font(.title3)
|
||||
.bold()
|
||||
}
|
||||
.foregroundStyle(card.theme.textColor.opacity(Design.Opacity.medium))
|
||||
}
|
||||
.foregroundStyle(card.theme.textColor.opacity(Design.Opacity.medium))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -90,27 +94,30 @@ private struct LogoBannerContent: View {
|
||||
let card: BusinessCard
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
colors: [card.theme.primaryColor, card.theme.secondaryColor],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
colors: [card.theme.primaryColor, card.theme.secondaryColor],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
|
||||
if let logoData = card.logoData, let uiImage = UIImage(data: logoData) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.clipped()
|
||||
} else {
|
||||
VStack(spacing: Design.Spacing.xSmall) {
|
||||
Image(systemName: "building.2.fill")
|
||||
.font(.system(size: Design.BaseFontSize.display, weight: .bold))
|
||||
Text("Logo")
|
||||
.font(.title3)
|
||||
.bold()
|
||||
if let logoData = card.logoData, let uiImage = UIImage(data: logoData) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
.clipped()
|
||||
} else {
|
||||
VStack(spacing: Design.Spacing.xSmall) {
|
||||
Image(systemName: "building.2.fill")
|
||||
.font(.system(size: Design.BaseFontSize.display, weight: .bold))
|
||||
Text("Logo")
|
||||
.font(.title3)
|
||||
.bold()
|
||||
}
|
||||
.foregroundStyle(card.theme.textColor.opacity(Design.Opacity.medium))
|
||||
}
|
||||
.foregroundStyle(card.theme.textColor.opacity(Design.Opacity.medium))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,27 +129,30 @@ private struct CoverBannerContent: View {
|
||||
let card: BusinessCard
|
||||
|
||||
var body: some View {
|
||||
if let coverPhotoData = card.coverPhotoData, let uiImage = UIImage(data: coverPhotoData) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.clipped()
|
||||
} else {
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
colors: [card.theme.primaryColor, card.theme.secondaryColor],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
GeometryReader { geometry in
|
||||
if let coverPhotoData = card.coverPhotoData, let uiImage = UIImage(data: coverPhotoData) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
.clipped()
|
||||
} else {
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
colors: [card.theme.primaryColor, card.theme.secondaryColor],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
|
||||
VStack(spacing: Design.Spacing.xSmall) {
|
||||
Image(systemName: "photo.fill")
|
||||
.font(.system(size: Design.BaseFontSize.display, weight: .bold))
|
||||
Text("Cover")
|
||||
.font(.title3)
|
||||
.bold()
|
||||
VStack(spacing: Design.Spacing.xSmall) {
|
||||
Image(systemName: "photo.fill")
|
||||
.font(.system(size: Design.BaseFontSize.display, weight: .bold))
|
||||
Text("Cover")
|
||||
.font(.title3)
|
||||
.bold()
|
||||
}
|
||||
.foregroundStyle(card.theme.textColor.opacity(Design.Opacity.medium))
|
||||
}
|
||||
.foregroundStyle(card.theme.textColor.opacity(Design.Opacity.medium))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -325,7 +335,7 @@ private struct ContactFieldsListView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
ForEach(card.orderedContactFields) { field in
|
||||
ContactFieldRowView(field: field) {
|
||||
ContactFieldRowView(field: field, themeColor: card.theme.primaryColor) {
|
||||
if let url = field.buildURL() {
|
||||
openURL(url)
|
||||
}
|
||||
@ -337,6 +347,7 @@ private struct ContactFieldsListView: View {
|
||||
|
||||
private struct ContactFieldRowView: View {
|
||||
let field: ContactField
|
||||
let themeColor: Color
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
@ -346,7 +357,7 @@ private struct ContactFieldRowView: View {
|
||||
.font(.body)
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: Design.CardSize.socialIconSize, height: Design.CardSize.socialIconSize)
|
||||
.background(field.iconColor)
|
||||
.background(themeColor)
|
||||
.clipShape(.circle)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
|
||||
@ -212,7 +212,7 @@ struct CardEditorView: View {
|
||||
if !contactFields.isEmpty {
|
||||
Section {
|
||||
ForEach(contactFields) { field in
|
||||
ContactFieldRowView(field: field) {
|
||||
ContactFieldRowView(field: field, themeColor: selectedTheme.primaryColor) {
|
||||
fieldToEdit = field
|
||||
}
|
||||
}
|
||||
@ -231,7 +231,7 @@ struct CardEditorView: View {
|
||||
|
||||
// Add new contact fields
|
||||
Section {
|
||||
ContactFieldPickerView { fieldType in
|
||||
ContactFieldPickerView(themeColor: selectedTheme.primaryColor) { fieldType in
|
||||
selectedFieldTypeForAdd = fieldType
|
||||
}
|
||||
} header: {
|
||||
@ -284,6 +284,7 @@ struct CardEditorView: View {
|
||||
fieldType: fieldType,
|
||||
initialValue: "",
|
||||
initialTitle: fieldType.titleSuggestions.first ?? "",
|
||||
themeColor: selectedTheme.primaryColor,
|
||||
onSave: { value, title in
|
||||
addContactField(fieldType: fieldType, value: value, title: title)
|
||||
}
|
||||
@ -294,6 +295,7 @@ struct CardEditorView: View {
|
||||
fieldType: field.fieldType,
|
||||
initialValue: field.value,
|
||||
initialTitle: field.title,
|
||||
themeColor: selectedTheme.primaryColor,
|
||||
onSave: { value, title in
|
||||
updateContactField(id: field.id, value: value, title: title)
|
||||
},
|
||||
@ -555,24 +557,33 @@ private struct ImageLayoutRow: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||
// Live card preview based on selected layout
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
// Banner preview based on layout
|
||||
EditorBannerPreviewView(
|
||||
photoData: photoData,
|
||||
coverPhotoData: coverPhotoData,
|
||||
logoData: logoData,
|
||||
avatarSystemName: avatarSystemName,
|
||||
selectedTheme: selectedTheme,
|
||||
selectedHeaderLayout: selectedHeaderLayout
|
||||
)
|
||||
VStack(spacing: 0) {
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
// Banner preview based on layout
|
||||
EditorBannerPreviewView(
|
||||
photoData: photoData,
|
||||
coverPhotoData: coverPhotoData,
|
||||
logoData: logoData,
|
||||
avatarSystemName: avatarSystemName,
|
||||
selectedTheme: selectedTheme,
|
||||
selectedHeaderLayout: selectedHeaderLayout
|
||||
)
|
||||
|
||||
// Overlay content based on layout
|
||||
// Overlay content based on layout
|
||||
if hasOverlappingContent {
|
||||
overlayContent
|
||||
.padding(.leading, Design.Spacing.large)
|
||||
.padding(.trailing, Design.Spacing.large)
|
||||
.offset(y: Design.CardSize.avatarOverlap)
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer to make room for overlapping content
|
||||
if hasOverlappingContent {
|
||||
overlayContent
|
||||
.offset(x: Design.Spacing.large, y: Design.CardSize.avatarOverlap)
|
||||
Spacer()
|
||||
.frame(height: Design.CardSize.avatarOverlap)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, hasOverlappingContent ? Design.CardSize.avatarOverlap : 0)
|
||||
|
||||
// Layout selector button
|
||||
Button(action: onSelectLayout) {
|
||||
@ -628,14 +639,16 @@ private struct EditorBannerPreviewView: View {
|
||||
let selectedHeaderLayout: CardHeaderLayout
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch selectedHeaderLayout.bannerContent {
|
||||
case .profile:
|
||||
profileBannerPreview
|
||||
case .logo:
|
||||
logoBannerPreview
|
||||
case .cover:
|
||||
coverBannerPreview
|
||||
GeometryReader { geometry in
|
||||
Group {
|
||||
switch selectedHeaderLayout.bannerContent {
|
||||
case .profile:
|
||||
profileBannerPreview(size: geometry.size)
|
||||
case .logo:
|
||||
logoBannerPreview(size: geometry.size)
|
||||
case .cover:
|
||||
coverBannerPreview(size: geometry.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: Design.CardSize.bannerHeight)
|
||||
@ -645,12 +658,13 @@ private struct EditorBannerPreviewView: View {
|
||||
// MARK: - Layout Previews
|
||||
|
||||
/// Profile photo fills the banner
|
||||
private var profileBannerPreview: some View {
|
||||
private func profileBannerPreview(size: CGSize) -> some View {
|
||||
ZStack {
|
||||
if let photoData, let uiImage = UIImage(data: photoData) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: size.width, height: size.height)
|
||||
.clipped()
|
||||
} else {
|
||||
themeGradient
|
||||
@ -668,7 +682,7 @@ private struct EditorBannerPreviewView: View {
|
||||
}
|
||||
|
||||
/// Logo (3:2) fills the banner
|
||||
private var logoBannerPreview: some View {
|
||||
private func logoBannerPreview(size: CGSize) -> some View {
|
||||
ZStack {
|
||||
themeGradient
|
||||
|
||||
@ -676,6 +690,7 @@ private struct EditorBannerPreviewView: View {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: size.width, height: size.height)
|
||||
.clipped()
|
||||
} else {
|
||||
VStack(spacing: Design.Spacing.xSmall) {
|
||||
@ -691,12 +706,13 @@ private struct EditorBannerPreviewView: View {
|
||||
}
|
||||
|
||||
/// Cover photo fills the banner
|
||||
private var coverBannerPreview: some View {
|
||||
private func coverBannerPreview(size: CGSize) -> some View {
|
||||
Group {
|
||||
if let coverPhotoData, let uiImage = UIImage(data: coverPhotoData) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: size.width, height: size.height)
|
||||
.clipped()
|
||||
} else {
|
||||
ZStack {
|
||||
@ -926,6 +942,7 @@ private struct ProfilePhotoView: View {
|
||||
|
||||
private struct ContactFieldRowView: View {
|
||||
let field: AddedContactField
|
||||
let themeColor: Color
|
||||
let onTap: () -> Void
|
||||
|
||||
var body: some View {
|
||||
@ -939,7 +956,7 @@ private struct ContactFieldRowView: View {
|
||||
|
||||
// Icon
|
||||
Circle()
|
||||
.fill(field.fieldType.iconColor)
|
||||
.fill(themeColor)
|
||||
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
|
||||
.overlay(
|
||||
field.fieldType.iconImage()
|
||||
|
||||
@ -39,6 +39,7 @@ struct AddedContactField: Identifiable, Equatable {
|
||||
/// Displays a vertical list of added contact fields with tap to edit and drag to reorder
|
||||
struct AddedContactFieldsView: View {
|
||||
@Binding var fields: [AddedContactField]
|
||||
var themeColor: Color = Color(red: 0.2, green: 0.2, blue: 0.2)
|
||||
let onEdit: (AddedContactField) -> Void
|
||||
|
||||
@State private var draggingField: AddedContactField?
|
||||
@ -51,12 +52,13 @@ struct AddedContactFieldsView: View {
|
||||
ForEach(fields) { field in
|
||||
FieldRow(
|
||||
field: field,
|
||||
themeColor: themeColor,
|
||||
onTap: { onEdit(field) },
|
||||
onDelete: { deleteField(field) }
|
||||
)
|
||||
.draggable(field.id.uuidString) {
|
||||
// Drag preview
|
||||
FieldRowPreview(field: field)
|
||||
FieldRowPreview(field: field, themeColor: themeColor)
|
||||
}
|
||||
.dropDestination(for: String.self) { items, _ in
|
||||
guard let droppedId = items.first,
|
||||
@ -94,11 +96,12 @@ struct AddedContactFieldsView: View {
|
||||
/// Preview shown while dragging a field
|
||||
private struct FieldRowPreview: View {
|
||||
let field: AddedContactField
|
||||
let themeColor: Color
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
Circle()
|
||||
.fill(field.fieldType.iconColor)
|
||||
.fill(themeColor)
|
||||
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
|
||||
.overlay(
|
||||
field.fieldType.iconImage()
|
||||
@ -130,6 +133,7 @@ private struct FieldRowPreview: View {
|
||||
/// A display row for a contact field - tap to edit, hold to drag
|
||||
private struct FieldRow: View {
|
||||
let field: AddedContactField
|
||||
let themeColor: Color
|
||||
let onTap: () -> Void
|
||||
let onDelete: () -> Void
|
||||
|
||||
@ -143,7 +147,7 @@ private struct FieldRow: View {
|
||||
|
||||
// Icon
|
||||
Circle()
|
||||
.fill(field.fieldType.iconColor)
|
||||
.fill(themeColor)
|
||||
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
|
||||
.overlay(
|
||||
field.fieldType.iconImage()
|
||||
|
||||
@ -3,6 +3,7 @@ import Bedrock
|
||||
|
||||
/// Grid view for selecting contact field types to add
|
||||
struct ContactFieldPickerView: View {
|
||||
var themeColor: Color = Color(red: 0.2, green: 0.2, blue: 0.2)
|
||||
let onSelect: (ContactFieldType) -> Void
|
||||
|
||||
private let columns = Array(repeating: GridItem(.flexible(), spacing: Design.Spacing.medium), count: 3)
|
||||
@ -27,7 +28,7 @@ struct ContactFieldPickerView: View {
|
||||
|
||||
LazyVGrid(columns: columns, spacing: Design.Spacing.large) {
|
||||
ForEach(ContactFieldType.allCases) { fieldType in
|
||||
FieldTypeButton(fieldType: fieldType) {
|
||||
FieldTypeButton(fieldType: fieldType, themeColor: themeColor) {
|
||||
onSelect(fieldType)
|
||||
}
|
||||
}
|
||||
@ -40,13 +41,14 @@ struct ContactFieldPickerView: View {
|
||||
|
||||
private struct FieldTypeButton: View {
|
||||
let fieldType: ContactFieldType
|
||||
let themeColor: Color
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
VStack(spacing: Design.Spacing.small) {
|
||||
Circle()
|
||||
.fill(fieldType.iconColor)
|
||||
.fill(themeColor)
|
||||
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
|
||||
.overlay(
|
||||
fieldType.iconImage()
|
||||
|
||||
@ -8,6 +8,7 @@ struct ContactFieldEditorSheet: View {
|
||||
let fieldType: ContactFieldType
|
||||
let initialValue: String
|
||||
let initialTitle: String
|
||||
let themeColor: Color
|
||||
let onSave: (String, String) -> Void
|
||||
let onDelete: (() -> Void)?
|
||||
|
||||
@ -19,12 +20,14 @@ struct ContactFieldEditorSheet: View {
|
||||
fieldType: ContactFieldType,
|
||||
initialValue: String = "",
|
||||
initialTitle: String = "",
|
||||
themeColor: Color = Color(red: 0.2, green: 0.2, blue: 0.2),
|
||||
onSave: @escaping (String, String) -> Void,
|
||||
onDelete: (() -> Void)? = nil
|
||||
) {
|
||||
self.fieldType = fieldType
|
||||
self.initialValue = initialValue
|
||||
self.initialTitle = initialTitle
|
||||
self.themeColor = themeColor
|
||||
self.onSave = onSave
|
||||
self.onDelete = onDelete
|
||||
_value = State(initialValue: initialValue)
|
||||
@ -61,7 +64,7 @@ struct ContactFieldEditorSheet: View {
|
||||
NavigationStack {
|
||||
VStack(spacing: 0) {
|
||||
// Header with icon
|
||||
FieldHeaderView(fieldType: fieldType)
|
||||
FieldHeaderView(fieldType: fieldType, themeColor: themeColor)
|
||||
|
||||
Divider()
|
||||
|
||||
@ -179,11 +182,12 @@ struct ContactFieldEditorSheet: View {
|
||||
|
||||
private struct FieldHeaderView: View {
|
||||
let fieldType: ContactFieldType
|
||||
let themeColor: Color
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
Circle()
|
||||
.fill(fieldType.iconColor)
|
||||
.fill(themeColor)
|
||||
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
|
||||
.overlay(
|
||||
fieldType.iconImage()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user