diff --git a/docs/SMOKE_TEST.md b/docs/SMOKE_TEST.md index 7c9bbd8..122493e 100644 --- a/docs/SMOKE_TEST.md +++ b/docs/SMOKE_TEST.md @@ -63,6 +63,7 @@ Use this checklist before a release or after changes to panel, pasteboard, setti 30. Confirm the Pinned empty state points to the Pin action instead of a plain-key shortcut. 31. Confirm each card's source or type badge reads as an attached header-corner tile instead of a small floating icon. 32. Confirm built-in collection chips use recognizable glyphs, while custom collection chips keep color-dot swatches. +33. Confirm cards from known apps show app identity in the header tile, falling back to source initials when an app icon is unavailable. ## Copy And Paste diff --git a/sources/clipbored/views/ClipboardPanelView.swift b/sources/clipbored/views/ClipboardPanelView.swift index de1f4bb..f10026b 100644 --- a/sources/clipbored/views/ClipboardPanelView.swift +++ b/sources/clipbored/views/ClipboardPanelView.swift @@ -1375,6 +1375,10 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate { cardViews.map(\.debugHeaderBadgeSymbol) } + var debugCardHeaderBadgeTexts: [String] { + cardViews.map(\.debugHeaderBadgeText) + } + var debugFirstCardHeaderTitle: String { cardViews.first?.debugHeaderTitle ?? "" } @@ -2724,6 +2728,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource { private(set) var debugPreviewSummary = "" private(set) var debugPreviewStyle = "" private(set) var debugHeaderBadgeSymbol = "" + private(set) var debugHeaderBadgeText = "" private(set) var debugHeaderTitle = "" private(set) var debugHeaderSubtitle = "" private(set) var debugHeaderColorHex = "" @@ -2941,6 +2946,16 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource { return text } + private static func sourceMonogram(from value: String?) -> String? { + guard let text = presentSourceText(value) else { return nil } + let words = text + .components(separatedBy: CharacterSet.alphanumerics.inverted) + .map(\.clipboardTrimmed) + .filter { !$0.isEmpty } + let initials = words.prefix(2).compactMap(\.first).map { String($0).uppercased() }.joined() + return initials.isEmpty ? nil : initials + } + private func availableCollectionNames() -> [String] { var names: [String] = [] var seen = Set() @@ -4042,6 +4057,9 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource { badge.translatesAutoresizingMaskIntoConstraints = false if let bundleId = item.sourceAppBundleId, let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) { + #if DEBUG + debugHeaderBadgeText = "" + #endif let icon = NSImageView(image: NSWorkspace.shared.icon(forFile: appURL.path)) icon.imageScaling = .scaleProportionallyUpOrDown icon.translatesAutoresizingMaskIntoConstraints = false @@ -4052,7 +4070,28 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource { icon.topAnchor.constraint(equalTo: badge.topAnchor, constant: headerBadgeIconInset), icon.bottomAnchor.constraint(equalTo: badge.bottomAnchor, constant: -headerBadgeIconInset) ]) + } else if let monogram = Self.sourceMonogram(from: itemSourceAppName) { + #if DEBUG + debugHeaderBadgeText = monogram + #endif + let label = NSTextField(labelWithString: monogram) + label.font = .systemFont(ofSize: layout.isCompact ? 15 : 17, weight: .heavy) + label.textColor = accentColor(for: item.kind) + label.alignment = .center + label.lineBreakMode = .byClipping + label.maximumNumberOfLines = 1 + label.setAccessibilityLabel(itemSourceAppName ?? monogram) + label.translatesAutoresizingMaskIntoConstraints = false + badge.addSubview(label) + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(equalTo: badge.leadingAnchor, constant: headerBadgeIconInset), + label.trailingAnchor.constraint(equalTo: badge.trailingAnchor, constant: -headerBadgeIconInset), + label.centerYAnchor.constraint(equalTo: badge.centerYAnchor) + ]) } else { + #if DEBUG + debugHeaderBadgeText = "" + #endif let image = NSImage(systemSymbolName: headerBadgeSymbol(for: item.kind), accessibilityDescription: kindLabel(for: item.kind)) ?? NSImage(systemSymbolName: "doc.on.clipboard", accessibilityDescription: kindLabel(for: item.kind)) ?? NSImage() diff --git a/tests/clipboredtests/ClipboardPanelViewTests.swift b/tests/clipboredtests/ClipboardPanelViewTests.swift index 680610d..743c9dd 100644 --- a/tests/clipboredtests/ClipboardPanelViewTests.swift +++ b/tests/clipboredtests/ClipboardPanelViewTests.swift @@ -632,10 +632,24 @@ final class ClipboardPanelViewTests: XCTestCase { func testCardHeaderUsesKindSymbolBadgeWhenSourceIconIsUnavailable() { let fixture = makePanelFixture() - fixture.store.upsert(makeItem(kind: .url, text: "https://example.com", store: fixture.store)) + var item = makeItem(kind: .url, text: "https://example.com", store: fixture.store) + item.sourceApp = nil + fixture.store.upsert(item) drainMainQueue() XCTAssertEqual(fixture.view.debugCardHeaderBadgeSymbols, ["link"]) + XCTAssertEqual(fixture.view.debugCardHeaderBadgeTexts, [""]) + } + + func testCardHeaderUsesSourceMonogramWhenAppIconIsUnavailable() { + let fixture = makePanelFixture() + var item = makeTextItem("Copied from a named app", store: fixture.store) + item.sourceApp = "Arc Browser" + + fixture.store.upsert(item) + drainMainQueue() + + XCTAssertEqual(fixture.view.debugCardHeaderBadgeTexts, ["AB"]) } func testCollectionRailShowsLiveCounts() {