WIP: show source initials in card badge

This commit is contained in:
Akshay Kolli
2026-06-30 16:56:04 -07:00
parent 4705e687fc
commit 6dcf0d523e
3 changed files with 55 additions and 1 deletions

View File

@@ -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. 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. 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. 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 ## Copy And Paste

View File

@@ -1375,6 +1375,10 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
cardViews.map(\.debugHeaderBadgeSymbol) cardViews.map(\.debugHeaderBadgeSymbol)
} }
var debugCardHeaderBadgeTexts: [String] {
cardViews.map(\.debugHeaderBadgeText)
}
var debugFirstCardHeaderTitle: String { var debugFirstCardHeaderTitle: String {
cardViews.first?.debugHeaderTitle ?? "" cardViews.first?.debugHeaderTitle ?? ""
} }
@@ -2724,6 +2728,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
private(set) var debugPreviewSummary = "" private(set) var debugPreviewSummary = ""
private(set) var debugPreviewStyle = "" private(set) var debugPreviewStyle = ""
private(set) var debugHeaderBadgeSymbol = "" private(set) var debugHeaderBadgeSymbol = ""
private(set) var debugHeaderBadgeText = ""
private(set) var debugHeaderTitle = "" private(set) var debugHeaderTitle = ""
private(set) var debugHeaderSubtitle = "" private(set) var debugHeaderSubtitle = ""
private(set) var debugHeaderColorHex = "" private(set) var debugHeaderColorHex = ""
@@ -2941,6 +2946,16 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
return text 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] { private func availableCollectionNames() -> [String] {
var names: [String] = [] var names: [String] = []
var seen = Set<String>() var seen = Set<String>()
@@ -4042,6 +4057,9 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
badge.translatesAutoresizingMaskIntoConstraints = false badge.translatesAutoresizingMaskIntoConstraints = false
if let bundleId = item.sourceAppBundleId, if let bundleId = item.sourceAppBundleId,
let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) { let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) {
#if DEBUG
debugHeaderBadgeText = ""
#endif
let icon = NSImageView(image: NSWorkspace.shared.icon(forFile: appURL.path)) let icon = NSImageView(image: NSWorkspace.shared.icon(forFile: appURL.path))
icon.imageScaling = .scaleProportionallyUpOrDown icon.imageScaling = .scaleProportionallyUpOrDown
icon.translatesAutoresizingMaskIntoConstraints = false icon.translatesAutoresizingMaskIntoConstraints = false
@@ -4052,7 +4070,28 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
icon.topAnchor.constraint(equalTo: badge.topAnchor, constant: headerBadgeIconInset), icon.topAnchor.constraint(equalTo: badge.topAnchor, constant: headerBadgeIconInset),
icon.bottomAnchor.constraint(equalTo: badge.bottomAnchor, 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 { } else {
#if DEBUG
debugHeaderBadgeText = ""
#endif
let image = NSImage(systemSymbolName: headerBadgeSymbol(for: item.kind), accessibilityDescription: kindLabel(for: item.kind)) let image = NSImage(systemSymbolName: headerBadgeSymbol(for: item.kind), accessibilityDescription: kindLabel(for: item.kind))
?? NSImage(systemSymbolName: "doc.on.clipboard", accessibilityDescription: kindLabel(for: item.kind)) ?? NSImage(systemSymbolName: "doc.on.clipboard", accessibilityDescription: kindLabel(for: item.kind))
?? NSImage() ?? NSImage()

View File

@@ -632,10 +632,24 @@ final class ClipboardPanelViewTests: XCTestCase {
func testCardHeaderUsesKindSymbolBadgeWhenSourceIconIsUnavailable() { func testCardHeaderUsesKindSymbolBadgeWhenSourceIconIsUnavailable() {
let fixture = makePanelFixture() 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() drainMainQueue()
XCTAssertEqual(fixture.view.debugCardHeaderBadgeSymbols, ["link"]) 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() { func testCollectionRailShowsLiveCounts() {