Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "lightbulb.png",
"filename" : "lightbulb-figure.png",
"idiom" : "universal",
"scale" : "1x"
},
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions Bitkit/Assets.xcassets/icons/lightbulb.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "lightbulb.pdf",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Point lightbulb image set to the actual asset file

This image set declares "filename": "lightbulb.pdf", but the commit only adds lightbulb.png in that folder, so the catalog entry points to a non-existent file. As a result, Image("lightbulb") (for example in profile hint UIs) can render as missing at runtime or fail asset validation in build tooling. Update the filename to lightbulb.png (or add the referenced PDF) so the asset resolves correctly.

Useful? React with 👍 / 👎.

"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
2 changes: 0 additions & 2 deletions Bitkit/Components/Activity/ActivityList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import SwiftUI
struct ActivityList: View {
@EnvironmentObject var activity: ActivityListViewModel
@EnvironmentObject var feeEstimatesManager: FeeEstimatesManager
@State private var isHorizontalSwipe = false

let viewType: ActivityViewType

Expand All @@ -31,7 +30,6 @@ struct ActivityList: View {
ActivityRow(item: item, feeEstimates: feeEstimatesManager.estimates)
}
.accessibilityIdentifier("Activity-\(index)")
.disabled(isHorizontalSwipe)
}
}
}
Expand Down
85 changes: 85 additions & 0 deletions Bitkit/Components/Card.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import SwiftUI

/// Shared gradient tile used by suggestions widget and shop discover
struct Card: View {
let title: String
let description: String
let imageName: String
let accentColor: Color
let onTap: () -> Void
let onDismiss: (() -> Void)?

init(
title: String,
description: String,
imageName: String,
accentColor: Color,
onTap: @escaping () -> Void,
onDismiss: (() -> Void)? = nil
) {
self.title = title
self.description = description
self.imageName = imageName
self.accentColor = accentColor
self.onTap = onTap
self.onDismiss = onDismiss
}

var body: some View {
ZStack(alignment: .topTrailing) {
Button(action: onTap) {
VStack(alignment: .leading, spacing: 0) {
Spacer()

Image(imageName)
.resizable()
.scaledToFit()
.frame(width: 96, height: 96)
.frame(maxWidth: .infinity, alignment: .center)

Text(title)
.font(.custom(Fonts.black, size: 20))
.lineLimit(1)
.kerning(-0.5)
.textCase(.uppercase)
.padding(.top, 4)

CaptionBText(description)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding(.vertical, 12)
.padding(.horizontal, 16)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(
LinearGradient(
gradient: Gradient(stops: [
.init(color: accentColor, location: 0.0),
.init(color: Color.black.opacity(0.1), location: 0.9),
.init(color: Color.black, location: 1.0),
]),
startPoint: .top,
endPoint: .bottom
)
)
)
}
.buttonStyle(.plain)

if let onDismiss {
Button(action: onDismiss) {
Image("x-mark")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.textSecondary)
.frame(width: 16, height: 16)
.padding(8)
}
.padding(8)
.accessibilityIdentifier("SuggestionDismiss")
.accessibility(label: Text("Dismiss \(title)"))
.buttonStyle(.plain)
}
}
}
}
57 changes: 0 additions & 57 deletions Bitkit/Components/Home/SuggestionsCard.swift

This file was deleted.

64 changes: 64 additions & 0 deletions Bitkit/Components/InsetHeaderScrollView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import SwiftUI

// MARK: - InsetHeaderScrollView

// Measured top header (`safeAreaInset`) and scroll content with `minHeight` to fill the viewport below it.
// Optional `scrollModifier` for refresh, margins, etc.

private enum HeaderHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0

static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
let next = nextValue()
guard next > 0 else { return }
value = next
}
}

private struct HeaderHeightMeasure: View {
var body: some View {
GeometryReader { proxy in
Color.clear.preference(key: HeaderHeightPreferenceKey.self, value: proxy.size.height)
}
}
}

