diff --git a/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListCardView+SnapshotTests.swift b/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListCardView+SnapshotTests.swift deleted file mode 100644 index c8ed94ba7..000000000 --- a/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListCardView+SnapshotTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -// swiftlint:disable:this file_name -import BitwardenResources -import SnapshotTesting -import XCTest - -@testable import AuthenticatorShared - -// MARK: - ItemListCardViewTests - -class ItemListCardViewTests: BitwardenTestCase { - // MARK: Tests - - /// Test a snapshot of the ItemListView previews. - func disabletest_snapshot_ItemListCardView_previews() { - for preview in ItemListCardView_Previews._allPreviews { - let name = preview.displayName ?? "Unknown" - assertSnapshots( - of: preview.content, - as: [ - "\(name)-portrait": .defaultPortrait, - "\(name)-portraitDark": .defaultPortraitDark, - "\(name)-portraitAX5": .tallPortraitAX5(heightMultiple: 3), - ], - ) - } - } -} diff --git a/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListCardView+ViewInspectorTests.swift b/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListCardView+ViewInspectorTests.swift deleted file mode 100644 index 3f5d47bf2..000000000 --- a/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListCardView+ViewInspectorTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// swiftlint:disable:this file_name -import BitwardenResources -import ViewInspector -import XCTest - -@testable import AuthenticatorShared - -// MARK: - ItemListCardViewTests - -class ItemListCardViewTests: BitwardenTestCase { - // MARK: Tests - - /// Test the actions are properly wired up in the ItemListCardView. - func test_ItemListCardView_actions() throws { - let expectationAction = expectation(description: "action Tapped") - let expectationClose = expectation(description: "close Tapped") - let subject = ItemListCardView( - bodyText: Localizations - .allowAuthenticatorAppSyncingInSettingsToViewAllYourVerificationCodesHere, - buttonText: Localizations.takeMeToTheAppSettings, - leftImage: {}, - titleText: Localizations.syncWithTheBitwardenApp, - actionTapped: { - expectationAction.fulfill() - }, - closeTapped: { - expectationClose.fulfill() - }, - ) - - try subject.inspect().find(buttonWithAccessibilityLabel: Localizations.close).tap() - wait(for: [expectationClose]) - - try subject.inspect().find(button: Localizations.takeMeToTheAppSettings).tap() - wait(for: [expectationAction]) - } -} diff --git a/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListCardView.swift b/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListCardView.swift deleted file mode 100644 index 0daca0d6a..000000000 --- a/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListCardView.swift +++ /dev/null @@ -1,136 +0,0 @@ -import BitwardenResources -import SwiftUI - -// MARK: - ItemListCardView - -/// An item list card view, -/// -struct ItemListCardView: View { - // MARK: Properties - - /// The body text to display in the card. - var bodyText: String - - /// The button text to display in the card. - var buttonText: String - - /// The image to display in the card. - @ViewBuilder let leftImage: ImageContent - - /// The button text for the secondary button in the card. - var secondaryButtonText: String? - - /// The title text to display in the card. - var titleText: String - - // MARK: Closures - - /// The callback action to perform. - var actionTapped: () -> Void - - /// The close callback to perform. - var closeTapped: () -> Void - - /// The action to perform when the secondary button is tapped. - var secondaryActionTapped: (() -> Void)? - - var body: some View { - VStack(spacing: 16) { - HStack(alignment: .top, spacing: 16) { - leftImage - - VStack(alignment: .leading, spacing: 0) { - Group { - Text(titleText) - .styleGuide(.headline) - .foregroundColor(Asset.Colors.textPrimary.swiftUIColor) - - Text(bodyText) - .styleGuide(.subheadline) - .foregroundColor(Asset.Colors.textSecondary.swiftUIColor) - } - .frame(maxWidth: .infinity, alignment: .leading) - } - - Button { - closeTapped() - } label: { - Image(decorative: SharedAsset.Icons.close16) - .padding(16) // Add padding to increase tappable area... - } - .padding(-16) // ...but remove it to not affect layout. - .buttonStyle(PlainButtonStyle()) - .accessibilityLabel(Localizations.close) - } - - VStack(spacing: 0) { - Button { - actionTapped() - } label: { - Text(buttonText) - } - .buttonStyle(.primary()) - - if let secondaryButtonText, let secondaryActionTapped { - Button { - secondaryActionTapped() - } label: { - Text(secondaryButtonText) - } - .buttonStyle(.bitwardenBorderless) - .padding(.bottom, -8) // Remove extra padding below the borderless button. - } - } - } - .padding(16) - .background { - Asset.Colors.backgroundPrimary.swiftUIColor - .clipShape(.rect(cornerRadius: 16)) - .shadow(color: .black.opacity(0.45), radius: 2, x: 0, y: 1) - } - .frame(maxWidth: .infinity) - } -} - -// MARK: Previews - -#if DEBUG -struct ItemListCardView_Previews: PreviewProvider { - static var previews: some View { - ScrollView { - VStack { - ItemListCardView( - bodyText: Localizations - .allowAuthenticatorAppSyncingInSettingsToViewAllYourVerificationCodesHere, - buttonText: Localizations.takeMeToTheAppSettings, - leftImage: { - Image(decorative: SharedAsset.Icons.arrowSync24) - .foregroundColor(Asset.Colors.primaryBitwardenLight.swiftUIColor) - .frame(width: 24, height: 24) - }, - titleText: Localizations.syncWithTheBitwardenApp, - actionTapped: {}, - closeTapped: {}, - ) - - ItemListCardView( - bodyText: Localizations - .allowAuthenticatorAppSyncingInSettingsToViewAllYourVerificationCodesHere, - buttonText: Localizations.takeMeToTheAppSettings, - leftImage: { - Image(decorative: SharedAsset.Icons.arrowSync24) - .foregroundColor(Asset.Colors.primaryBitwardenLight.swiftUIColor) - .frame(width: 24, height: 24) - }, - secondaryButtonText: Localizations.learnMore, - titleText: Localizations.syncWithTheBitwardenApp, - actionTapped: {}, - closeTapped: {}, - secondaryActionTapped: {}, - ) - } - .padding(16) - } - } -} -#endif diff --git a/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListView+ViewInspectorTests.swift b/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListView+ViewInspectorTests.swift index fb6b609ab..aedc2ba60 100644 --- a/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListView+ViewInspectorTests.swift +++ b/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListView+ViewInspectorTests.swift @@ -43,7 +43,7 @@ class ItemListViewTests: BitwardenTestCase { /// Test the close taps trigger the associated effect. @MainActor - func test_itemListCardView_close_download() throws { + func test_actionCard_close_download() async throws { let state = ItemListState( itemListCardState: .passwordManagerDownload, loadingState: .data([ItemListSection.fixture()]), @@ -54,16 +54,15 @@ class ItemListViewTests: BitwardenTestCase { timeProvider: timeProvider, ) - try subject.inspect().find(buttonWithAccessibilityLabel: Localizations.close).tap() - - waitFor(!processor.effects.isEmpty) + let actionCard = try subject.inspect().find(actionCard: Localizations.downloadTheBitwardenApp) + try await actionCard.find(asyncButton: Localizations.close).tap() XCTAssertEqual(processor.effects.last, .closeCard(.passwordManagerDownload)) } /// Test the close taps trigger the associated effect. @MainActor - func test_itemListCardView_close_sync() throws { + func test_actionCard_close_sync() async throws { let state = ItemListState( itemListCardState: .passwordManagerSync, loadingState: .data([]), @@ -74,9 +73,8 @@ class ItemListViewTests: BitwardenTestCase { timeProvider: timeProvider, ) - try subject.inspect().find(buttonWithAccessibilityLabel: Localizations.close).tap() - - waitFor(!processor.effects.isEmpty) + let actionCard = try subject.inspect().find(actionCard: Localizations.syncWithTheBitwardenApp) + try await actionCard.find(asyncButton: Localizations.close).tap() XCTAssertEqual(processor.effects.last, .closeCard(.passwordManagerSync)) } diff --git a/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListView.swift b/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListView.swift index ced0da399..22074b8f0 100644 --- a/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListView.swift +++ b/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListView.swift @@ -7,7 +7,7 @@ import SwiftUI // MARK: - SearchableItemListView /// A view that displays the items in a single vault group. -private struct SearchableItemListView: View { // swiftlint:disable:this type_body_length +private struct SearchableItemListView: View { // MARK: Properties /// A flag indicating if the search bar is focused. @@ -131,52 +131,42 @@ private struct SearchableItemListView: View { // swiftlint:disable:this type_bod /// The Password Manager download card definition. private var itemListCardPasswordManagerInstall: some View { - ItemListCardView( - bodyText: Localizations.storeAllOfYourLoginsAndSyncVerificationCodesDirectlyWithTheAuthenticatorApp, - buttonText: Localizations.downloadTheBitwardenApp, - leftImage: { - Image(decorative: SharedAsset.Icons.shield24) - .foregroundColor(Asset.Colors.primaryBitwardenLight.swiftUIColor) - .frame(width: 24, height: 24) - }, - titleText: Localizations.downloadTheBitwardenApp, - actionTapped: { + ActionCard( + title: Localizations.downloadTheBitwardenApp, + message: Localizations.storeAllOfYourLoginsAndSyncVerificationCodesDirectlyWithTheAuthenticatorApp, + actionButtonState: ActionCard.ButtonState(title: Localizations.downloadTheBitwardenApp) { openURL(ExternalLinksConstants.passwordManagerLink) }, - closeTapped: { - Task { - await store.perform(.closeCard(.passwordManagerDownload)) - } + dismissButtonState: ActionCard.ButtonState(title: Localizations.close) { + await store.perform(.closeCard(.passwordManagerDownload)) }, - ) + ) { + Image(decorative: SharedAsset.Icons.shield24) + .foregroundColor(SharedAsset.Colors.iconSecondary.swiftUIColor) + .frame(width: 24, height: 24) + } .padding(.top, 16) } /// The Password Manager sync card definition. private var itemListCardSync: some View { - ItemListCardView( - bodyText: Localizations - .allowAuthenticatorAppSyncingInSettingsToViewAllYourVerificationCodesHere, - buttonText: Localizations.takeMeToTheAppSettings, - leftImage: { - Image(decorative: SharedAsset.Icons.arrowSync24) - .foregroundColor(Asset.Colors.primaryBitwardenLight.swiftUIColor) - .frame(width: 24, height: 24) - }, - secondaryButtonText: Localizations.learnMore, - titleText: Localizations.syncWithTheBitwardenApp, - actionTapped: { + ActionCard( + title: Localizations.syncWithTheBitwardenApp, + message: Localizations.allowAuthenticatorAppSyncingInSettingsToViewAllYourVerificationCodesHere, + actionButtonState: ActionCard.ButtonState(title: Localizations.takeMeToTheAppSettings) { openURL(ExternalLinksConstants.passwordManagerSettings) }, - closeTapped: { - Task { - await store.perform(.closeCard(.passwordManagerSync)) - } + dismissButtonState: ActionCard.ButtonState(title: Localizations.close) { + await store.perform(.closeCard(.passwordManagerSync)) }, - secondaryActionTapped: { + secondaryButtonState: ActionCard.ButtonState(title: Localizations.learnMore) { openURL(ExternalLinksConstants.totpSyncHelp) }, - ) + ) { + Image(decorative: SharedAsset.Icons.arrowSync24) + .foregroundColor(SharedAsset.Colors.iconSecondary.swiftUIColor) + .frame(width: 24, height: 24) + } .padding(.top, 16) } @@ -722,6 +712,36 @@ struct ItemListView_Previews: PreviewProvider { // swiftlint:disable:this type_b timeProvider: PreviewTimeProvider(), ) }.previewDisplayName("SharedItems") + + NavigationView { + ItemListView( + store: Store( + processor: StateProcessor( + state: ItemListState( + itemListCardState: .passwordManagerDownload, + loadingState: .data([]), + ), + ), + ), + timeProvider: PreviewTimeProvider(), + ) + } + .previewDisplayName("Password Manager Download Card") + + NavigationView { + ItemListView( + store: Store( + processor: StateProcessor( + state: ItemListState( + itemListCardState: .passwordManagerSync, + loadingState: .data([]), + ), + ), + ), + timeProvider: PreviewTimeProvider(), + ) + } + .previewDisplayName("Password Manager Sync Card") } } #endif diff --git a/AuthenticatorShared/UI/Vault/ItemList/ItemList/__Snapshots__/ItemListCardViewTests/test_snapshot_ItemListCardView_previews.Unknown-portrait.png b/AuthenticatorShared/UI/Vault/ItemList/ItemList/__Snapshots__/ItemListCardViewTests/test_snapshot_ItemListCardView_previews.Unknown-portrait.png deleted file mode 100644 index 72cca22b9..000000000 Binary files a/AuthenticatorShared/UI/Vault/ItemList/ItemList/__Snapshots__/ItemListCardViewTests/test_snapshot_ItemListCardView_previews.Unknown-portrait.png and /dev/null differ diff --git a/AuthenticatorShared/UI/Vault/ItemList/ItemList/__Snapshots__/ItemListCardViewTests/test_snapshot_ItemListCardView_previews.Unknown-portraitAX5.png b/AuthenticatorShared/UI/Vault/ItemList/ItemList/__Snapshots__/ItemListCardViewTests/test_snapshot_ItemListCardView_previews.Unknown-portraitAX5.png deleted file mode 100644 index 27d85ccca..000000000 Binary files a/AuthenticatorShared/UI/Vault/ItemList/ItemList/__Snapshots__/ItemListCardViewTests/test_snapshot_ItemListCardView_previews.Unknown-portraitAX5.png and /dev/null differ diff --git a/AuthenticatorShared/UI/Vault/ItemList/ItemList/__Snapshots__/ItemListCardViewTests/test_snapshot_ItemListCardView_previews.Unknown-portraitDark.png b/AuthenticatorShared/UI/Vault/ItemList/ItemList/__Snapshots__/ItemListCardViewTests/test_snapshot_ItemListCardView_previews.Unknown-portraitDark.png deleted file mode 100644 index 982c8a65b..000000000 Binary files a/AuthenticatorShared/UI/Vault/ItemList/ItemList/__Snapshots__/ItemListCardViewTests/test_snapshot_ItemListCardView_previews.Unknown-portraitDark.png and /dev/null differ diff --git a/BitwardenKit/UI/Platform/Application/Appearance/Styles/BitwardenBorderlessButtonStyle.swift b/BitwardenKit/UI/Platform/Application/Appearance/Styles/BitwardenBorderlessButtonStyle.swift index de629528d..a64d73fa1 100644 --- a/BitwardenKit/UI/Platform/Application/Appearance/Styles/BitwardenBorderlessButtonStyle.swift +++ b/BitwardenKit/UI/Platform/Application/Appearance/Styles/BitwardenBorderlessButtonStyle.swift @@ -18,12 +18,22 @@ public struct BitwardenBorderlessButtonStyle: ButtonStyle { : SharedAsset.Colors.buttonOutlinedDisabledForeground.swiftUIColor } + /// If this button should fill to take up as much width as possible. + var shouldFillWidth = false + + /// The size of the button. + var size: ButtonStyleSize? + // MARK: ButtonStyle public func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundStyle(foregroundColor) - .styleGuide(.subheadlineSemibold) + .styleGuide(size?.fontStyle ?? .subheadlineSemibold) + .padding(.vertical, size?.verticalPadding ?? 0) + .padding(.horizontal, size?.horizontalPadding ?? 0) + .frame(maxWidth: shouldFillWidth ? .infinity : nil, minHeight: size?.minimumHeight ?? nil) + .contentShape(Capsule()) .opacity(configuration.isPressed ? 0.5 : 1) } } @@ -33,15 +43,31 @@ public struct BitwardenBorderlessButtonStyle: ButtonStyle { public extension ButtonStyle where Self == BitwardenBorderlessButtonStyle { /// The style for a borderless button in this application. /// + /// This style does not add any padding to the button. Padding should be applied by the caller. + /// static var bitwardenBorderless: BitwardenBorderlessButtonStyle { BitwardenBorderlessButtonStyle() } + + /// The style for a borderless button in this application with padding and font size based on + /// the button size. + /// + /// - Parameters: + /// - shouldFillWidth: A flag indicating if this button should fill all available space. + /// - size: The size of the button, which determines the padding and font size applied. + /// + static func bitwardenBorderless( + shouldFillWidth: Bool = true, + size: ButtonStyleSize, + ) -> BitwardenBorderlessButtonStyle { + BitwardenBorderlessButtonStyle(shouldFillWidth: shouldFillWidth, size: size) + } } // MARK: Previews #if DEBUG -#Preview() { +#Preview("States") { VStack { Button("Bitwarden") {} @@ -49,6 +75,20 @@ public extension ButtonStyle where Self == BitwardenBorderlessButtonStyle { .disabled(true) } .buttonStyle(.bitwardenBorderless) - .padding(.vertical, 14) + .padding() +} + +#Preview("Sizes") { + VStack { + Button("Small") {} + .buttonStyle(.bitwardenBorderless(size: .small)) + + Button("Medium") {} + .buttonStyle(.bitwardenBorderless(size: .medium)) + + Button("Large") {} + .buttonStyle(.bitwardenBorderless(size: .large)) + } + .padding() } #endif diff --git a/BitwardenShared/UI/Platform/Application/Views/ActionCard+ViewInspectorTests.swift b/BitwardenKit/UI/Platform/Application/Views/ActionCard+ViewInspectorTests.swift similarity index 65% rename from BitwardenShared/UI/Platform/Application/Views/ActionCard+ViewInspectorTests.swift rename to BitwardenKit/UI/Platform/Application/Views/ActionCard+ViewInspectorTests.swift index 4e3ab4805..e6974b4f8 100644 --- a/BitwardenShared/UI/Platform/Application/Views/ActionCard+ViewInspectorTests.swift +++ b/BitwardenKit/UI/Platform/Application/Views/ActionCard+ViewInspectorTests.swift @@ -1,8 +1,9 @@ // swiftlint:disable:this file_name import SwiftUI +import ViewInspectorTestHelpers import XCTest -@testable import BitwardenShared +@testable import BitwardenKit final class ActionCardTests: BitwardenTestCase { // MARK: Tests @@ -38,4 +39,22 @@ final class ActionCardTests: BitwardenTestCase { XCTAssertTrue(dismissButtonTapped) } + + /// Tapping the secondary button should call the secondary button state's action closure. + @MainActor + func test_secondaryButton_tap() async throws { + var secondaryButtonTapped = false + let subject = ActionCard( + title: "Title", + message: "Message", + secondaryButtonState: ActionCard.ButtonState(title: "Secondary") { + secondaryButtonTapped = true + }, + ) + + let button = try subject.inspect().find(asyncButton: "Secondary") + try await button.tap() + + XCTAssertTrue(secondaryButtonTapped) + } } diff --git a/BitwardenShared/UI/Platform/Application/Views/ActionCard.swift b/BitwardenKit/UI/Platform/Application/Views/ActionCard.swift similarity index 76% rename from BitwardenShared/UI/Platform/Application/Views/ActionCard.swift rename to BitwardenKit/UI/Platform/Application/Views/ActionCard.swift index 5fa3a5d43..140954576 100644 --- a/BitwardenShared/UI/Platform/Application/Views/ActionCard.swift +++ b/BitwardenKit/UI/Platform/Application/Views/ActionCard.swift @@ -1,4 +1,3 @@ -import BitwardenKit import BitwardenResources import SwiftUI @@ -6,12 +5,12 @@ import SwiftUI /// A view that displays a card representing an action that the user needs to take. /// -struct ActionCard: View { +public struct ActionCard: View { // MARK: Types /// A data model containing the properties for a button within an action card. /// - struct ButtonState { + public struct ButtonState { // MARK: Properties /// An action to perform when the button is tapped. @@ -28,7 +27,7 @@ struct ActionCard: View { /// - title: The title of the button. /// - action: An action to perform when the button is tapped. /// - init(title: String, action: @escaping () async -> Void) { + public init(title: String, action: @escaping () async -> Void) { self.action = action self.title = title } @@ -48,12 +47,15 @@ struct ActionCard: View { /// The message to display in the card, below the title. let message: String? + /// State that describes the secondary button. + let secondaryButtonState: ButtonState? + /// The title of the card. let title: String // MARK: View - var body: some View { + public var body: some View { VStack(alignment: .leading, spacing: 16) { HStack(alignment: .top, spacing: 8) { if let leadingContent { @@ -82,9 +84,18 @@ struct ActionCard: View { } } - if let actionButtonState { - AsyncButton(actionButtonState.title, action: actionButtonState.action) - .buttonStyle(.primary(size: .medium)) + if actionButtonState != nil || secondaryButtonState != nil { + VStack(spacing: 4) { + if let actionButtonState { + AsyncButton(actionButtonState.title, action: actionButtonState.action) + .buttonStyle(.primary(size: .medium)) + } + + if let secondaryButtonState { + AsyncButton(secondaryButtonState.title, action: secondaryButtonState.action) + .buttonStyle(.bitwardenBorderless(size: .medium)) + } + } } } .foregroundStyle(SharedAsset.Colors.textPrimary.swiftUIColor) @@ -108,19 +119,22 @@ struct ActionCard: View { /// - message: The message to display in the card. /// - actionButtonState: State that describes the action button. /// - dismissButtonState: State that describes the dismiss button. + /// - secondaryButtonState: State that describes the secondary button. /// - leadingContent: Content that is displayed at the leading edge of the title and message. /// - init( + public init( title: String, message: String? = nil, actionButtonState: ButtonState? = nil, dismissButtonState: ButtonState? = nil, + secondaryButtonState: ButtonState? = nil, @ViewBuilder leadingContent: () -> LeadingContent, ) { self.actionButtonState = actionButtonState self.dismissButtonState = dismissButtonState self.leadingContent = leadingContent() self.message = message + self.secondaryButtonState = secondaryButtonState self.title = title } @@ -131,17 +145,20 @@ struct ActionCard: View { /// - message: The message to display in the card. /// - actionButtonState: State that describes the action button. /// - dismissButtonState: State that describes the dismiss button. + /// - secondaryButtonState: State that describes the secondary button. /// - init( + public init( title: String, message: String? = nil, actionButtonState: ButtonState? = nil, dismissButtonState: ButtonState? = nil, + secondaryButtonState: ButtonState? = nil, ) where LeadingContent == EmptyView { self.actionButtonState = actionButtonState self.dismissButtonState = dismissButtonState leadingContent = nil self.message = message + self.secondaryButtonState = secondaryButtonState self.title = title } } @@ -163,6 +180,14 @@ struct ActionCard: View { dismissButtonState: ActionCard.ButtonState(title: "Dismiss") {}, ) + ActionCard( + title: "Title", + message: "Message", + actionButtonState: ActionCard.ButtonState(title: "Tap me!") {}, + dismissButtonState: ActionCard.ButtonState(title: "Dismiss") {}, + secondaryButtonState: ActionCard.ButtonState(title: "Secondary button") {}, + ) + ActionCard( title: "Title", message: "Message", diff --git a/ViewInspectorTestHelpers/InspectableView.swift b/ViewInspectorTestHelpers/InspectableView.swift index e10a5bbcd..ea3030b8f 100644 --- a/ViewInspectorTestHelpers/InspectableView.swift +++ b/ViewInspectorTestHelpers/InspectableView.swift @@ -12,7 +12,7 @@ public struct ActionCardType: BaseViewType { public static var typePrefix: String = "ActionCard" public static var namespacedPrefixes: [String] = [ - "BitwardenShared.ActionCard", + "BitwardenKit.ActionCard", ] }