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
35 changes: 16 additions & 19 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,23 @@ the host passes in.
- Assets live in `Sources/DashUIKit/Resources/Media.xcassets` and are reached via
`Bundle.module` / `Bundle.dashUIKit`.

## ⚠️ Hard constraint: must support iOS 14
## Deployment target: iOS 18

**This library must remain available on iOS 14.** The package deployment target is
`iOS(.v14)` (see `Package.swift`) and this is non-negotiable — every public component
must be usable from an iOS 14 host.
The package targets **iOS 18 / macOS 15** (see `Package.swift`), matching dashwallet-ios,
the only consumer. Everything SwiftUI shipped up to iOS 17 — `@FocusState`,
`presentationDetents`, `presentationBackground`, the `#Preview` macro, `.isToggle` — is
available unconditionally, so components carry no `@available` annotation and no fallback
branch for it.

When writing or changing code:

- Annotate public API with `@available(iOS 14, macOS 11, *)` (or `macOS 10.15` for
Foundation-level token types). **Do not** make a component's *minimum* availability
higher than iOS 14.
- If you need a SwiftUI API introduced after iOS 14 (e.g. `@FocusState` 15,
`presentationDetents` 16, `#Preview` macro 17), **gate it with `if #available(...)`
and provide an iOS 14 fallback path** — never raise the whole component's floor.
See `SearchBar` (15+ focus path + 14 legacy path), `BottomSheet` / `selfSizingSheet`
(16+ detents, no-op below), and `AddressFieldView` (17 axis field + older fallback).
- It is fine for `#Preview`-only code to require iOS 17 — previews are `#if DEBUG` and
never ship — but the component itself must still build and run on iOS 14.
- **Do not** add `@available(iOS …)` for anything at or below the floor; the package
declares it once. Annotate only what genuinely needs a *higher* version than the
floor, and then gate it with `if #available(...)` plus a path that works below it —
`selfSizingSheet` does this for the iOS 26 corner styling (`#unavailable(iOS 26.0)`).
- Raising the floor again is a package-level decision, not a per-component one. If a
component cannot work at the declared floor, say so in review rather than annotating
around it.

## Where things are

Expand Down Expand Up @@ -56,9 +55,8 @@ Sources/DashUIKit/
or `Font.dash.subhead` when you only need the font. Tokens defined in `DashTextStyle.swift`.
- **Icons:** pass a `DashIconSource` (`.system` / `.custom(name, bundle:)` / `.uiImage`) and
render with `Image(dash: source)`. Custom assets resolve from `.dashUIKit`/`.module`.
- **Availability:** annotate public API with `@available(iOS 14, macOS 11, *)` (or higher only
when a newer SwiftUI feature requires it, with an iOS 14 fallback path — see `SearchBar`,
`BottomSheet`).
- **Availability:** don't annotate. The floor is declared in `Package.swift`; annotate only
an API that needs something *newer* than it, and gate that with `if #available(...)`.
- **UIKit-only files** are wrapped in `#if canImport(UIKit)` (e.g. `SearchBar`, `Toast`,
`AddressFieldView`, `DashSwitch`).
- **Localization:** user-facing strings use `NSLocalizedString(_, bundle: .module, comment:)`.
Expand All @@ -68,8 +66,7 @@ Sources/DashUIKit/
## Build / preview

- This is a plain SwiftPM library — `swift build` compiles it; there is no app target.
- Develop visually with Xcode SwiftUI **#Previews** (open a file, use the canvas). Previews
require iOS 17 for the `#Preview` macro; older `PreviewProvider` previews work back further.
- Develop visually with Xcode SwiftUI **#Previews** (open a file, use the canvas).

