From 3bbe98156cee5315dd8863cc30a3fcfa68157e15 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Mon, 21 Sep 2026 20:31:28 +0300 Subject: [PATCH 1/3] feat(components): Criteria and SimpleSelect, plus the field and sheet work behind them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new components, from the Join DashPay and create-username designs: - `Criteria` — the validation-rule block: a mark box that sets the row height, four states (pending ring, met, failed, blocking), and the icons the design system draws for each. Rules that are met are dropped by the caller, so the list shrinks as the input becomes valid. - `SimpleSelect` — the option rows the funding-privacy page picks a source with. `AddressFieldView` gains what those screens needed, all additive: - an optional `isFocused` binding, mirrored both ways, so a host can raise the keyboard on entry and drop it before presenting a sheet while the field still owns its `@FocusState`; - `showsClearButton`, because a short field the user can clear by hand does not need a ✕ to hit by accident, and `isAccepted`, which drops the focus ring once the content has been accepted; - the 3pt focus/error ring from Figma, drawn as an inset stroke rather than a shadow (with no blur and no offset a shadow lands under the field and is never seen); - the QR button only when an `onScanQR` handler exists — a username has no QR form, and the glyph was offering a control that did nothing. A field that can show neither control now reserves no trailing space. `SystemMessageView` takes a `buttonStyle`: a message that points somewhere else reads as a link, not as a call to action, and is spaced against the sentence it follows instead of sitting in a pill's padding. `BottomSheet` collapses its header when there is no title, no back button and no close button — 64pt of nothing that a self-sizing sheet was measuring into its own height. Also: the `RedAlpha20` token and six feature icons (friends, profile, recovery, selected-list, username, username-block). Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 3 +- .../Components/AddressFieldView.swift | 123 +++++- .../Components/BottomSheet/BottomSheet.swift | 13 +- Sources/DashUIKit/Components/Criteria.swift | 385 ++++++++++++++++++ .../DashUIKit/Components/SimpleSelect.swift | 122 ++++++ .../Components/SystemMessageView.swift | 30 +- .../DashUIKit/Foundation/Color+DashUI.swift | 1 + .../DashUIKit/Foundation/Icon_DashUI.swift | 6 + .../Red/RedAlpha20.colorset/Contents.json | 20 + .../feature-friends.imageset/Contents.json | 23 ++ .../feature-friends.png | Bin 0 -> 805 bytes .../feature-friends@2x.png | Bin 0 -> 1437 bytes .../feature-friends@3x.png | Bin 0 -> 2087 bytes .../feature-profile.imageset/Contents.json | 23 ++ .../feature-profile.png | Bin 0 -> 792 bytes .../feature-profile@2x.png | Bin 0 -> 1386 bytes .../feature-profile@3x.png | Bin 0 -> 2048 bytes .../feature-recovery.imageset/Contents.json | 23 ++ .../feature-recovery.png | Bin 0 -> 559 bytes .../feature-recovery@2x.png | Bin 0 -> 1003 bytes .../feature-recovery@3x.png | Bin 0 -> 1259 bytes .../Contents.json | 23 ++ .../feature-selected-list.png | Bin 0 -> 450 bytes .../feature-selected-list@2x.png | Bin 0 -> 693 bytes .../feature-selected-list@3x.png | Bin 0 -> 917 bytes .../Contents.json | 23 ++ .../feature-username-block.png | Bin 0 -> 673 bytes .../feature-username-block@2x.png | Bin 0 -> 1131 bytes .../feature-username-block@3x.png | Bin 0 -> 1629 bytes .../feature-username.imageset/Contents.json | 23 ++ .../feature-username.png | Bin 0 -> 907 bytes .../feature-username@2x.png | Bin 0 -> 1646 bytes .../feature-username@3x.png | Bin 0 -> 2355 bytes docs/README.md | 1 + docs/buttons-and-inputs.md | 74 ++++ 35 files changed, 901 insertions(+), 15 deletions(-) create mode 100644 Sources/DashUIKit/Components/Criteria.swift create mode 100644 Sources/DashUIKit/Components/SimpleSelect.swift create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Colors/Tokens/Red/RedAlpha20.colorset/Contents.json create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-friends.imageset/Contents.json create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-friends.imageset/feature-friends.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-friends.imageset/feature-friends@2x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-friends.imageset/feature-friends@3x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-profile.imageset/Contents.json create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-profile.imageset/feature-profile.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-profile.imageset/feature-profile@2x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-profile.imageset/feature-profile@3x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-recovery.imageset/Contents.json create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-recovery.imageset/feature-recovery.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-recovery.imageset/feature-recovery@2x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-recovery.imageset/feature-recovery@3x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-selected-list.imageset/Contents.json create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-selected-list.imageset/feature-selected-list.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-selected-list.imageset/feature-selected-list@2x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-selected-list.imageset/feature-selected-list@3x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/Contents.json create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/feature-username-block.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/feature-username-block@2x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/feature-username-block@3x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username.imageset/Contents.json create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username.imageset/feature-username.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username.imageset/feature-username@2x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username.imageset/feature-username@3x.png diff --git a/README.md b/README.md index 7f558dd..b378873 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,8 @@ 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) | -| `AddressFieldView` | Crypto-address text field with QR / clear and error state | [Buttons & inputs](docs/buttons-and-inputs.md) | +| `AddressFieldView` | Labeled text field (label / field / help text) with optional QR, clear and error state | [Buttons & inputs](docs/buttons-and-inputs.md) | +| `Criteria` | Requirement check-list — `Criterion` rows in six states | [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) | | `SwapAmountView` | Animated primary/secondary amount display | [Amount & currency](docs/amount-and-currency.md) | diff --git a/Sources/DashUIKit/Components/AddressFieldView.swift b/Sources/DashUIKit/Components/AddressFieldView.swift index 44aecc2..f87467f 100644 --- a/Sources/DashUIKit/Components/AddressFieldView.swift +++ b/Sources/DashUIKit/Components/AddressFieldView.swift @@ -26,6 +26,10 @@ private enum Layout { static let iconSize: CGFloat = 17 static let cornerRadius: CGFloat = 16 static let actionTapArea: CGFloat = 40 + /// Width of the focus / error ring, drawn hard-edged outside the field — + /// Figma's `shadow: 0 0 0 3px`, which is a 3pt spread with no blur and no + /// offset. + static let ringWidth: CGFloat = 3 } @available(iOS 15, macOS 12, *) @@ -44,6 +48,20 @@ public struct AddressFieldView: View { /// something that describes the field rather than acts on it; the /// controls that act live inside the field itself. private let accessory: Accessory + /// Lets the host raise and dismiss the keyboard — focus on appear, blur + /// before presenting a sheet. The field still owns its `@FocusState`; this + /// mirrors it in both directions so the host does not need one. `nil` means + /// the field is the only one deciding, which is what every existing caller + /// does. + private var isFocused: Binding? + /// Whether the trailing ✕ is offered once there is text. A long address + /// needs it; a short field the user can clear by hand does not, and there + /// it is only one more thing to hit by accident. + private let showsClearButton: Bool + /// The content has passed whatever the host validates it against, so the + /// field stops asking for attention: the focus ring is dropped. An error + /// still overrides it — a name can be well-formed and still refused. + private let isAccepted: Bool @FocusState private var isTextFieldFocused: Bool @@ -56,6 +74,9 @@ public struct AddressFieldView: View { isDisabled: Bool = false, onScanQR: (() -> Void)? = nil, onPaste: (() -> Void)? = nil, + isFocused: Binding? = nil, + showsClearButton: Bool = true, + isAccepted: Bool = false, @ViewBuilder accessory: () -> Accessory ) { self._text = text @@ -66,6 +87,9 @@ public struct AddressFieldView: View { self.isDisabled = isDisabled self.onScanQR = onScanQR self.onPaste = onPaste + self.isFocused = isFocused + self.showsClearButton = showsClearButton + self.isAccepted = isAccepted self.accessory = accessory() } @@ -107,6 +131,16 @@ public struct AddressFieldView: View { RoundedRectangle(cornerRadius: Layout.cornerRadius) .stroke(borderColor, lineWidth: borderWidth) ) + // Not `.shadow`: with no blur and no offset a shadow lands exactly + // under the field and is never seen, and in the focused state the + // background is clear, so what little showed would be cast by the + // text rather than by the field's outline. A stroke on the outward + // inset is the ring the design draws. + .overlay( + RoundedRectangle(cornerRadius: Layout.cornerRadius + Layout.ringWidth / 2, style: .continuous) + .inset(by: -Layout.ringWidth / 2) + .stroke(ringColor, lineWidth: Layout.ringWidth) + ) if let errorText { Text(errorText) @@ -114,6 +148,18 @@ public struct AddressFieldView: View { .foregroundStyle(Color.dash.errorText) } } + .onAppear { + // A host that wants the keyboard up on entry sets its flag before + // this field exists, so the mirror below never fires for it. + if isFocused?.wrappedValue == true { isTextFieldFocused = true } + } + .onChange(of: isTextFieldFocused) { focused in + if isFocused?.wrappedValue != focused { isFocused?.wrappedValue = focused } + } + .onChange(of: isFocused?.wrappedValue) { requested in + guard let requested, requested != isTextFieldFocused else { return } + isTextFieldFocused = requested + } } } @@ -130,7 +176,10 @@ public extension AddressFieldView where Accessory == EmptyView { errorText: String? = nil, isDisabled: Bool = false, onScanQR: (() -> Void)? = nil, - onPaste: (() -> Void)? = nil + onPaste: (() -> Void)? = nil, + isFocused: Binding? = nil, + showsClearButton: Bool = true, + isAccepted: Bool = false ) { self.init( text: text, @@ -141,6 +190,9 @@ public extension AddressFieldView where Accessory == EmptyView { isDisabled: isDisabled, onScanQR: onScanQR, onPaste: onPaste, + isFocused: isFocused, + showsClearButton: showsClearButton, + isAccepted: isAccepted, accessory: { EmptyView() }) } } @@ -197,14 +249,20 @@ extension AddressFieldView { private var actionButton: some View { Group { if text.isEmpty { - Button(action: { onScanQR?() }) { - DashIcon.Other.textFieldQR.image - .resizable() - .scaledToFit() - .frame(width: Layout.iconSize, height: Layout.iconSize) + // Only when there is somewhere for the tap to go. A field for + // something that has no QR form — a username, say — passes no + // handler, and drawing the glyph anyway offered a control that + // did nothing. + if let onScanQR { + Button(action: onScanQR) { + DashIcon.Other.textFieldQR.image + .resizable() + .scaledToFit() + .frame(width: Layout.iconSize, height: Layout.iconSize) + } + .accessibilityLabel(NSLocalizedString("Scan QR code", bundle: .module, comment: "DashUIKit")) } - .accessibilityLabel(NSLocalizedString("Scan QR code", bundle: .module, comment: "DashUIKit")) - } else { + } else if showsClearButton { Button(action: { text = "" }) { DashIcon.Other.textFieldClear.image .renderingMode(.template) @@ -216,10 +274,20 @@ extension AddressFieldView { .accessibilityLabel(NSLocalizedString("Clear address", bundle: .module, comment: "DashUIKit")) } } - .frame(width: Layout.actionTapArea, height: Layout.actionTapArea) + // The tap area is reserved rather than sized to whichever control is + // showing, so the field's width does not shift as the user types. A + // field that can never show either control reserves nothing. + .frame( + width: hasTrailingControl ? Layout.actionTapArea : 0, + height: Layout.actionTapArea) .contentShape(Rectangle()) } + /// Whether any trailing control can appear in this field's configuration. + private var hasTrailingControl: Bool { + onScanQR != nil || showsClearButton + } + // MARK: - Styling private var backgroundColor: Color { @@ -236,6 +304,17 @@ extension AddressFieldView { isFocusedState ? 1 : 0 } + /// The ring around the field, as the design system draws it: red while + /// something is wrong, blue while the user is working in the field, and + /// nothing once the content has been accepted — at that point the field + /// has no more to say and a glow would keep drawing the eye to a question + /// already answered. + private var ringColor: Color { + if hasError { return Color.dash.redAlpha20 } + if isFocusedState, !isAccepted { return Color.dash.blueAlpha20 } + return .clear + } + private var isFocusedState: Bool { isTextFieldFocused && !isDisabled } @@ -265,6 +344,32 @@ extension AddressFieldView { .padding() } +@available(iOS 17, macOS 14, *) +#Preview("Empty, no scan handler") { + // Nothing to scan — a username, a label, a note. No trailing glyph is + // drawn, where before an inert QR button was. + AddressFieldView( + text: .constant(""), + label: "Username", + placeholder: "Enter a username", + hasError: false, errorText: nil + ) + .padding() +} + +@available(iOS 17, macOS 14, *) +#Preview("Accepted — no ring, no clear button") { + AddressFieldView( + text: .constant("satoshi"), + label: "Username", + placeholder: "Enter a username", + hasError: false, errorText: nil, + showsClearButton: false, + isAccepted: true + ) + .padding() +} + @available(iOS 17, macOS 14, *) #Preview("Empty with Paste button") { AddressFieldView( diff --git a/Sources/DashUIKit/Components/BottomSheet/BottomSheet.swift b/Sources/DashUIKit/Components/BottomSheet/BottomSheet.swift index 77dfdcd..06edb3f 100644 --- a/Sources/DashUIKit/Components/BottomSheet/BottomSheet.swift +++ b/Sources/DashUIKit/Components/BottomSheet/BottomSheet.swift @@ -67,7 +67,9 @@ public struct BottomSheet: View { grabber .frame(maxWidth: .infinity, minHeight: 18, maxHeight: 18, alignment: .center) - header + if hasHeaderContent { + header + } contentSection } @@ -122,6 +124,15 @@ public struct BottomSheet: View { hasCustomCloseAction: onClose != nil) } + /// A header with no title, no back button and no close button is 64pt of + /// nothing: it pushes the content down, and on a self-sizing sheet that + /// blank strip becomes part of the sheet's own measured height. Collapse + /// it rather than reserve it. Any one of the three brings it back, and the + /// binding means a sheet can gain the header mid-flight. + private var hasHeaderContent: Bool { + !title.isEmpty || showBackButton || showsCloseButton + } + private var header: some View { NavigationBar( leading: { diff --git a/Sources/DashUIKit/Components/Criteria.swift b/Sources/DashUIKit/Components/Criteria.swift new file mode 100644 index 0000000..d16ef8f --- /dev/null +++ b/Sources/DashUIKit/Components/Criteria.swift @@ -0,0 +1,385 @@ +// +// 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 + +/// Outside the views so the rows and the block measure from one place. +private enum Layout { + /// Gap between the mark and the text. + static let gap: CGFloat = 6 + /// The box the mark is centred in. It, not the text, sets the row height. + static let markBox: CGFloat = 26 + /// The mark itself — every icon the design system draws for this row is + /// this size. + static let mark: CGFloat = 19 + /// Padding that lifts the 18pt text line box to the mark box's height. + static let textVerticalPadding: CGFloat = 4 + /// Stroke of the marks drawn here, in the 19pt box. + static let strokeWidth: CGFloat = 2 + /// The unfilled `pending` ring. + static let ringWidth: CGFloat = 1.5 +} + +// MARK: - CriterionState + +/// How one requirement currently stands. +@available(iOS 14, macOS 11, *) +public enum CriterionState: Equatable { + /// Stated but not evaluated yet — an empty ring. + case pending + /// Being evaluated — a spinner in the mark's place. + case checking + /// Satisfied — green disc, white tick. + case met + /// Not satisfied — red disc, white cross. + case failed + /// Not satisfied, and it is the reason the user cannot continue — the same + /// red disc, with the text in the error colour. + case blocking + /// Satisfied, but worth reading — yellow disc, white exclamation. + case warning +} + +// MARK: - Criterion + +/// One requirement in a ``Criteria`` block. +@available(iOS 14, macOS 11, *) +public struct Criterion: Identifiable { + /// What the row's mark slot holds — one of the standard discs, or the + /// caller's own icon. + enum Mark { + case state(CriterionState) + case icon(DashIconSource) + } + + public let id: String + public let text: String + let mark: Mark + + /// `id` defaults to `text`, which is what distinguishes the rows in + /// practice; pass one explicitly when a row's text changes while the row + /// stays the same (an availability line, say), so it animates in place + /// instead of being replaced. + public init(id: String? = nil, text: String, state: CriterionState) { + self.id = id ?? text + self.text = text + self.mark = .state(state) + } + + /// The design system's icon slot, for a row whose mark is none of the + /// standard discs. Rendered in the same 19pt box, untinted — the artwork + /// carries its own colour, exactly as the Figma slot does. + public init(id: String? = nil, text: String, icon: DashIconSource) { + self.id = id ?? text + self.text = text + self.mark = .icon(icon) + } +} + +// MARK: - Criteria + +/// A block of requirements with their status, as under a username or password +/// field: one mark per row saying whether that rule is met. +/// +/// Stateless — the host decides which rules exist, what they say and how each +/// one currently stands. A rule that should not be shown is simply not passed, +/// which is also how the design system models it: there is no "hidden" mark. +/// +/// ```swift +/// Criteria([ +/// Criterion(text: "Between 3 and 23 characters", state: .met), +/// Criterion(text: "Letters, numbers and hyphens only", state: .failed), +/// Criterion(text: "Username available", state: .checking), +/// ]) +/// ``` +@available(iOS 14, macOS 11, *) +public struct Criteria: View { + private let items: [Criterion] + private let spacing: CGFloat + + /// - Parameters: + /// - items: the requirements, in the order they should read. + /// - spacing: gap between rows. The default is the design's 10pt. + public init(_ items: [Criterion], spacing: CGFloat = 10) { + self.items = items + self.spacing = spacing + } + + public var body: some View { + VStack(alignment: .leading, spacing: spacing) { + ForEach(items) { item in + CriterionRow(criterion: item) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +// MARK: - Row + +@available(iOS 14, macOS 11, *) +private struct CriterionRow: View { + let criterion: Criterion + + var body: some View { + HStack(alignment: .top, spacing: Layout.gap) { + mark + .frame(width: Layout.markBox, height: Layout.markBox) + + Text(criterion.text) + .dashFont(.footnote) + .foregroundColor(textColor) + // Without this a two-line requirement is truncated to one: + // the row offers the text its ideal single-line height. + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, Layout.textVerticalPadding) + } + .accessibilityElement(children: .combine) + .accessibilityLabel(Text(criterion.text)) + .accessibilityValue(Text(accessibilityValue)) + } + + // MARK: Mark + + @ViewBuilder private var mark: some View { + switch criterion.mark { + case .icon(let source): + Image(dash: source) + .resizable() + .scaledToFit() + .frame(width: Layout.mark, height: Layout.mark) + case .state(let state): + switch state { + case .pending: + // `strokeBorder` insets by half the line width, so a 19pt + // circle strokes at r 8.75 and still reaches 19pt across — + // the design system's default icon to the point. + Circle() + .strokeBorder(Color.dash.gray300Alpha50, lineWidth: Layout.ringWidth) + .frame(width: Layout.mark, height: Layout.mark) + case .checking: + // In the mark's own box, so the rows do not shift while a + // check is running. + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .frame(width: Layout.mark, height: Layout.mark) + case .met: + disc(Color.dash.green) { + CheckShape() + .stroke( + Color.dash.white, + style: StrokeStyle( + lineWidth: Layout.strokeWidth, + lineCap: .round, + lineJoin: .round)) + } + case .failed, .blocking: + disc(Color.dash.red) { + // The design's cross spans 5pt of the 19pt disc, and + // `XmarkIcon` insets its stroke to 7/9 of `size` — the + // same symmetric shape, so 5 ÷ (7/9) reproduces it. + XmarkIcon( + size: Layout.mark * 5 / 19 / (7.0 / 9.0), + color: Color.dash.white, + lineWidth: Layout.strokeWidth) + } + case .warning: + disc(Color.dash.yellow) { + ExclamationShape() + .stroke( + Color.dash.white, + style: StrokeStyle( + lineWidth: Layout.strokeWidth, + lineCap: .round, + lineJoin: .round)) + } + } + } + } + + private func disc( + _ color: Color, + @ViewBuilder glyph: () -> Glyph + ) -> some View { + ZStack { + Circle().fill(color) + glyph() + } + .frame(width: Layout.mark, height: Layout.mark) + } + + // MARK: Styling + + private var textColor: Color { + switch criterion.mark { + case .icon: + return Color.dash.primaryText + case .state(let state): + // Only `blocking` colours the text: it is the one state that says + // "this is why you cannot continue" rather than "this rule is not + // met yet". + return state == .blocking ? Color.dash.errorText : Color.dash.primaryText + } + } + + /// What VoiceOver reads after the requirement itself. The row combines its + /// children, so without this the mark is silent and every state sounds + /// identical. + private var accessibilityValue: String { + guard case .state(let state) = criterion.mark else { return "" } + switch state { + case .pending: + return NSLocalizedString("Not checked yet", bundle: .module, comment: "DashUIKit") + case .checking: + return NSLocalizedString("Checking", bundle: .module, comment: "DashUIKit") + case .met: + return NSLocalizedString("Met", bundle: .module, comment: "DashUIKit") + case .failed, .blocking: + return NSLocalizedString("Not met", bundle: .module, comment: "DashUIKit") + case .warning: + return NSLocalizedString("Met, with a warning", bundle: .module, comment: "DashUIKit") + } + } +} + +// MARK: - Marks drawn here + +/// The tick inside the `met` disc, from the design system's own 19-unit +/// artwork (`M6.78571 9.94668L8.65314 12.2143L12.2143 7.69048`). +/// +/// `CheckmarkIcon` is deliberately not reused: it comes from a different +/// source SVG (15×12, polyline aspect 0.746) while this tick is 5.43 × 4.52 — +/// aspect 0.832. Scaled to the right width it would draw about a tenth +/// flatter than the design. +@available(iOS 14, macOS 11, *) +private struct CheckShape: Shape { + private static let viewBox: CGFloat = 19 + private static let points = [ + CGPoint(x: 6.78571, y: 9.94668), + CGPoint(x: 8.65314, y: 12.2143), + CGPoint(x: 12.2143, y: 7.69048), + ] + + func path(in rect: CGRect) -> Path { + var path = Path() + let scaled = Self.points.map { point in + CGPoint( + x: rect.minX + rect.width * point.x / Self.viewBox, + y: rect.minY + rect.height * point.y / Self.viewBox) + } + path.move(to: scaled[0]) + path.addLine(to: scaled[1]) + path.addLine(to: scaled[2]) + return path + } +} + +/// The exclamation inside the `warning` disc. +/// +/// TODO(criteria-warning): the design system draws `Default`, `Xmark` and +/// `Checkmark` only — there is no warning icon to mirror yet, so this one is +/// drawn to match their weight and proportions in the same 19-unit box. Swap +/// it for the artwork once the design system has it. +@available(iOS 14, macOS 11, *) +private struct ExclamationShape: Shape { + private static let viewBox: CGFloat = 19 + /// The stem, top to bottom, and then the dot as a zero-length segment the + /// round cap turns into a circle — the same trick the source SVGs use. + private static let stem = (top: CGPoint(x: 9.5, y: 5), bottom: CGPoint(x: 9.5, y: 10.5)) + private static let dot = CGPoint(x: 9.5, y: 13.75) + + func path(in rect: CGRect) -> Path { + func place(_ point: CGPoint) -> CGPoint { + CGPoint( + x: rect.minX + rect.width * point.x / Self.viewBox, + y: rect.minY + rect.height * point.y / Self.viewBox) + } + var path = Path() + path.move(to: place(Self.stem.top)) + path.addLine(to: place(Self.stem.bottom)) + path.move(to: place(Self.dot)) + path.addLine(to: place(Self.dot)) + return path + } +} + +// MARK: - Previews + +#if DEBUG + +@available(iOS 17, macOS 14, *) +#Preview("Every state") { + Criteria([ + Criterion(text: "Pending — not evaluated yet", state: .pending), + Criterion(text: "Checking — a query is in flight", state: .checking), + Criterion(text: "Met — the rule is satisfied", state: .met), + Criterion(text: "Failed — the rule is not satisfied", state: .failed), + Criterion(text: "Blocking — this is why Continue is disabled", state: .blocking), + Criterion(text: "Warning — satisfied, but read this", state: .warning), + ]) + .padding(20) + .background(Color.dash.secondaryBackground) +} + +@available(iOS 17, macOS 14, *) +#Preview("Username rules") { + Criteria([ + Criterion(text: "Between 3 and 23 characters", state: .met), + Criterion(text: "Letters, numbers and hyphens only", state: .failed), + Criterion(text: "You need to have more 0.25 Dash to create this username", state: .met), + Criterion(text: "Username available", state: .checking), + ]) + .padding(20) + .background(Color.dash.secondaryBackground) +} + +@available(iOS 17, macOS 14, *) +#Preview("Wrapping") { + Criteria([ + Criterion( + text: "This username would also require voting — try adding a digit between 2 and 9", + state: .blocking), + Criterion(text: "Username available", state: .met), + ]) + .frame(width: 260) + .padding(20) + .background(Color.dash.secondaryBackground) +} + +@available(iOS 17, macOS 14, *) +#Preview("Icon slot") { + Criteria([ + Criterion(text: "Standard disc", state: .met), + Criterion(text: "The design system's icon slot", icon: .system("bolt.circle.fill")), + ]) + .padding(20) + .background(Color.dash.secondaryBackground) +} + +@available(iOS 17, macOS 14, *) +#Preview("Dark") { + Criteria([ + Criterion(text: "Between 3 and 23 characters", state: .met), + Criterion(text: "Letters, numbers and hyphens only", state: .blocking), + Criterion(text: "Username available", state: .pending), + ]) + .padding(20) + .background(Color.dash.secondaryBackground) + .preferredColorScheme(.dark) +} + +#endif diff --git a/Sources/DashUIKit/Components/SimpleSelect.swift b/Sources/DashUIKit/Components/SimpleSelect.swift new file mode 100644 index 0000000..2e05834 --- /dev/null +++ b/Sources/DashUIKit/Components/SimpleSelect.swift @@ -0,0 +1,122 @@ +// +// 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: - SimpleSelect + +/// One option in a short list the user picks from: a name, a line saying what +/// picking it means, and a card that carries the selection itself. +/// +/// The card *is* the control — there is no trailing radio or checkmark. The +/// description is part of what is being chosen, so it would be wrong for it to +/// sit outside the hit target; selection is shown by the card's own border and +/// fill instead. +/// +/// For a row with an icon, a trailing value or an explicit radio/checkbox mark, +/// use ``RadioButtonRow``. +@available(iOS 14, macOS 11, *) +public struct SimpleSelect: View { + private let title: String + private let description: String? + private let isSelected: Bool + private let action: () -> Void + + public init( + title: String, + description: String? = nil, + isSelected: Bool, + action: @escaping () -> Void + ) { + self.title = title + self.description = description + self.isSelected = isSelected + self.action = action + } + + public var body: some View { + Button(action: action) { + VStack(alignment: .leading, spacing: 1) { + Text(title) + .dashFont(.subheadMedium) + .foregroundColor(Color.dash.primaryText) + + if let description { + Text(description) + .dashFont(.footnote) + .foregroundColor(Color.dash.secondaryText) + } + } + .multilineTextAlignment(.leading) + // Options stand in a column and are compared with each other, so + // they take the width they are offered rather than each shrinking + // to its own text. + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 20) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(isSelected ? Color.dash.blueAlpha5 : Color.clear)) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke( + isSelected ? Color.dash.blue : Color.dash.gray300Alpha30, + lineWidth: 1.5)) + // An unselected card has no fill, and an unfilled shape takes taps + // only where it is drawn — without this only the text is tappable. + .contentShape(RoundedRectangle(cornerRadius: 16)) + } + .buttonStyle(.plain) + .accessibilityAddTraits(isSelected ? [.isButton, .isSelected] : .isButton) + } +} + +#if DEBUG + +@available(iOS 17, macOS 14, *) +#Preview("SimpleSelect") { + struct Harness: View { + @State private var selected = 0 + + var body: some View { + VStack(spacing: 10) { + SimpleSelect( + title: "Shielded balance", + description: "Keeps your username private", + isSelected: selected == 0, + action: { selected = 0 }) + + SimpleSelect( + title: "Dash balance", + description: "Funds will be traceable to your username", + isSelected: selected == 1, + action: { selected = 1 }) + + SimpleSelect( + title: "No description", + isSelected: selected == 2, + action: { selected = 2 }) + } + .padding(20) + .background(Color.dash.primaryBackground) + } + } + + return Harness() +} + +#endif diff --git a/Sources/DashUIKit/Components/SystemMessageView.swift b/Sources/DashUIKit/Components/SystemMessageView.swift index 67752c8..ff26799 100644 --- a/Sources/DashUIKit/Components/SystemMessageView.swift +++ b/Sources/DashUIKit/Components/SystemMessageView.swift @@ -29,6 +29,10 @@ public struct SystemMessageView: View { public let secondaryButtonName: String? public let onSecondaryAction: (() -> Void)? public let onClose: (() -> Void)? + /// Look of the primary action. A filled pill is right when the message + /// asks for something; a message that merely points somewhere else reads + /// better with `.plainBlue`, which is a link rather than a call to action. + public let buttonStyle: DashButtonStyle public init( title: String, subtitle: String? = nil, @@ -38,8 +42,10 @@ public struct SystemMessageView: View { onAction: (() -> Void)? = nil, secondaryButtonName: String? = nil, onSecondaryAction: (() -> Void)? = nil, - onClose: (() -> Void)? = nil + onClose: (() -> Void)? = nil, + buttonStyle: DashButtonStyle = .filledBlue ) { + self.buttonStyle = buttonStyle self.title = title self.subtitle = subtitle self.icon = icon @@ -58,7 +64,7 @@ public struct SystemMessageView: View { .scaledToFit() .frame(width: 30, height: 30) - VStack(alignment: .leading, spacing: 20) { + VStack(alignment: .leading, spacing: isLinkAction ? 6 : 20) { VStack(alignment: .leading, spacing: 0) { Text(title) .dashFont(.subhead) @@ -78,7 +84,7 @@ public struct SystemMessageView: View { DashUIKit.DashButton( text: buttonName, size: .small, - style: .filledBlue, + style: buttonStyle, action: onAction ) } @@ -91,7 +97,12 @@ public struct SystemMessageView: View { ) } } - .padding(.bottom, 14) + // A link belongs against the text it follows: `DashButton` + // pads every style for a tappable pill, which leaves a + // plain label visibly indented from the sentence above it, + // and the 20pt stack gap reads as a gap between sections. + .padding(.horizontal, isLinkAction ? -DashButtonSize.small.hPadding : 0) + .padding(.bottom, isLinkAction ? 4 : 14) } } .padding(.vertical, 5) @@ -110,6 +121,17 @@ public struct SystemMessageView: View { .background(backgroundColor) .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) } + + /// The primary action is a link rather than a pill — it points somewhere + /// instead of asking to be pressed, and is spaced accordingly. + private var isLinkAction: Bool { + switch buttonStyle { + case .plainBlue, .plainBlack, .plainRed, .plainWhite: + return true + default: + return false + } + } } // MARK: - Preview diff --git a/Sources/DashUIKit/Foundation/Color+DashUI.swift b/Sources/DashUIKit/Foundation/Color+DashUI.swift index b81bc06..8b25187 100644 --- a/Sources/DashUIKit/Foundation/Color+DashUI.swift +++ b/Sources/DashUIKit/Foundation/Color+DashUI.swift @@ -260,6 +260,7 @@ public enum DashColors { public static var red: Color { dashAsset("Red") } public static var redAlpha5: Color { dashAsset("RedAlpha5") } public static var redAlpha10: Color { dashAsset("RedAlpha10") } + public static var redAlpha20: Color { dashAsset("RedAlpha20") } // MARK: Tokens / White diff --git a/Sources/DashUIKit/Foundation/Icon_DashUI.swift b/Sources/DashUIKit/Foundation/Icon_DashUI.swift index a97588f..88e0398 100644 --- a/Sources/DashUIKit/Foundation/Icon_DashUI.swift +++ b/Sources/DashUIKit/Foundation/Icon_DashUI.swift @@ -80,14 +80,20 @@ public enum DashIcon { /// come in a plain and a `-purple` variant — the plain one is tinted by /// the caller, the purple one carries its own colour. public enum Features: String, CaseIterable, DashIconAsset { + case friends = "feature-friends" case identity = "feature-identity" case instant = "feature-instant" case platform = "feature-platform" case platformPurple = "feature-platform-purple" + case profile = "feature-profile" + case recovery = "feature-recovery" + case selectedList = "feature-selected-list" case shield = "feature-shield" case shieldPurple = "feature-shield-purple" case timer = "feature-timer" case timerPurple = "feature-timer-purple" + case username = "feature-username" + case usernameBlock = "feature-username-block" } // MARK: - Checkbox diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Colors/Tokens/Red/RedAlpha20.colorset/Contents.json b/Sources/DashUIKit/Resources/Media.xcassets/Colors/Tokens/Red/RedAlpha20.colorset/Contents.json new file mode 100644 index 0000000..fa9afbc --- /dev/null +++ b/Sources/DashUIKit/Resources/Media.xcassets/Colors/Tokens/Red/RedAlpha20.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.200", + "blue" : "0x42", + "green" : "0x38", + "red" : "0xEB" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-friends.imageset/Contents.json b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-friends.imageset/Contents.json new file mode 100644 index 0000000..75308eb --- /dev/null +++ b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-friends.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "feature-friends.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "feature-friends@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "feature-friends@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-friends.imageset/feature-friends.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-friends.imageset/feature-friends.png new file mode 100644 index 0000000000000000000000000000000000000000..cb7fcd96945b4a321f33a4d6beacb2bceffc51a1 GIT binary patch literal 805 zcmV+=1KRwFP)-#cGj(1H7nRjI z7KEL^<~IjwaD-XnuBfmlTkg^K+^=-HOeEOjveS6F(!s3?d~@*@;A4%)hy1;c%DP`W zy*wjeV>s?1B@2xwD;^Ayu!6P>4RcWGVz7efcx>De19bokECt3M?HDw)6@Fxty;=Kk zS%e|B0v7_2d}eBECs3ecY^y?BA;ly719k#+o-;vkFuZ)&3D-=!w}WFCVka<2!s75o z8=;eSs-R!1?|Ws^0TVUwAX6ROia^coYKgtFkLlt(ehR2O@TwnwIv}w(Vc2rVu0`fGJbvn<)~kB*!{vk+4HzNaMt&u>>oF4;*J90Lto5p%_P7fYThXu6 zb|ar{-?#;0#p!6BL!3sDi{?e>xm0RxNOBegpP=^dXR@uhr3M0ZS(_|b7U=RP(_#K2 zYV%KfIh*a8Rur=+e*L8d5r3hliW4ZIvSyv2ueLB{RC@(oO;7 j!r^3XPDn^dNU-M*x-#Q%_CI^Q00000NkvXXu0mjfdQ4@N literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-friends.imageset/feature-friends@2x.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-friends.imageset/feature-friends@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4412419e19835c3457d775965b4f3b1e91fb4bbe GIT binary patch literal 1437 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 HeB8eQkQ2|E literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-profile.imageset/Contents.json b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-profile.imageset/Contents.json new file mode 100644 index 0000000..f07b527 --- /dev/null +++ b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-profile.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "feature-profile.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "feature-profile@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "feature-profile@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-profile.imageset/feature-profile.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-profile.imageset/feature-profile.png new file mode 100644 index 0000000000000000000000000000000000000000..33a0363850ff0f45aa19cb5682abd9f07ee57ccf GIT binary patch literal 792 zcmV+z1LypSP)EGZjXyRkJB zYtl~u5$eEHbp@g18#IyFNj^cs#6~j!ODkb1MM!0dqW()m&b((UsBP?fuZ^oN{z)e} zzBl&c_wL?%4oFByNVrx6W}*7q0Y(Kt=Ab<1T%Qy!;87EORPtS1k;@CL_LiYUiLQ#& za+d$iJ;COe0o=hDMwGXLb@QC$ zGpJ+Cn*ac@6Sly!< zXD%g$2bsve)x|qt$OW}D8YY9DZJi6n9sK2k#=6i)V-~1{1BTY_adh9#F<68&CV|VK z1BTkyLv;5?Q}?@nMyPEcg2()hG-jkfR9O1BkMfiG|8ws1-jIYJAP#00IHCUI$Zv5I zi$EuvMse{r)xejK^B4f#TC0V7abCX=vW2hl}3jQaN@iyg)M4)Y@MRNo|SvV%ln zwWBEQ8LuiZm5DT(Q9H;*bc~LTBrS}N#w;-KY@rWu?(yO*Vp+G%@5Ms3!fv;APGC#p zTjc6)f`MPfV?hoWQ?8@7ec%`4S7Zouh9*{Pg5Ksjy;C%3p>(J&hN9>Qz+mU7Km7U> zgVn!C9dGh$T#%ikmZxjk2C2z?%j^U!pb_jCjh zqD-H*N`md|n4s0_(Sp!(ZvDEBongEqLo3KLO5seq651hjp&wB#eQTGkgoK2I8{i*H W5z)Qe(t|Vr0000;v6bb$)QGYqK7(g>Z$8f zxh4Gz>PXy!8{nGe46WfWh$C)rXmW#7e4$rTE&jX_U_j+Z{Dv3JRXn7aEuF*AG+2i&Pv(D08(hZjQop7l;X`z2$>l;*fcgFKv*tf?%i{kw?}32O#Hvj z?>%%L_ptI{7twAXA_y&I83FJ9X6>Gt2a{$vg^%CAaN?LdR>YMD&%lP9?%U-WnD7N0 z*uE_=S-nw0{mSbjjcxt(ClFCP$~v?qj?Q1D5()=hTP!^ zNV_kSxp&+}xL$S>{{;@yYR>Qk)T#<5rE~7faJneqL};+CfhV9c6)@@U8Dxqw8475E z%V4s-`y8&2gU!aMDKOy9Kz6!n{pUTnLjE8d#S1^VyKn_e+B^I4krWb7B&;*p3Mj(D zcTcS?)FY3N?6Rl0I1eywpiK6KW>W3jC$TWY$392xnLQzie`pj8WFHZcE&Od}U8QOt2tmEB&xG%Yp zL|~A)4WxYF>W9;DE#xFfR66DlvNz;Ni`GaS;V$6_3>IzuW9y?ghl=+yC@W3xrEE3} z#k&TkfO;E_Pz~hEkQK+rP&Rw@#x$HU<_|Dn3aFe4GD&M7!(i1&GM#}&b&wF(8ptr3 zVaaXBHYx%Go`3-o3&^fxoB++l#AuWF8eW9CgnJRF*CYY7lcvShi)WD-W_JQlKq^YB zWV4WB+q6iW-KOpYCK6@MjU>Z48R%go&QMc7H50yoL9dl$IAxzjdRvY_pPAUHre<^| zGJ)p3oz0{L_WFD0vG~CSwoQzGkRP5olm*NNmS)%uQ8EI4ZVT*zTYDP`NZkokB&QlN zxj9GU;6o@Drc@WTy8UQlV=@PkYgI2-?QMj^EXD9$Y-RN$}JdulN+RqqO575P2)3r zZo;IK(x+cp6>1WWDjSM6RAE-~-jM%(eA>h=POvD^gq{czu5VV-67Jux85e_Q^e}D- zPQ%?%u?NtJLraQ|n9bv(e{EI_obRg52v?wj%LkoamH4%hZn&nyW|Ni8N%pO96E-t6 sCeb_|kH_Qjcsw4D$K&yMJf2g^-^?|yoo*oMPyhe`07*qoM6N<$f|aj@IRF3v literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-profile.imageset/feature-profile@3x.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-profile.imageset/feature-profile@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..03c6c6066a5bff60b70bedb04fa891042bac0237 GIT binary patch literal 2048 zcmaJ?c{mdc9G+!1mN{~K$T32h+&(1N+SqcJM15k7xo2!f=B%+u&XRH;AIW{R$&@3a z++DPsrSQ2v*HGm1J|_dLJ%_df6Yd;k8ui4JyFf&$V4002M`i$PsD$mHM>WcTvAEfo}4_$r>(HJbICktN3s7I(BEm|YGKg!Sd%MUNK&jjBu2jd#iLGU z%p7fN{_dy9fl=)(OIay+6p(@ZZVhS8*k35!=iR&#Yav4P9V3r=v?hojs6<{o@<`uC z)?Wc#dT#oS-zR}HeyhxYcy+iPa-=>0<#_PoGih#MQ^ zyeFF60A@aw4tjM3D^<~Xwaz&(Uaksmal&f)jXj#zqWcG1yEMlyL!DzK+PwZ1#9Mv; z=xuA8v7rWfpZ_roVC^c&tNMD>GTJlK$VSKxZu#X{wp>{Mi`~s#4iP@J7p_y&Q@PVJ zU$`2r)5tv&yY66?1SWHP8Ev&@ zKAe(x8F#bJ^}j54uH9?T?O^C^jNQP9j4fChbJAkz)%T#;hV?OhF{(uiCxy5Sv&+@P zjV!rd{=5=q9eE7c>q45Pts()Ufzg{@h>);JMwbbD$JnpUG!9~#p!~hsEAlY#Nh-$M zENk#N=V>!<=nP^m_4?8d0Hs9$0wID8Hd6g*<{!3Uc-9{=+oW&}YO(>0X9raF%`7f` zjqkRg+1kj{&9IBgK@Xy4BSq_@!Uj`?rt3$@-r<*TgX7QuZuzjEMv~E}o zRZ}Cl9-o-#rYK2Kh1fVO6Q{NmDCP)&1}`9*(o5JXN7fizKy_m0^R?P zReDzZFQ2u-`jG~P4c&X(^UAb0+(JesFYL1`oNVf{YoJDYg%caIzGPBS)wysBFP_bJ zNiRf0_U?7&WCTIXcBplG+2Rg=(}|O;$91yHezcX>?-lN#@(kA$EmMW)9pQPFG`-)u zQ%+0W6)}yo9j+r+rNye>LwcWDE`1;@_v7$$9Ntd*jJ~D_6#$LeV1#1z&U4K_e=W|? z6DoJq&q(HF4%&`lbdy!G@LL`4LAALtf|B;K07(k5g^xz@GIa+ZO9s|9^4om5AW?*J z@i$&ft9i|b$AV4cDm3m%N-RA_t{RwFF$ zr>B~IKZI-z3Y!*w!h&Z6)(KJq=S-)ve|pf3G&o^DmP@_In~O9bUPW-TIv`|XMzBg9 zhUpG=xa@Z3E!f*Iprm?Q+;ECQ-RM5IcIWe}h3EfS(pc3ez=+~PCawmF&^oSfFat05}z%WsbZsW;uHAA1&L5_`Q!v|2-J-y*C+Tu6eB zE?@pL9&ob&aaFv9*?3Pv>Eas)j7P12mBaYP!=ac(wUrwzhX*#J0pO3uaa4kcIq~$; z8&1Ze=r4Rwn{bw~n$VU}!HZIb*l!^c5S9%x@ksn6;Ed$m)da zhO9|w?SFUeGLNi4OlCIDfQQb=Eo<(%1UPI>_4N#tSCXlUv%m&OXrKI1H&k+0L8OF! z`w6SqYXRY0EWF)m{EHN@eKl$@Jc9e38S%5^k^-Tn42|x)a7RM~vf7xgArS z^}kAR_Ip`rhb3OK{9GKBJT^iV!Jb%3T0ZaUA$gHrCdXEmO*GI;UB>kfdZl8V>Lx@4 z8u|NeY4T8PZ-n{?VMuyiUe~J7lbB(gRR+{oe z1$k(re);1@4%#6~GeQvB5NPERuOOQI*6m1JigQg|+?FMW?(~0MhjyfX3DV2FNr^f# RTya>?04&-LRfF`n`48!Ht}Xxo literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-recovery.imageset/Contents.json b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-recovery.imageset/Contents.json new file mode 100644 index 0000000..20a8ce3 --- /dev/null +++ b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-recovery.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "feature-recovery.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "feature-recovery@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "feature-recovery@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-recovery.imageset/feature-recovery.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-recovery.imageset/feature-recovery.png new file mode 100644 index 0000000000000000000000000000000000000000..6ba2033807f053655ec7757bd39758b538b2bfba GIT binary patch literal 559 zcmV+~0?_@5P)b>Q>x4 z7Ea)J13ZD;Ks@}`hr{9c55#B@y+@!p zLtY5RIIlIO#>X`%>t$H=(XE9#84o6k1Sen!_}~Jm!BiSXZHoZe4T?Z8LRcH88iTEB zJ91!=B(D{q+>#ZaFWdfPvwa;KI~w!$@vohN5=_wYGym3QB@rD~5jJAby?dSsiU5~5E)!WG_8XseH4LsLPHy%Ig1HmMf*e+zQq3+MIdHBiy8D!L1zIU?dp+0W^&}> ztc#{iK8wf5%a4X*hgjUj%EzXTkUvEGX#zbV^Qz-`q=)c8`{c5D5B883g7oG|TFT%o xwao!C#bZH!{Kn`;v2ja+X*GPwPRn6F71YAgn6wOGn-OdS*+# zf8shEc?cl9v?bQ0$d^0k_IElTPv9Y%7N~YGN{5+k6880w@)I1n2XJR)PgUel-BdGA zVt^rnXsyur?<^mvHgLaEJ@hg9*jFoCAR;1K6hFD}hwtmEXMBl3-i*bh8ft{mKKc*G zh2xX6RS0aV(qmK>4zT@b`cUQLSx?={^Gx|=WT1L#`C0Yc$8h8xKvQ`f-LY2%@#<-2 z$dUYvA=0m?Gw{_znC<1Y%(mowR}D$_P*2|se;oM8M0QK|sbIBxnJq%-es4_l%w3=Ur@iS3$!L9tMbdQ zH-sY(0gN_GZaR%gmmUwYB~A|1@grE5qk=aJ%}AS`IJ|i!gdF4b z%J9zP-~Z?$3}RFjUJdS?{M2)UY1GY>Uq;X5=*b9Av(iLF|0_S8i21;y8ga~O#8t=A zFX>p!b=uC~DQ25d4+|bXYns&T6x9c`I!9hBSJmtcAaU*(|*Ag>5gPqF$ z^xoT~fLoSj>v8Dd-&mn~9X#6`up_amX0K07%9knoZ~kmswm!ZG(Q;m$3vyZf^`w%#>|rW0|lnlCz+y`0kzZQfRx^n91=V9G4{`=JdH5fKp)5fKp)5fR-B ZzW`MrG9lmoymtTq002ovPDHLkV1k`l(o_Hd literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-recovery.imageset/feature-recovery@3x.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-recovery.imageset/feature-recovery@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..41a14894204c28a274175b5e16ac2353ec8ec670 GIT binary patch literal 1259 zcmVwh=000000000000000000000002s79%MQUu}py+>j>ff1fY%^7`BI{!;osT}mQp z3Osz<6FdD=tn5g0<&v3^^nW;+`zl}9;=xeF?zw-}-C0MHt(`2qQ`MkfU|-x~`j7it zPud_egSkaM9lw$|+v0Ypt8MLXb0*q~;#k^JT(7VL`ThT$%4MhH#}aQ_+^Iap6AWt~~9Y&3)}rci;VaPVp;e0O9nZ#CRnQx- z;I^mpSd=v|@GpPHE1sTcHikQdtnV%Kd7?0fqO5s=zcJrcFR&6z~7JNSbr&TGS!jm%?fub>J12puiR-}4`1s8e_4+inD1ZDvcF4rKtOy} zww5FCFC=bYZsh06=ZmRgFt3Ff{&$6r;o({NP1X}-DGD6-7!yjQvWw zfE}8Z-(*V^_~#OXKl7Y7Ai(FWZLwT||8-zq*T;~2{;c@fEfEeHed8B%J;*;%;Gcf# z%5OEMemtu)wZ&b3zi;4uF`uQKx&ik9nZ%v-I}(fc`jX%w}VCY#Vy3o?$_urS&|w4ew0Nsi|cKa_udF8arc12^h94zl(jGLPo=QAXe01Xz&CQ$ z{NT>S&q3DRIjuXJR@Sb-UxSfHmCyULTU#86-g@)$GEG@K0)GuRv-|tq@ZAFZeLHON zDEbcS*X`S6DeiQ4Tb;l^wud$DQyFUjL7jC2000000000000000000000000G(+8y_ VqZd%8ww3?@002ovPDHLkV1k#6T-5*o literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-selected-list.imageset/Contents.json b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-selected-list.imageset/Contents.json new file mode 100644 index 0000000..eb1c600 --- /dev/null +++ b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-selected-list.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "feature-selected-list.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "feature-selected-list@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "feature-selected-list@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-selected-list.imageset/feature-selected-list.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-selected-list.imageset/feature-selected-list.png new file mode 100644 index 0000000000000000000000000000000000000000..cf1832c6ee86ccb3c380539c4fc7b9fc97777621 GIT binary patch literal 450 zcmV;z0X_bSP)z`mB3 zUtZzF!rn6!G)y~1IDbYGY2=lLx+o|TgNwlw&d0lC%@;*WsL}YGMdFrMC;yx_3O=&t z31}H#K-K?diOmRx${VU;yn~ygiu;RXTN0Htf@2!N7;I<+#gX_GN%tSJ7D-sUrn2Gx zVE*w)MKbe!H#?(gcWsLdPZ0BR)k*jmyg?j^XOVPmka1;M1& literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-selected-list.imageset/feature-selected-list@2x.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-selected-list.imageset/feature-selected-list@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..544de7213989fdc869b61d68fbdac6735e7f446f GIT binary patch literal 693 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)oCO|{#S9E$svykh8Km+7D9BhG zFkMr`+|mkcM5 za@@SFX%@k>wzA@$Ue0InZ|a(6GyZYinZHA|zPLr@mold z-Oh34Er#b-pMEvIum81uwo>l4?kmrKiZGs=xkPW_+EngP;~9%elHY9Yww8{66R`Xi z1Fv6#l>El>_jxg=zOlVp&=6$GC^hGFsQSUJ{?qFIFD=z#{!spDw#czPPCaXW_pZIk zIOUrBv51{te>3N2xGvhyRBK%N;;zZ{gUZe6RR_!^C4@sA@3XH{?*DP>Y}o6rLo6@U zgJioKO@Pb5`=$ znRfHT9KbLP5q{{k^G`niwHk*#%RHo#b3McVPWfl_bBQ+hr{f)SwReB|7q{7aLD0Fc zf$Z8-W;ZfTIeEQGV}Zy9{xAOOXM$Y3!}w}X?tIAaEhrf15EHWNWBsmEvKAK>O=1b3 z)?Tzl@6oix3qfnUS)@Fs@0DtKUfUY9!gXm?M_FLz1+52nBn&@4jGW!rfds^2m@Zn( Wo?x|M6DKfLF?hQAxvXoo1K-6l5$8 za(7}_cTVOdki(Mh=srdB_P~4D!u|7CEm>x?GDJ0h+2y}+<=#O1Fo1%tU)a5xN~P(!?TZ_Y zt_Oeib6+txw&A|qyY}aQwuk@vgqn*fCWrzgdsBaxZ@rC$H1~>PnK(%2P^z+1m{kXz1FHmVu+^PSk!xwr0gZtPUCH_#d4d@^0!B%A8c}q@#6|;nyMDRCW2S@v4;4(r%T-CXnWpl>v55J z@Md1n(-gBSJ?00r>@-9DCZDyM>#&FMT;J|cIcw9WEF8;CB|GCbGuJey{>)N}UuV){vh?=GI}0_wRejiZ>B^ofb!zDy+iwJjq?umS z)@ZCMh>~9a=j@x^?lKAg<6gcx>9L2=ue|hO+?>_i%a_JzJ`B3SqWd9l!Sv5*n=QeH zd-(q8?91ZJRGyft=(K2Y=Ij?8d*uFppeM=wB0Neq?sHo;kO_{*I+FbvtyG z1;Z!bNnUzt`G%S+y!|cz#qt~4*DVhbEla&r!5s5WG4@^P#7j~Svx-aZbKc*pi;+Zu c0uA0jId`hvcSt+zyc;C%>FVdQ&MBb@0CMh$1poj5 literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/Contents.json b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/Contents.json new file mode 100644 index 0000000..513f3c9 --- /dev/null +++ b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "feature-username-block.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "feature-username-block@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "feature-username-block@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/feature-username-block.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/feature-username-block.png new file mode 100644 index 0000000000000000000000000000000000000000..710bbad03b402777a9dabf74efca1337bfebbf59 GIT binary patch literal 673 zcmV;S0$%-zP)i*-yq5jbwiqD`mXJ8!2g3kVTK72z#3PteZ-N!yN5AXnpc<^#M8LIVK zp^01y9HRmp2L~_y{QjZQstlT(GutYO-$FGkTz21Ta#EFpu@mb0G&bgR+Xkv&}91B`E`|qMVH;f*~?i>VS{VJNx$hBx;Gn zyfm>IR~V#FV<r`miD4+rS(a>{U!S#g>pfKCx-62Aj>KlJ9R8%*oiMu^Ma#f#eAV zs2b@JU(bTjCR0^$%E?!td?nt;blmAORkd6ygR7M?_fQc`+|xwQpgsq8W|NWJJ=Be6 z9@6j*cczn(R$ur|V%l~G)JMVgYju&5T`&tRIIlSW_I%HwCj*)MMWYwpIN?;it@q}Z z#~5vp(={EBTrqd=G_|2Q;N^@yy?K@#zge9GH8nLgHD$O38;`qPM8pea00000NkvXX Hu0mjf;WZ@l literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/feature-username-block@2x.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/feature-username-block@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a5c3d9a222b90c645055330c014c00951c973d66 GIT binary patch literal 1131 zcmV-x1eE)UP)wB9SBkzFGdWQ<(96*W0cljX{fB*Her}gUDY0 zzPdi9;xJ)&UZ-&Hd30@c4Ut6+@YTxAEo-q8vt+-(KgsWxZ6l&+CG5jv<}PZ$f;TVs z_U<8qC;|Gi^&<~8VhlEZdA$2Cgoy{}9~i#p&8Td@Wau6&zezt>#CxD?-j8Y+p4TpC z0>VTC^nK6$(m`FhnZVJ?jV=U z4upt|zy^)JDX~!~G6D;XGBk_WC=>~hjJb`5A*MedAh?7ZSiUDF7bZ)Kc|JgZ;1Srs z@;y=Rf(HsOVWO0~ETSxI@Mjn3fpi58Wd!=*4+zE`ROA}(kO>bUgt$C_@}sGPN;r7Q zDG*@grQ2Z4lk1DUu;zW%i)DARM{|d$whj*tF#3-4S(fnM;(=^<-tlWz-L7h0fZrO4 z04ptr3}fBT9U<)aHUC0l57$_xIQX*%2RK2IqoF3&<_HuV z;K%tF8`%{0BLz+}1%GVPKa3Rm#<^YNOk0wFb zky=iQE#;ar3jI#d#7@NS4~hlYlu>Azwh2&> ze`3eCV)VRE-UcAfi6IF!57g#B3rck&5P1Jf|Ap{s`dRgvHCe`6g^HUrIb=iDW#NBN-3qLJ^(l3f2itlaXJ71002ovPDHLkV1mh_3)=ty literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/feature-username-block@3x.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username-block.imageset/feature-username-block@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..302396ed1098f208f5a96ee064b5fa82f5bcf5be GIT binary patch literal 1629 zcmV-j2BP_iP)PuN6bJDCYd0PiL>f!M5xe#Y7#5Lil0_SP zksZDP<^+ZlU`~L20)`V{c3JQ-S%g^1g5d<|tOzEN9cPqeJl*|L)yzb;GY-gfea*bz z0tv9>nf`ZGzpCo4280ko2qA@ap}IVmA97x?=OCn3~2BI{8Al2tU4BfOWO33#ve` zxgX;|RoB;CR@ct1!Frslhe8t^uGSnx`D6WE;fS+<^&|$u8Va2cb1U!n?!gf!UG(1K zjdit3^AJjCTWE~cSt{;%Zi^&6Y8%xV`M;2d+^4lXo4 zTd=t>VYLP4QLd-)5=^h=(b1vRSuU#qGxIv8oGP|9Yz~{O1zc6m)7^SUn*Dxn9hO)N zn4)L{q7s2`!V)V**08wx3F2Qxqgc4o>0~g>N(;{EXbs{7>c>`&iIspEk+m)<6t=o) z<6ZQAnl=ahnSF@Utc;0`x8OXFTM*+k^S^t>kB#{*x+o>!GZcjI17=VHF;+x;6^2RQQ_|N z8pZ8}PRGe(B5FjimG@^Js`E9NalMUkaBUOoITv?bg{#e}-yN$H4E0H1n0Jq^oQ=9y zcFj-ieTWX3s)xeE5KzGWP&Gv`dw2vCFeff=KONBvC+f4L2!xG*Gt{FwIQruO9Ls4? z=Vvh96Bw_eYsFaKTUtp8ysfu9f=zmBCeTf;b<-EhV3>_@a93Tk64WPA2l@eu8VA=O z#l*S{8*jnJyJ$6A^E<>?CE+y;Gaj&7_Y7u8VI75T|uuft`D@2;V`RP=cPo0xJQRW!{E3fjQrS1y%xnzeph6^f4V< z3n%uTY(QzHO9}$9wcU5D1spfZHW1D#j6s>ry{D}OtgC4z+QyVC^*p*oR+kHBXLG&T z*)+G}jJdQ@T#Z-_c{u2bGdR=W^Ec?(cALrepEBXKpE6)s}AnNAqzMvY% z+gICZZx>fLiP-DCrAswo^H-zTbop58|M5CtZST73vf0ZnTV76Cr(pr+d^2%u=Zv#O=6DLI3m5VX#$Hz!TxzM-}1FP9w(@Pk9*&uqg&>m)uoQUpRCs? z+aU!z5^TUDRjVe|s;m7lKRxVfxC83yP;)MKeAqVPQv@IIY1J!UscaQQNyKzjeZwX( zCu3f~mFYj!0q8P8tx@x9{Tx;AV%`n0r3xX05JCtcgb+dqA%qY@2qA_>N4w)SR!36z;V!~8qv zYXDMGQc^AzLXtGT7l3Q4kX(e|uKL_X^_Gn}NKRnmR}pfwr#=;@<^|PJy}gN;BqK0P z+!>Nru)f?z3S^90z??10~jyEs^^9j_h z1%ftgddAMl2|+Eetk?FBEf_MNz#!K$zgO?y;1#Gc{QAkzc88-?7!qG#kZTDxGT9yV zspnsiE7VsOd!(Nc6>+Ho!(7YQ46S>or$yHUTI5|A5>FtHcHwm`p^LSpX6PD0V|%uF z9Fr!@F0k=)Lmh+Vo2Sf^^KWH4VC9*pAGoL_d6-S0Q;@mto7agE)zV0{KgZUM&)V1H zA`Hl47Tj(TusJma)0<~-9_!Ru;`3j_{I8M>o z?1fynvQ#H1wBDf}cM*Y^i^E(S1_#90*&Mm?$!ym4%mni2W8gfY9e7&7xPr1-*IrW5 zWKpz0eaB;z1D-VTVrfOaN*Lju8dsI#mJ-}Um_YSixxrxkoD+`0N%W~v=KPNLYDveE ze(gBpcZX$UaNu9)bQAmam!lqwOdbYApJ27rQ}MSsAx@2DRhJcADr%jh-CCz#nJOEu zU_X=Hgk>3t2-K`E*C8?`=i*7j5?)}nE3_CjR-rm1q2O6!Zu-HCS&s3KI_a>8yNaR%=#2}Z`0(%$j zmF&+>1KxD{Wy$1Zcy4&Is6BF#eY`Rg16P=6NM_5KP#26Sahm^ng0=lWmI`}|7~QQ$ h3oWFiq@-Lue*xWAB52N%($N3_002ovPDHLkV1mw5q`?3H literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username.imageset/feature-username@2x.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username.imageset/feature-username@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c03c4b4ebb0fc305da6e0709143914ec3d0f55bb GIT binary patch literal 1646 zcmV-!29f!RP)fs%s(>*uvjb>i^XEGSS%Kc#bU8ot_=v&OzmwEBtBH1vIqo@cMhSc+D#C& z2j!&(iP}HiRp_#7DG}Mk|L>; zM?l`Y#+t@Yem$8cIeN2zQ?z zo|ZF(d{=?QMu*<`m-jE*0ue@5rid zrV*GVbuSi<02(PAaU&#$MTB(v-h`wtVA9mRs&Ej*3m6ee6d?F89576}0w$f~8&x=x z6LKR`6hfk#9!#ekfeGgLv>XTOL?B$|Mi?OrM$2w6t zp5wa$UgH2%3+rmeX1m2<)bIz=q;q#jBsCM$RKhv_XF~@JVqQSqIliQLf@+C(i5odz z!8Png%=UpMr{^ajLpNepKohBJf(+e=IRVp3-Eqj!jhO2L5mt4(C=W@Ss#>ZqhTo_@ zi+5j{{9=(QEwwr~+{NO6LCoxp@rNz16WmdU^~Fb_GrX&S7se63RY%Jc3o1GbP`>R& zp65P#a`^cQ;&ya^JuZeKkO!`s&w?EXj_|ww#vuOl-H0css=TNo)T_Wr@!W_f;OyC1 z`|QRigI7$t5kq~TuEhfGgf_MOE(^N8RE5WjB`=zI_Z7h&=n`-v)zU&cEjMB)0z0m9 z4};PErEv5;UaUh>{8Vgsf7>m+<;jfPh?xlVzu?vD7k=uNf~UA|;=L=+j{aK)^F&1A zLUN8lH)5^|udA7q?N(a}wGkag_~NNq=4k5jxRG#0LI)~`C$4ALfcxm!aU+U1 zgO))zV%Ey7d+Avp6acQIyefvBdcPv}d3JQXED2Tur_^#MAB!UR={g#I7^E>2x!^{i zJ*aaA-AFo<3B!sy5>8#&lP^cUh;no5?wkt~Lm`n+3Ev;4nsg(XuM1aRy)W{ItO3RI;6M-YB9G)uCxL=aasMk@Y7|jTc)j0WwdhQxUhu%c~ z12+vfl34^INZzaGT=qH%Dr*&v9@aqzF%!Nnuv3dMwKkHOfY0yE?XZPXQiTS?oVF^z zN*bx_!~H8>QDhjR3yV`i_QI=mhTMo5FFXeQ2pD^~^5Sh33b=Z~GqZT#FI`xRsl57E z*+MZ~AtX`Sx_`3)6Ox;N#_%U09Iq`p`_bW2pUTn6E(C{7h6K#4h@^(buanMaV~UUh z*FOSdsQym#A5KmWh^NTT;faOk_dYc;n7;lI7{QCSE;vYXo(kg5hvSnqLJV&{N7x5L zOy{pPC%`XC-3KJ#CGUidiid#B*;}{DV2F=4g}q-W5j;e~WfgHOqVmvkZ+32Or}|Zb s`Adt%VzF2(7K_DVu~;k?i{<0te^m89*Y4sHp#T5?07*qoM6N<$f?U(^;{X5v literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username.imageset/feature-username@3x.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Features/feature-username.imageset/feature-username@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2ae35ed00fc68b8e7bd6790d6f8270688a0eae49 GIT binary patch literal 2355 zcmaKui$4Ni=h-kj!0Ch}P2F z8p)jrLr&z9qm_Ft8N%zlf57>Cp6B;`zR&0R1AbID7keo%6bt|Wq#PY=&hKZ`KNc6= zzb5`h!}|duIAFp605#)(e84v6l+S*7And&TSwPLe;cxpO9AJIU8USEqNc{E{*=K2z zEt+&81V@VW4ZRAmyB-kiTgb@40s!JQjyBdFQ3sX_rtuSY3Y{&eMvS2&qxUMs|KiPr z%7?U9Pj`gr=GLFI9kTI?F8I>PpwwU?6IVALXu-nz@|o-Ua~9TFW43yWK%zQ&IDa5> zSHt}=O!}FcZjoxS71TYwI3w&!eY%XozX%}CyEhPbVQ(Vwv)#Cr*jg0i&{E0z z+XFdw`I-dYMvRZQSlPWONnePK*{zL3&o`Q$8uL&-T&z5s`yT4hWn{h=mwm@Oj?%KZ zF512AX<79yT@PKiRusQfoN%1g>ihm?y7|%gBdqE8)D%WDFw)zwILS5B;<&a!yS-p_ z6zL%j>2sR;4D}qe?m)Xx&b`iO)r)o>WA6sb%lmB#OP@7hF?3=ZlfjHwU~)+`@r99t zseX__DDU{gRvMTyH*yMnJ>4kC+^m`PeQDj)sr!fc#tbEe6pdUr75?G57~F%U)$eFU$6Bt@$t;~$xZSj$MJZqq7vBgCk>F%qB(>L|KwS-pw;JD$R53Fw?vmwO%v%CE~jNJno*0PQEMnZJ6fn=e*T zTMc@=9lF&RP_<4DI1jU9GGwfy=X?=0V;B0)n`e<==w`llWYh6(g@{@~k0Nzh@nMvp`>O+2}G!dy3YVny}Lq^bU{`OQ;u4P%)fpmaEbUYsCC_Ruo*C4-BaJu5Q^C2;Fb| zTY|L==H-lI)ZS__7TVH>n=t#7G1y$+9+(fV$IQ#_L(m+W@kG_@0HVqq$dk#FGNQ^$ z?IlXd6PUdkgfVDH_kj@~WUggXF%Uln?!85<`lD9eY|9?D!y>_=>O;umH43xaGh52f zZ#4C&_BHuGl|HnKzT}Sg{0>asj+#IV&$a=UlkO=^*WG& z-$0-dnYkMF3V-9|_18)fw1SctWx5CfL4v3%9u{a;^q6i^wJne+QyE(@qBqNxZ z<5an~obr|M@$;dGDr1V`n~V?gAiU?ul1?w@n2S$jN%T-VxJ#4BOp&P{AI#SW;T2?- zZ6{I)%t?u*P}N!qrxjd-;`qFUw_?^LClpSYG2{^x9;i%rlUMTozjlpnQJ*}{0?Ogl zAcsy&vdOl|TZTK8^2VMWGDZHnS$@GeW@Q?r>b&j&Ox@S2*0?hn&u8iylCmob;7nDW zzF#?{_C^e@qU0EbKJPN)+XCu6bkbR^sN91#AZ))@Mz5O9jHqe0S~GX45$;{YHOS?L z9*TEX+xwN@M`%;2qq)CL-mTnKuWFD`X~cXQ(wf5sn(Z6*5wXZ0j*A!oOaKiNtj89gddO>o&+JunJOP-$HUHtGBur zH3v%1MGqKFcnQEg-krLN+TsX|U|O&lEF?`dI|qH)5t{c64qLS_>$S+iv1P+hOOd?7mED-Siybt%M*xOwCC8u>3=``1HN*{G|b+&D4ujb15?F#~@ z*f5{&)RKewnFH*iZCGEwuUT!xZ9_EoOQlvmorq97LwBo4TAM{&YkNCdv0fyIzQ=ipO7$K0--0L? zdjHRLuIcq*+t?k68${MANavRSB24EmPKsp0mW{a>u;}2_-qG-SFp9rFeEd#hyO1uj z=7=nEzAsN5Dtzc0?M6NpnSCUz!TZF@SicZ;wM)Oi5yoNg883$mu3^JPq@QKAmh%=~ zAiA6mE?5WzKb9W&yd5DkxV4bk%wbc^GVih_nH!SJ>$k6xT%GQ%F3`%`@huw5gsu;7 z#a0M>J7JNLg(?aQxNOfE@7jyno~~`GU3Yu=H{G^Jby589JadiVfESd_tDi54G+~}d z4cWo8@J}{VS0>HqMVABLiPpAA-6;18YeXZeMzCvN*83(*buo0_{T92)7O&=Os~43K zv`v{|zy92;CMB;%%oEp_MwZbbk^DKD>ep%ke+XV#3vVpgOlo10`3t3x2Pfau^8-(e!)*;ZvDj3}O)Yk_ z5mv=D?HcVrOl*4A&wSYtTHRn_;aIAk^hspw(}LI8Z^x?DZBLF#kNPW}w=bKMi1#% zDQfMjq*-yL+lp34^~#Ry>4o3MP&8mRJe%B48+nZ}fy9c-=?UQpUk=I`3n{x^u{3!k zx-LC^2G@mJ7}IN6d?l`qC5Q&@>~gU1m#4O`Eq8uXcfc8FX`UA4-D-$DdFs>fokW4i zt;ArjuPk_izm4#K3^vHEW!nD-s^n2q-G<(B{YqWJQSpD#*{l}8;UFxT93;Zu|7ie@ Mwk|d`XMGa>2NL^cCjbBd literal 0 HcmV?d00001 diff --git a/docs/README.md b/docs/README.md index 8ff032f..76a4b98 100644 --- a/docs/README.md +++ b/docs/README.md @@ -41,6 +41,7 @@ and callbacks; they render and report intent. They require `import DashUIKit` an | `ConverterCard` | [Lists & rows](lists-and-rows.md#convertercard) | | `ConverterCardItem` | [Lists & rows](lists-and-rows.md#convertercarditem) | | `ConverterCardRow` | [Lists & rows](lists-and-rows.md#convertercardrow) | +| `Criteria` (`Criterion`, `CriterionState`) | [Buttons & inputs](buttons-and-inputs.md#criteria) | | `CurrencyOption` | [Amount & currency](amount-and-currency.md#currencyoption) | | `DashAmount` | [Amount & currency](amount-and-currency.md#dashamount) | | `DashBalanceView` | [Amount & currency](amount-and-currency.md#dashbalanceview) | diff --git a/docs/buttons-and-inputs.md b/docs/buttons-and-inputs.md index 8e8715d..df64157 100644 --- a/docs/buttons-and-inputs.md +++ b/docs/buttons-and-inputs.md @@ -88,6 +88,12 @@ A labeled crypto-address input with a trailing action button (scan-QR when empty when filled), error styling, and a multi-line read-out state. iOS 17+ uses a vertical axis field (1–3 lines); older iOS uses a single-line field. +Also the general-purpose text input: its three parts — label, field, help text — are the +design system's `Input` component, so it is what any labeled field is built from, not +only an address one. The scan-QR button appears **only when `onScanQR` is given**; a +field for something with no QR form (a username, a note) passes no handler and gets no +trailing glyph while empty. + ```swift @State private var address = "" AddressFieldView( @@ -103,6 +109,10 @@ AddressFieldView( State-driven styling: +- **Ring** (3pt, hard-edged, outside the field — Figma's `0 0 0 3px`): + `redAlpha20` while `hasError`, `blueAlpha20` while + focused, and none once `isAccepted` — the field stops asking for attention when the + content has passed whatever the host validates it against. An error still overrides it. - **Focused:** clear background + gray border. - **Error:** red-tinted background, `errorText` shown beneath. - **Blurred + filled (no error):** clean read-out — the trailing action button is hidden @@ -110,6 +120,70 @@ State-driven styling: shift between focused/unfocused. - **Disabled:** field non-editable, action button omitted. +`showsClearButton: false` drops the trailing ✕ for a field short enough to clear by hand; +with no `onScanQR` either, no trailing space is reserved at all. + +--- + +## Criteria + +File `Components/Criteria.swift` · `@available(iOS 14, macOS 11, *)` + +A block of requirements with their status — the check-list under a username or password +field. One mark per row says whether that rule is met. + +```swift +Criteria([ + Criterion(text: "Between 3 and 23 characters", state: .met), + Criterion(text: "Letters, numbers and hyphens only", state: .failed), + Criterion(text: "You need to have more 0.25 Dash to create this username", state: .blocking), + Criterion(text: "Username available", state: .checking), +]) +``` + +`init(_ items: [Criterion], spacing: CGFloat = 10)` — `spacing` is the gap between rows. + +### States + +| `CriterionState` | Mark | Text | +|---|---|---| +| `.pending` | empty ring, `gray300Alpha50` | primary | +| `.checking` | circular spinner in the mark's box | primary | +| `.met` | green disc, white tick | primary | +| `.failed` | red disc, white cross | primary | +| `.blocking` | red disc, white cross | `errorText` | +| `.warning` | yellow disc, white exclamation | primary | + +`.failed` and `.blocking` draw the same mark: the difference is whether the rule is the +reason the user cannot continue, which is what colours the text. + +A rule that should not be shown is simply left out of the array — there is no "hidden" +state, matching the design system, which has no mark for one. + +### Icon slot + +The design system models this row as an icon slot plus text, so a row can carry artwork +of its own instead of a standard disc. It is rendered in the same 19pt box, untinted: + +```swift +Criterion(text: "Listed in the marketplace", icon: .custom("tag.badge", bundle: .main)) +``` + +### Geometry + +Fixed by the design system, not by the caller: 6pt between mark and text; the mark is +19pt centred in a 26pt box, which sets the row height; the text is `.dashFont(.footnote)` +with 4pt of vertical padding, and wraps rather than truncates. + +The `met` tick and the `warning` exclamation are drawn in this file rather than reusing +`CheckmarkIcon`: that icon comes from a different source SVG whose polyline is about a +tenth flatter than this one. The `failed` cross does reuse `XmarkIcon`, whose shape is +identical once scaled. + +> The design system draws three icons for this row — default, cross and tick. `.warning` +> has no artwork yet and is drawn here to match the others' weight; swap it for the real +> asset when it lands. + --- ## NumericKeyboardView From 9ffc7c70530f94d97e7fa9617f6f65f82b805bb2 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:03:25 +0300 Subject: [PATCH 2/3] feat(icons): add menu-identities and menu-voting The More screen moves its rows onto the design system's menu icons, and two of them had no asset: Identities and the new Voting row. Both come from the Figma Menu export (light only, like the rest of the Menu set), normalized and assembled with the same scripts as the existing imagesets, and are exposed as `DashIcon.Menu.identities` and `DashIcon.Menu.voting`. `menu-username-voting` stays: it is a different glyph and other screens may use it. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../DashUIKit/Foundation/Icon_DashUI.swift | 2 ++ .../menu-identities.imageset/Contents.json | 23 ++++++++++++++++++ .../menu-identities.png | Bin 0 -> 764 bytes .../menu-identities@2x.png | Bin 0 -> 1345 bytes .../menu-identities@3x.png | Bin 0 -> 1967 bytes .../Menu/menu-voting.imageset/Contents.json | 23 ++++++++++++++++++ .../Menu/menu-voting.imageset/menu-voting.png | Bin 0 -> 447 bytes .../menu-voting.imageset/menu-voting@2x.png | Bin 0 -> 757 bytes .../menu-voting.imageset/menu-voting@3x.png | Bin 0 -> 1035 bytes 9 files changed, 48 insertions(+) create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-identities.imageset/Contents.json create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-identities.imageset/menu-identities.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-identities.imageset/menu-identities@2x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-identities.imageset/menu-identities@3x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/Contents.json create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/menu-voting.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/menu-voting@2x.png create mode 100644 Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/menu-voting@3x.png diff --git a/Sources/DashUIKit/Foundation/Icon_DashUI.swift b/Sources/DashUIKit/Foundation/Icon_DashUI.swift index 88e0398..b06129e 100644 --- a/Sources/DashUIKit/Foundation/Icon_DashUI.swift +++ b/Sources/DashUIKit/Foundation/Icon_DashUI.swift @@ -167,6 +167,7 @@ public enum DashIcon { case flexa = "menu-flexa" case floppyDisk = "menu-floppy-disk" case gPay = "menu-g-pay" + case identities = "menu-identities" case importPrivateKey = "menu-import-private-key" case infoRect = "menu-info-rect" case invitation = "menu-invitation" @@ -216,6 +217,7 @@ public enum DashIcon { case uphold = "menu-uphold" case userSearch = "menu-user-search" case usernameVoting = "menu-username-voting" + case voting = "menu-voting" case wallet = "menu-wallet" case zenledger = "menu-zenledger" } diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-identities.imageset/Contents.json b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-identities.imageset/Contents.json new file mode 100644 index 0000000..be8db51 --- /dev/null +++ b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-identities.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "menu-identities.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "menu-identities@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "menu-identities@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-identities.imageset/menu-identities.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-identities.imageset/menu-identities.png new file mode 100644 index 0000000000000000000000000000000000000000..bbd33c05be2e2e4edec9578d79cc2da6c5480803 GIT binary patch literal 764 zcmVo&TP+vD;9hU3en!n6|9$+ zP+H<{sXbkG>ttIhoCU@X{p$3?wEMgmD6j{k6Jf-#H+%5BvS=cD%QC+=Fci8hDX&n9G zSZyH5!ZNg_33`qt%_EN1_j3&gn`z7rmm@5sh)x|V2`vW`GG>Q`bA`kJp5$0@6O?@u z$ulb|?8VI3RU7xzq8fbz*-6l=ndruS(LKC`F70oLr@iBU37+Z+ql^KO2-Q3|bxqLEX-J$R?Xo|-LYod)pYC-mf>I$L(G29nuTKj|dBU{tro8R1h-M)CYMZ8GVo}Jk zWZo5)iFA~?5zB4BK^ya{cEDu5rIYOWc~7YjV>O!ICQU>yY%)5%4jm-9r#o5G+c@4) uqQEF-b7n*%899-tUbtLY=4RB3!hSu;G#1S_*G`YbkKGCZ=a06vSgDRCuHK_!NT+j5qbWZ7OqlQnA&@1F-6Akmrf(HUT%pe*{R96lhu78d=ipNW z$KL`d(V>F;dk>y!qdwn4hPVr6k#ed=cUmMt6|l&x5opo9H{lgps9y@;fW6?x1D{%B zjUL3uP!U`3-h4$t+W-= z3%>jCCbeu6&N2N~d(rm^_ZIV{mpo;eeqYyZS9jtEB;bXD~!q*w&x_W0d6HrN@nt259^UwDF3e@c!7qW2 zrQ_&|9gj?NT&K`==>QI_Q2uK7^LG=)M!{JRuFKgHCq%RcjbOP4OE`!1oj(g#I9sxD zeGb+r^>5T^jbPD%1*SQyB%E?wMlGyF8;02Cu##x_MY|DQi6H89v|ta$f}QhFF>p&8 zBUPjZO_%Nl;I$e9c%;pXD;Lkg7|rTKv|vtR3$s>c-#9OfvnRazpdl_zyA1H{a$IY-UY)@eWb1oovlbmXPe-^jOS^9XFSo=(f9 zj72MtkR}EF%W(yg!$VQv8(4W>Lxnvn-o}(bvS8jbA=M3xJ5tuu?Ac0}0O;GqGRBo2 zJzL7XTIn+$tEDECyrFZ-M}icnHoX|ZLVIf6ROrjHG_ekgzMQ)BFCrp?y;f>`vzOb*IX+Z(g00000NkvXXu0mjf DHqwEB literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-identities.imageset/menu-identities@3x.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-identities.imageset/menu-identities@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0b0dedd3daca18c409e8952187fb52f87f849257 GIT binary patch literal 1967 zcmV;g2T=HlP)QY0YLGlQ6PX>4~%Wz!@b7DZAegEDr$NHTLKgxE&9&AvMyFu~I8 zp5Crb^8IFR(s+9#O`Chj+uArNDZSeAm8&`kpIu_}dM#>~hnEMQjX%7(OBOvhh4H z=jKhi8#6+a^$KTBT1^=}g|j5BTnav112eorSAH$R_bbvLPN27`a$C3yE^hpC34U{} z4@4%W>QmK(rm&}l zna0Y6W8i?^4U6M%zybw)7HAWTn$QBqSAaC=aav*N*nySSLQlyZ&)<3^y9xW zOVatvP98#t@<^Yjw0sHz6M?BT|pu*+~fE63iOUeN08VjZv7VcSyjk_ zN56R3Es@c7>j!j`vlgL6%!ymS2N5`mE02u~Jj;MTe@+9-KWt%19%*wG|0lIVN05M# zh?E&G42D4>Z@VcEI~jQnQZMw2kg(pix;se|Js&pf6*8eM5hJu?eLr5m66d44Dl`p# zG}9;@4|9s|y)f&bIrFAGC}>2XPQ#)N914=Qs@WHBSM;tG zJ;u;pXyJQu4G$ZT#Zt)um2)LYZPDM*oV#m9kAAcdeQ(;|QO>QyFk#i`5j^<#?OsE-IYvco)PxSV;$*HQ^-}&HKPI@7O0b$B6_p-8yhR! zt$ilme5H}5?*Q1>C0F*~BX|58tA+9m2bD=-8tXri>$>EmH` zB}!+tBYJ1l9yajPZhHwTZZ}#~)5IvQsy8CkUx!}*d2wI$Qrep;W{0PB{y_h|>blee zZvSS-X{Mp!n4U&j&A{-*KqF^p4ix_wp$?_HA0c<~$;f4N2+s;3d?C{&61iQ& zkuxhFGaMp^r{jb+fHs@gx7TN#?n>< zyAnd-sLa@R>uBaY3~hPc5HuqrBO@atBO_y9@E>8ef^-T>rWybM002ovPDHLkV1i~1 Bn!Er2 literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/Contents.json b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/Contents.json new file mode 100644 index 0000000..15cb0a7 --- /dev/null +++ b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "menu-voting.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "menu-voting@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "menu-voting@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/menu-voting.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/menu-voting.png new file mode 100644 index 0000000000000000000000000000000000000000..468bbfb09a003675c20f8d08f9d8bd27a743d9c4 GIT binary patch literal 447 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBK3|DzL`iUdT1k0gQ7VI5W_oVoyp7Y6fx4_b zT^vIy7~f7i?|axlq`kiE)LvU9QB5BP<&>5@2Qv-9Chadh0SQghcyBC7ZO#c0n7ZSc zU(V+{)3zusu$m|xo@K9js{4QKrl;c478^Dnk!RF7puM$jM(czepZ70q@_fz|Z0xD2 zYE#+o;ya`8^pDH!BA0_K|G!eO@(mRHEa@%yC$>&x5!{-7f?ml#{PkpA%+_t literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/menu-voting@2x.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/menu-voting@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3143281531d15d9e432158dadbb7919f89ac6f50 GIT binary patch literal 757 zcmV$<*2R+uu0J6wD!U&>Bq^%?*B{Td0T^?z}zuyx{{dzzk5C{YU zoq@q7>E!@;aSASeBkC=Xyw1=vb_GwbW`G!#B6AWxX;Zj8!7{u2;J;!dqicdMhjX+* z1Y7Wf6U|6W*9_^^ceH>#!C)V@4s&saAAe5Z7&|4M{)@qj%LX%7i1H&OM=LnOPKJ4L z+;Aq{=lxG`h#kQUADho*@8A&gCEdK7?}+kOuFBechMSjjiI)R7!o1*zlyeK16)ekH z;20gotYBU^-&Y@sS-~AIXNft%n{pnaAN6^>~VWi1cp^lO%5zs<8Sjoi0;wlouhgX7WUP-kmO= zBG(of*-7uK$*PyMbRzDHm5j*2XvLv;0<=vQvudy|nBOzq%8WKH`MH~a3RLK z99EU!EpOtlbz{z&QnYX}!B@34Tc$3U;X*l+(aNW@X?34*0i!>=z?j6IHJPpu+#A;( zt2Xz>So^Yl)Tu7Nw n&{I&9pS$eu3IqayK)?bwglMIYJezUC00000NkvXXu0mjfRANwf literal 0 HcmV?d00001 diff --git a/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/menu-voting@3x.png b/Sources/DashUIKit/Resources/Media.xcassets/Icons & Illustrations/Menu/menu-voting.imageset/menu-voting@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0537bae38ac1bd6fcdb1c9c36ba92128ff2c5d95 GIT binary patch literal 1035 zcmV+m1oZofP)=;2up zoPE&`V8=+30R|5;L?}Thc(P$0AlAY2ulV?F9T5sAXo#N0pbl8_8N=^8h)ftktFrNb zZ8qUzSJv}wL?n!$3(LkrJ9vNo1d#}&bciZ-w}TZ75sBu5o`1M?P=!DbA7*7Ic+CgR z(kf-+kCa96!Px9NdU)CaU;Tl)lFR!TJf0$8&CY1P>Sj&s>i68CW`ky^S2lkC9ufTN zqq(4G7m(zKnG2%C8oVLR173`?~xG-NLe=_zij}uOACUa}C)gzd;MJiI(ylG<%Mg@)va3`+`fx zU(nU=3oZ?hrNgS7U}^aYy5(g$PcjiE_H?^?OXN2?Vat)mx`{tsH6iK_CR+~UpnS3v`5gl0^GFN zlDTbD_mn7~(NQ8gCE(8I!?JhS8 z+7k-e6AIcB3fdD2+7k-etxR`-f_8~f(0&NDXo<1zm@bwb^oexZ);9vY4vP$;yH4aGKFosoGp(vqPHE zRjAqgvfLJ)G^5K>A+xlPk|^l1IIpxnswrr@&7he6KG|P`GAx4}TX_BG7!~M)X2qwK z*DqfD!dzsC8IzMvh5WYq*2;#UXG@o&S$$-4`Z*iJiZ8qbNxg5DKCKh7q7Pctp&Wf} zuY!65$))z0joahXEmPk^aOM+ZORg^ZF7~W0-e6EY%W`$KiIhFV?70;-fyhUdYSsJ+ zuio_z2VI0^D*v`jE|5=Tshe2+rxHR4A%qY@2q7dizW|O`?J}3RP;>wQ002ovPDHLk FV1nFP(@y{Z literal 0 HcmV?d00001 From 55bd6c6f0bfe84f4699bca075f650eb4a0d8612a Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:41:44 +0300 Subject: [PATCH 3/3] =?UTF-8?q?fix(components):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20unique=20Criteria=20rows,=20blocking=20state=20for?= =?UTF-8?q?=20VoiceOver,=20link=20layout,=20SimpleSelect=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Criteria: a repeated `id` (two rules with the same text and no explicit id) gave SwiftUI two rows with one identity. Render identities are now unique; the first occurrence keeps its bare id, so a row with a stable explicit id still animates in place. - VoiceOver: `.blocking` read the same as `.failed` ("Not met"). It now says the rule is required to continue, which the red text says visually. - SystemMessageView: the link layout (negative padding, tight spacing) was applied whenever the style was a link, including a row that shows only the secondary pill. It now applies only when the primary link renders. - Docs: `SimpleSelect` gets a catalog row and a section in Buttons & inputs; `Criteria` and `SimpleSelect` join the section summary. Co-Authored-By: Claude Opus 5.5 (1M context) --- Sources/DashUIKit/Components/Criteria.swift | 27 ++++++++++-- .../Components/SystemMessageView.swift | 13 ++++-- docs/README.md | 3 +- docs/buttons-and-inputs.md | 41 +++++++++++++++++++ 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/Sources/DashUIKit/Components/Criteria.swift b/Sources/DashUIKit/Components/Criteria.swift index d16ef8f..e925bc4 100644 --- a/Sources/DashUIKit/Components/Criteria.swift +++ b/Sources/DashUIKit/Components/Criteria.swift @@ -72,7 +72,8 @@ public struct Criterion: Identifiable { /// `id` defaults to `text`, which is what distinguishes the rows in /// practice; pass one explicitly when a row's text changes while the row /// stays the same (an availability line, say), so it animates in place - /// instead of being replaced. + /// instead of being replaced. Two rows that share an `id` still render + /// as two rows — ``Criteria`` tells repeats apart itself. public init(id: String? = nil, text: String, state: CriterionState) { self.id = id ?? text self.text = text @@ -120,12 +121,26 @@ public struct Criteria: View { public var body: some View { VStack(alignment: .leading, spacing: spacing) { - ForEach(items) { item in - CriterionRow(criterion: item) + ForEach(renderIdentities, id: \.key) { entry in + CriterionRow(criterion: entry.item) } } .frame(maxWidth: .infinity, alignment: .leading) } + + /// Each item's `id`, made unique. A repeated `id` — two rules with the + /// same text and no explicit id — would give SwiftUI two rows with one + /// identity, and updates could land on the wrong one. The first + /// occurrence keeps its bare `id`, so a row with a stable explicit id + /// still animates in place; only later repeats get a suffix. + private var renderIdentities: [(key: String, item: Criterion)] { + var seen: [String: Int] = [:] + return items.map { item in + let count = seen[item.id, default: 0] + seen[item.id] = count + 1 + return (count == 0 ? item.id : "\(item.id)#\(count)", item) + } + } } // MARK: - Row @@ -248,8 +263,12 @@ private struct CriterionRow: View { return NSLocalizedString("Checking", bundle: .module, comment: "DashUIKit") case .met: return NSLocalizedString("Met", bundle: .module, comment: "DashUIKit") - case .failed, .blocking: + case .failed: return NSLocalizedString("Not met", bundle: .module, comment: "DashUIKit") + case .blocking: + // Not just unmet: this is the rule that stops the user going on, + // which the red text says to a sighted user. + return NSLocalizedString("Not met, required to continue", bundle: .module, comment: "DashUIKit") case .warning: return NSLocalizedString("Met, with a warning", bundle: .module, comment: "DashUIKit") } diff --git a/Sources/DashUIKit/Components/SystemMessageView.swift b/Sources/DashUIKit/Components/SystemMessageView.swift index ff26799..666f786 100644 --- a/Sources/DashUIKit/Components/SystemMessageView.swift +++ b/Sources/DashUIKit/Components/SystemMessageView.swift @@ -64,7 +64,7 @@ public struct SystemMessageView: View { .scaledToFit() .frame(width: 30, height: 30) - VStack(alignment: .leading, spacing: isLinkAction ? 6 : 20) { + VStack(alignment: .leading, spacing: rendersPrimaryLink ? 6 : 20) { VStack(alignment: .leading, spacing: 0) { Text(title) .dashFont(.subhead) @@ -101,8 +101,8 @@ public struct SystemMessageView: View { // pads every style for a tappable pill, which leaves a // plain label visibly indented from the sentence above it, // and the 20pt stack gap reads as a gap between sections. - .padding(.horizontal, isLinkAction ? -DashButtonSize.small.hPadding : 0) - .padding(.bottom, isLinkAction ? 4 : 14) + .padding(.horizontal, rendersPrimaryLink ? -DashButtonSize.small.hPadding : 0) + .padding(.bottom, rendersPrimaryLink ? 4 : 14) } } .padding(.vertical, 5) @@ -132,6 +132,13 @@ public struct SystemMessageView: View { return false } } + + /// The link layout only when the link is actually on screen. With a link + /// style but no primary button, the row holds just the secondary pill, + /// and pulling it out by the link's padding would misalign it. + private var rendersPrimaryLink: Bool { + isLinkAction && buttonName != nil && onAction != nil + } } // MARK: - Preview diff --git a/docs/README.md b/docs/README.md index 76a4b98..dd05369 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ and callbacks; they render and report intent. They require `import DashUIKit` an (`Font.dash.*`, `.dashFont`, `DashTextStyle`), icons (`DashIconSource`, `Image(dash:)`), bundle access. - **[Buttons & inputs](buttons-and-inputs.md)** — `DashButton`, `DashSwitch`, `SearchBar`, - `AddressFieldView`, `NumericKeyboardView`. + `AddressFieldView`, `Criteria`, `SimpleSelect`, `NumericKeyboardView`. - **[Amount & currency](amount-and-currency.md)** — `EnterAmountView`, `SwapAmountView`, `ReceiveEstimateView`, `DashAmount`, `DashBalanceView`, `DashPickerView`, `CurrencyOption`. @@ -64,6 +64,7 @@ and callbacks; they render and report intent. They require `import DashUIKit` an | `ScrollViewWithOnScrollChanged` | [Utilities](utilities.md#scrollviewwithonscrollchanged) | | `scaleToFitWidth` | [Utilities](utilities.md#scaletofitwidth) | | `SearchBar` | [Buttons & inputs](buttons-and-inputs.md#searchbar) | +| `SimpleSelect` | [Buttons & inputs](buttons-and-inputs.md#simpleselect) | | `SuccessIllustration` | [Feedback](feedback.md#successillustration--errorillustration) | | `SwapAmountView` | [Amount & currency](amount-and-currency.md#swapamountview) | | `SystemMessageView` | [Feedback](feedback.md#systemmessageview) | diff --git a/docs/buttons-and-inputs.md b/docs/buttons-and-inputs.md index df64157..276f0bb 100644 --- a/docs/buttons-and-inputs.md +++ b/docs/buttons-and-inputs.md @@ -186,6 +186,47 @@ identical once scaled. --- +## SimpleSelect + +File `Components/SimpleSelect.swift` · `@available(iOS 14, macOS 11, *)` + +One option in a short list the user picks from: a title, an optional line saying what picking +it means, and a card that carries the selection itself. The card *is* the control — there is no +trailing radio or checkmark — so the description sits inside the hit target. + +```swift +VStack(spacing: 20) { + SimpleSelect( + title: "Shielded balance", + description: "Keeps your username private", + isSelected: selection == .shielded, + action: { selection = .shielded }) + SimpleSelect( + title: "Dash balance", + description: "Funds will be traceable to your username", + isSelected: selection == .core, + action: { selection = .core }) +} +``` + +`init(title: String, description: String? = nil, isSelected: Bool, action: @escaping () -> Void)` +— stateless: the host owns the selection and passes `isSelected` for each option. + +| State | Border | Fill | +|---|---|---| +| selected | `blue`, 1.5pt | `blueAlpha5` | +| unselected | `gray300Alpha30`, 1.5pt | none | + +The card takes the full width it is offered (options are compared in a column), has a 16pt +corner radius and 20 × 12pt padding; the title is `.subheadMedium`, the description `.footnote` +in `secondaryText`. The whole card is tappable, including an unselected card with no fill. +VoiceOver reads it as a button, with the selected trait on the chosen option. + +For a row with an icon, a trailing value or an explicit radio/checkbox mark, use +[`RadioButtonRow`](lists-and-rows.md#radiobuttonrow). + +--- + ## NumericKeyboardView File `Components/NumericKeyboardView.swift` · `@available(iOS 14, macOS 11, *)`