Skip to content
Merged
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
79 changes: 79 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# DashUIKit context

SwiftUI design-system library for Dash Core Group apps. Pure UI components, design
tokens (colors / typography), and small layout utilities. No business logic, no
networking — every component is dumb and driven entirely by the values and closures
the host passes in.

- Swift tools: **6.3**
- Single library target **`DashUIKit`** (`import DashUIKit`)
- Assets live in `Sources/DashUIKit/Resources/Media.xcassets` and are reached via
`Bundle.module` / `Bundle.dashUIKit`.

## ⚠️ Hard constraint: must support iOS 14

**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.

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.

## Where things are

```text
Sources/DashUIKit/
Button/ DashButton (+ DashButtonSize / DashButtonStyle)
Components/ Most components (one type per file)
EnterAmount/ Amount-entry suite (EnterAmountView, SwapAmountView, …)
Geometry/ Layout readers + scale-to-fit helpers
Icons/ Code-drawn icons (XmarkIcon)
Illustrations/ Loading / success / error illustrations
Table List/ List1View (label/value row)
ViewModifiers/ MenuViewModifier (card chrome)
Foundation/ Color / Font / DashTextStyle / Image / line-height / Bundle
Resources/ Media.xcassets (colors, icons, illustrations)
```

## Conventions (match these when adding code)

- **One public type per file**, named after the file. Heavy `#Preview` / `PreviewProvider`
coverage under `#if DEBUG` is expected — add previews for every state.
- **Theming is asset-driven.** Never hardcode a `Color(...)`/hex. Use `Color.dash.<token>`
(see `Foundation/Color+DashUI.swift`) — each maps to a named color set in the asset
catalog with light/dark variants. Add a new token there, not inline.
- **Typography:** use `.dashFont(.subhead)` (sets font **and** design line height together)
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`).
- **UIKit-only files** are wrapped in `#if canImport(UIKit)` (e.g. `SearchBar`, `Toast`,
`AddressFieldView`, `DashSwitch`).
- **Localization:** user-facing strings use `NSLocalizedString(_, bundle: .module, comment:)`.
- Components are **stateless/value-driven** — state (`@Binding`) and callbacks live with the
host. Don't add view models or persistence here.

## 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.

## Component catalog

Full per-component reference (API, props, behavior, usage) lives in **`docs/`**. Start at
[`docs/README.md`](docs/README.md) — it indexes every component so you can check whether a
given UI element already exists before building a new one. The public surface is also
summarized in [`README.md`](README.md).
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@ let package = Package(
.process("Resources/Media.xcassets"),
]
),
.testTarget(
name: "DashUIKitTests",
dependencies: ["DashUIKit"]
),
]
)
155 changes: 155 additions & 0 deletions Sources/DashUIKit/Components/CoinSelector.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//
// Created by Dash Core Group. All rights reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import SwiftUI

@available(iOS 14, macOS 11, *)
public enum CoinSelectorTrailing {
case price(String)
case halted
}

private enum CoinSelectorLayout {
static let spacing: CGFloat = 16
static let textSpacing: CGFloat = 1
static let padding: CGFloat = 10
static let badgeHPadding: CGFloat = 8
static let badgeVPadding: CGFloat = 2
static let badgeCornerRadius: CGFloat = 6
}