## Component catalog

Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ let package = Package(
name: "DashUIKit",
defaultLocalization: "en",
platforms: [
.iOS(.v14)
.iOS(.v18),
.macOS(.v15)
],
products: [
.library(name: "DashUIKit", targets: ["DashUIKit"]),
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Every component is **presentational and value-driven**: it owns no business logi
networking, or persistence. You pass in formatted strings, flags, bindings, and
callbacks; the component renders and reports user intent back to you.

- **Platforms:** iOS 14+ (some features light up on 15 / 16 / 17 with graceful fallbacks)
- **Platforms:** iOS 18+ / macOS 15+
- **Swift tools:** 6.3
- **Module:** `import DashUIKit`

Expand Down Expand Up @@ -67,7 +67,7 @@ a component already exists before building your own.
|---|---|---|
| `DashButton` | Themed button — 4 sizes × 11 styles, icons, loading, full-width | [Buttons & inputs](docs/buttons-and-inputs.md) |
| `DashSwitch` | DS-tinted toggle | [Buttons & inputs](docs/buttons-and-inputs.md) |
| `SearchBar` | Search field with animated cancel (iOS 15+) and iOS 14 fallback | [Buttons & inputs](docs/buttons-and-inputs.md) |
| `SearchBar` | Search field with an animated cancel button | [Buttons & inputs](docs/buttons-and-inputs.md) |
| `AddressFieldView` | Crypto-address text field with QR / clear and error state | [Buttons & inputs](docs/buttons-and-inputs.md) |
| `NumericKeyboardView` | Locale-aware on-screen number pad with action button | [Buttons & inputs](docs/buttons-and-inputs.md) |
| `EnterAmountView` | Amount-entry screen control — single & dual-swap modes | [Amount & currency](docs/amount-and-currency.md) |
Expand Down
19 changes: 0 additions & 19 deletions Sources/DashUIKit/Button/DashButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import SwiftUI

@available(iOS 14, macOS 10.15, *)
public enum DashButtonSize: Sendable {
case large, medium, small, extraSmall

Expand Down Expand Up @@ -67,7 +66,6 @@ public enum DashButtonSize: Sendable {
}
}

@available(iOS 14, macOS 10.15, *)
public enum DashButtonStyle {
case filledBlue, filledRed, strokeGray, tintedBlue, tintedGray, plainBlue, plainBlack, plainRed, filledWhiteBlue, tintedWhite, plainWhite

Expand Down Expand Up @@ -120,7 +118,6 @@ public enum DashButtonStyle {
}
}

@available(iOS 14, macOS 11, *)
public struct DashButton: View {

public var text: String? = "Label"
Expand Down Expand Up @@ -216,15 +213,13 @@ public struct DashButton: View {

#if DEBUG

@available(iOS 17, macOS 14, *)
private let previewButtonSizes: [DashButtonSize] = [
.large,
.medium,
.small,
.extraSmall,
]

@available(iOS 17, macOS 14, *)
private extension DashButtonSize {
var previewTitle: String {
switch self {
Expand All @@ -236,7 +231,6 @@ private extension DashButtonSize {
}
}

@available(iOS 17, macOS 14, *)
private extension DashButtonStyle {
var previewTitle: String {
switch self {
Expand Down Expand Up @@ -264,7 +258,6 @@ private extension DashButtonStyle {
}
}

@available(iOS 17, macOS 14, *)
private struct DashButtonStylePreview: View {
let style: DashButtonStyle

Expand Down Expand Up @@ -313,7 +306,6 @@ private struct DashButtonStylePreview: View {
}
}

@available(iOS 17, macOS 14, *)
#Preview("Example") {
VStack {
DashButton(
Expand All @@ -335,57 +327,46 @@ private struct DashButtonStylePreview: View {
.padding(.horizontal)
}

@available(iOS 17, macOS 14, *)
#Preview("Filled Blue") {
DashButtonStylePreview(style: .filledBlue)
}

@available(iOS 17, macOS 14, *)
#Preview("Filled Red") {
DashButtonStylePreview(style: .filledRed)
}

@available(iOS 17, macOS 14, *)
#Preview("Stroke Gray") {
DashButtonStylePreview(style: .strokeGray)
}

@available(iOS 17, macOS 14, *)
#Preview("Tinted Blue") {
DashButtonStylePreview(style: .tintedBlue)
}

@available(iOS 17, macOS 14, *)
#Preview("Tinted Gray") {
DashButtonStylePreview(style: .tintedGray)
}

@available(iOS 17, macOS 14, *)
#Preview("Plain Blue") {
DashButtonStylePreview(style: .plainBlue)
}

@available(iOS 17, macOS 14, *)
#Preview("Plain Black") {
DashButtonStylePreview(style: .plainBlack)
}

@available(iOS 17, macOS 14, *)
#Preview("Plain Red") {
DashButtonStylePreview(style: .plainRed)
}

@available(iOS 17, macOS 14, *)
#Preview("Filled White Blue") {
DashButtonStylePreview(style: .filledWhiteBlue)
}

@available(iOS 17, macOS 14, *)
#Preview("Tinted White") {
DashButtonStylePreview(style: .tintedWhite)
}