struct InsetHeaderScrollView<Header: View, Content: View, ScrollModifier: ViewModifier>: View {
let header: () -> Header
let content: () -> Content
let scrollModifier: ScrollModifier

@State private var headerHeight: CGFloat = 0

init(
header: @escaping () -> Header,
content: @escaping () -> Content,
scrollModifier: ScrollModifier = EmptyModifier()
) {
self.header = header
self.content = content
self.scrollModifier = scrollModifier
}

var body: some View {
GeometryReader { geo in
ScrollView(showsIndicators: false) {
content()
.frame(minHeight: contentMinHeight(in: geo), alignment: .top)
}
.safeAreaInset(edge: .top, spacing: 0) {
header().background(HeaderHeightMeasure())
}
.modifier(scrollModifier)
.onPreferenceChange(HeaderHeightPreferenceKey.self) { newValue in
if newValue > 0 { headerHeight = newValue }
}
}
}

/// Before the first header measurement, use full height so `minHeight` is non-negative.
private func contentMinHeight(in geo: GeometryProxy) -> CGFloat {
let insetTop = headerHeight > 0 ? headerHeight : geo.size.height
return max(0, geo.size.height - insetTop)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ let cards: [SuggestionCardData] = [
id: "support",
title: t("cards__support__title"),
description: t("cards__support__description"),
imageName: "lightbulb",
imageName: "lightbulb-figure",
color: .yellow24,
action: .support
),
Expand Down Expand Up @@ -245,10 +245,16 @@ struct Suggestions: View {
spacing: 16
) {
ForEach(visibleCards) { card in
SuggestionCard(data: card, onDismiss: { dismissCard(card) })
.onTapGesture { if !isPreview { onItemTap(card) } }
.accessibilityElement(children: .contain)
.accessibilityIdentifier("Suggestion-\(card.accessibilityId)")
Card(
title: card.title,
description: card.description,
imageName: card.imageName,
accentColor: card.color,
onTap: { if !isPreview { onItemTap(card) } },
onDismiss: { dismissCard(card) }
)
.accessibilityElement(children: .contain)
.accessibilityIdentifier("Suggestion-\(card.accessibilityId)")
}
}
.allowsHitTesting(!isPreview)
Expand Down
21 changes: 0 additions & 21 deletions Bitkit/Extensions/View+AllowSwipeBack.swift

This file was deleted.

55 changes: 55 additions & 0 deletions Bitkit/Extensions/View+SwipeGestures.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import SwiftUI

// Swipe-to-go-back (nav stack) and horizontal swipes between `SegmentedControl` tabs.

extension View {
/// Controls whether the interactive swipe-back gesture is enabled on this screen.
/// Use `.allowSwipeBack(false)` on screens that use a custom header without a back button
/// (e.g. `SheetHeader` with default `showBackButton: false`) so users can't swipe to dismiss.
/// Default is `true`; only apply this modifier when you want to disable the gesture.
func allowSwipeBack(_ allowed: Bool) -> some View {
modifier(AllowSwipeBackModifier(allowed: allowed))
}

// MARK: Segmented tab swipes

/// Swipe left/right to move between adjacent tabs (same order as `T.allCases` / `SegmentedControl`).
func swipeSegmentedTabs<T: Hashable & CaseIterable>(
selection: Binding<T>,
minimumDragDistance: CGFloat = 20,
swipeThreshold: CGFloat = 50,
animation: Animation = .easeInOut(duration: 0.2)
) -> some View {
highPriorityGesture(
DragGesture(minimumDistance: minimumDragDistance, coordinateSpace: .local)
.onEnded { value in
let horizontalAmount = value.translation.width
let verticalAmount = value.translation.height
guard abs(horizontalAmount) > abs(verticalAmount) else { return }

let tabs = Array(T.allCases)
guard let currentIndex = tabs.firstIndex(of: selection.wrappedValue) else { return }

if horizontalAmount < -swipeThreshold, currentIndex < tabs.count - 1 {
withAnimation(animation) {
selection.wrappedValue = tabs[currentIndex + 1]
}
} else if horizontalAmount > swipeThreshold, currentIndex > 0 {
withAnimation(animation) {
selection.wrappedValue = tabs[currentIndex - 1]
}
}
}
)
}
}

private struct AllowSwipeBackModifier: ViewModifier {
let allowed: Bool

func body(content: Content) -> some View {
content
.onAppear { SwipeBackState.allowSwipeBack = allowed }
.onDisappear { SwipeBackState.allowSwipeBack = true }
}
}
Loading
Loading