@available(iOS 14, macOS 11, *)
public struct CoinSelector<Icon: View>: View {

private let name: String
private let code: String
private let network: String?
private let trailing: CoinSelectorTrailing?
private let icon: Icon

public init(
name: String,
code: String,
network: String? = nil,
trailing: CoinSelectorTrailing? = nil,
@ViewBuilder icon: () -> Icon
) {
self.name = name
self.code = code
self.network = network
self.trailing = trailing
self.icon = icon()
}

private var isHalted: Bool {
if case .halted = trailing { return true }
return false
}

public var body: some View {
HStack(spacing: CoinSelectorLayout.spacing) {
icon
info
trailingView
}
.padding(CoinSelectorLayout.padding)
.opacity(isHalted ? 0.5 : 1.0)
.contentShape(Rectangle())
}

private var info: some View {
VStack(alignment: .leading, spacing: CoinSelectorLayout.textSpacing) {
Text(name)
.dashFont(.subheadMedium)
.foregroundColor(Color.dash.primaryText)
.lineLimit(1)
.truncationMode(.tail)
Text(code)
.dashFont(.footnote)
.foregroundColor(Color.dash.tertiaryText)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
}

@ViewBuilder
private var trailingView: some View {
if case .halted = trailing {
haltedLabel
} else {
VStack(alignment: .trailing, spacing: 0) {
if case .price(let value) = trailing {
Text(value)
.dashFont(.caption1)
.foregroundColor(Color.dash.primaryText)
.lineLimit(1)
}
if let network {
Text(network)
.dashFont(.caption1)
.foregroundColor(Color.dash.tertiaryText)
.lineLimit(1)
.fixedSize()
}
}
}
}

private var haltedLabel: some View {
Text(NSLocalizedString("halted", bundle: .module, comment: "DashUIKit"))
.dashFont(.caption1)
.foregroundColor(Color.dash.tertiaryText)
.padding(.horizontal, CoinSelectorLayout.badgeHPadding)
.padding(.vertical, CoinSelectorLayout.badgeVPadding)
.background(Color.dash.black1000Alpha8)
.clipShape(RoundedRectangle(cornerRadius: CoinSelectorLayout.badgeCornerRadius, style: .continuous))
}
}

#if DEBUG

@available(iOS 14, macOS 11, *)
struct CoinSelector_Previews: PreviewProvider {

static var iconPlaceholder: some View {
RoundedRectangle(cornerRadius: 6)
.fill(Color.dash.blue)
.frame(width: 30, height: 30)
}

static var previews: some View {
VStack(spacing: 0) {
CoinSelector(name: "Rune", code: "RUNE", network: "Maya", trailing: .price("$0.40")) {
iconPlaceholder
}
CoinSelector(name: "Ethereum", code: "ETH", network: "NEAR", trailing: .price("$3,200.00")) {
iconPlaceholder
}
CoinSelector(name: "USD Coin", code: "USDC", network: "Multiple", trailing: .price("$1.00")) {
iconPlaceholder
}
CoinSelector(name: "Tether", code: "USDT", trailing: .halted) {
iconPlaceholder
}
CoinSelector(name: "Bitcoin", code: "BTC") {
iconPlaceholder
}
}
.previewLayout(.sizeThatFits)
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// Created by Roman Chornyi
// Copyright © 2026 Dash Core Group. All rights reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import SwiftUI

// MARK: - ConverterArrowBadge

/// The arrow badge centered on the seam between the two `ConverterCard` rows.
///
/// - `onSwap == nil` → static `arrow-down` icon, non-interactive.
/// - `onSwap != nil` → tappable `diagonal-up-down` button that rotates 180° on each tap and fires
/// the swap action.
@available(iOS 14, macOS 11, *)
struct ConverterArrowBadge: View {
let onSwap: (() -> Void)?
@State private var rotation: Double = 0

var body: some View {
Group {
if let onSwap {
Button {
withAnimation(.spring(response: 0.35, dampingFraction: 0.82)) { rotation += 180 }
onSwap()
} label: {
badge(iconName: "diagonal-up-down", iconRotation: rotation)
}
.buttonStyle(.plain)
} else {
badge(iconName: "arrow-down", iconRotation: 0)
}
}
.frame(height: 35)
.padding(.horizontal, 10)
}

private func badge(iconName: String, iconRotation: Double) -> some View {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color.dash.secondaryBackground)
.frame(width: 35, height: 35)
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.stroke(Color.dash.primaryBackground, lineWidth: 5)
)
.overlay(
Image(dash: .custom(iconName, bundle: .dashUIKit))
.rotationEffect(.degrees(iconRotation))
)
}
}
Loading
Loading