@available(iOS 17, macOS 14, *)
#Preview("Plain White") {
DashButtonStylePreview(style: .plainWhite)
}
Expand Down
60 changes: 19 additions & 41 deletions Sources/DashUIKit/Components/AddressFieldView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ private enum Layout {
static let actionTapArea: CGFloat = 40
}

@available(iOS 15, macOS 12, *)
public struct AddressFieldView<Accessory: View>: View {

@Binding private var text: String
Expand Down Expand Up @@ -118,7 +117,6 @@ public struct AddressFieldView<Accessory: View>: View {

}

@available(iOS 15, macOS 12, *)
public extension AddressFieldView where Accessory == EmptyView {
/// No label accessory — the original shape, unchanged for callers that
/// have nothing to put there.
Expand All @@ -145,7 +143,6 @@ public extension AddressFieldView where Accessory == EmptyView {
}
}

@available(iOS 15, macOS 12, *)
extension AddressFieldView {
// MARK: - Subviews

Expand All @@ -162,36 +159,25 @@ extension AddressFieldView {
)
}

@ViewBuilder private var textField: some View {
if #available(iOS 17.0, *) {
TextField(
"",
text: $text,
// A prompt must stay a `Text`; `dashFont` returns `some View`, and a
// line height cannot be applied to `Text` anyway.
prompt: Text(placeholder)
.font(Font.dash.subhead)
.foregroundStyle(Color.dash.black1000Alpha30),
axis: .vertical
)
.lineLimit(1...2)
.dashFont(.subhead)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.foregroundStyle(Color.dash.primaryText)
.tint(Color.dash.primaryText)
.focused($isTextFieldFocused)
.disabled(isDisabled)
} else {
TextField(placeholder, text: $text)
.dashFont(.subhead)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.foregroundStyle(Color.dash.primaryText)
.tint(Color.dash.primaryText)
.focused($isTextFieldFocused)
.disabled(isDisabled)
}
private var textField: some View {
TextField(
"",
text: $text,
// A prompt must stay a `Text`; `dashFont` returns `some View`, and a
// line height cannot be applied to `Text` anyway.
prompt: Text(placeholder)
.font(Font.dash.subhead)
.foregroundStyle(Color.dash.black1000Alpha30),
axis: .vertical
)
.lineLimit(1...2)
.dashFont(.subhead)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.foregroundStyle(Color.dash.primaryText)
.tint(Color.dash.primaryText)
.focused($isTextFieldFocused)
.disabled(isDisabled)
}

private var actionButton: some View {
Expand Down Expand Up @@ -253,7 +239,6 @@ extension AddressFieldView {

#if DEBUG

@available(iOS 17, macOS 14, *)
#Preview("Empty") {
AddressFieldView(
text: .constant(""),
Expand All @@ -265,7 +250,6 @@ extension AddressFieldView {
.padding()
}

@available(iOS 17, macOS 14, *)
#Preview("Empty with Paste button") {
AddressFieldView(
text: .constant(""),
Expand All @@ -278,7 +262,6 @@ extension AddressFieldView {
.padding()
}

@available(iOS 17, macOS 14, *)
#Preview("With Text") {
AddressFieldView(
text: .constant("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"),
Expand All @@ -290,7 +273,6 @@ extension AddressFieldView {
.padding()
}

@available(iOS 17, macOS 14, *)
#Preview("Multiline") {
AddressFieldView(
text: .constant("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh\nbc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"),
Expand All @@ -302,7 +284,6 @@ extension AddressFieldView {
.padding()
}

@available(iOS 17, macOS 14, *)
#Preview("Error") {
AddressFieldView(
text: .constant("invalid-address"),
Expand All @@ -314,7 +295,6 @@ extension AddressFieldView {
.padding()
}

@available(iOS 17, macOS 14, *)
#Preview("Filled") {
AddressFieldView(
text: .constant("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"),
Expand All @@ -326,7 +306,6 @@ extension AddressFieldView {
.padding()
}

@available(iOS 17, macOS 14, *)
#Preview("Blurred + filled (no action button)") {
AddressFieldView(
text: .constant("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"),
Expand All @@ -338,7 +317,6 @@ extension AddressFieldView {
.padding()
}

@available(iOS 17, macOS 14, *)
#Preview("Label accessory") {
AddressFieldView(
text: .constant("yV1D1ivvSUyKPJnbFmzSTVh1MyZ3JbeVkY"),
Expand Down
Loading
Loading