WIP: add compact shelf cards

This commit is contained in:
Akshay Kolli
2026-06-30 03:11:40 -07:00
parent 3c58ab8c38
commit 870e284fbd
5 changed files with 186 additions and 64 deletions

View File

@@ -1,6 +1,6 @@
# ClipBored
ClipBored is a small native macOS clipboard manager. It captures local clipboard history and opens a keyboard-first bottom panel for search, sorting, copy, paste, pinning, and deletion. It runs as a dockless menu-bar utility by default, with an optional Dock icon mode.
ClipBored is a small native macOS clipboard manager. It captures local clipboard history and opens a keyboard-first responsive bottom panel for search, sorting, copy, paste, pinning, and deletion. It runs as a dockless menu-bar utility by default, with an optional Dock icon mode.
The project is intentionally dependency-light: Swift Package Manager, AppKit, Carbon hotkeys, SQLite, and system frameworks only.

View File

@@ -15,7 +15,6 @@ This roadmap keeps future work aligned with the project's constraints: small exe
## Product Polish
- Improve keyboard focus states and VoiceOver labels.
- Add a compact mode for narrower displays.
- Add import/export only if the storage and privacy story remains clear.
## Performance

View File

@@ -43,6 +43,7 @@ Use this checklist before a release or after changes to panel, pasteboard, setti
10. Double-click an item and confirm it attempts to paste or falls back to copy without creating a duplicate history entry.
11. Right-click a card, use Capture Rules to ignore its source app, copy from that app again, and confirm the new item is skipped.
12. Drag an unassigned card onto the Client Work chip and confirm the chip count increases and the card appears when Client Work is selected.
13. Resize or test on a narrow display and confirm the bottom shelf switches to compact cards that still show two recent clips cleanly.
## Copy And Paste

View File

@@ -1,12 +1,47 @@
import AppKit
private struct ClipboardItemCardLayout: Equatable {
let width: CGFloat
let height: CGFloat
let inset: CGFloat
let headerHeight: CGFloat
let bodyHeight: CGFloat
let footerHeight: CGFloat
let actionButtonSize: CGFloat
let primaryActionButtonSize: CGFloat
let actionRailHeight: CGFloat
static let regular = ClipboardItemCardLayout(
width: 320,
height: 244,
inset: 16,
headerHeight: 56,
bodyHeight: 152,
footerHeight: 36,
actionButtonSize: 24,
primaryActionButtonSize: 30,
actionRailHeight: 34
)
static let compact = ClipboardItemCardLayout(
width: 264,
height: 220,
inset: 13,
headerHeight: 50,
bodyHeight: 138,
footerHeight: 32,
actionButtonSize: 22,
primaryActionButtonSize: 28,
actionRailHeight: 32
)
var isCompact: Bool {
self == Self.compact
}
}
final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
private enum Metrics {
static let cardRailHeight: CGFloat = 266
static let cardWidth: CGFloat = 320
static let cardHeight: CGFloat = 244
static let cardSpacing: CGFloat = 16
static let cardStackInset: CGFloat = 10
static let actionButtonSize: CGFloat = 30
static let panelTopInset: CGFloat = 12
static let panelSideInset: CGFloat = 22
@@ -14,6 +49,49 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
static let panelStatusBarHeight: CGFloat = 24
static let minimumBottomInset: CGFloat = 20
static let panelCornerRadius: CGFloat = 0
static let compactCardThreshold: CGFloat = 760
static let emptyStateMinimumWidth: CGFloat = 760
}
private enum CardDensity: String {
case regular
case compact
static func fitting(width: CGFloat) -> CardDensity {
width > 0 && width < Metrics.compactCardThreshold ? .compact : .regular
}
var layout: ClipboardItemCardLayout {
switch self {
case .regular: return .regular
case .compact: return .compact
}
}
var cardSpacing: CGFloat {
switch self {
case .regular: return 16
case .compact: return 12
}
}
var cardStackInset: CGFloat {
switch self {
case .regular: return 10
case .compact: return 8
}
}
var railHeight: CGFloat {
layout.height + (cardStackInset * 2) + 2
}
var emptyStateMinimumWidth: CGFloat {
switch self {
case .regular: return Metrics.emptyStateMinimumWidth
case .compact: return 420
}
}
}
private enum Palette {
@@ -66,6 +144,8 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
private var mainStack: NSStackView?
private var bottomSafeInset = Metrics.minimumBottomInset
private var currentStatusTone: StatusTone = .ready
private var cardDensity: CardDensity = .regular
private var scrollViewHeightConstraint: NSLayoutConstraint?
private var cardViews: [ClipboardItemCardView] = []
private var collectionButtons: [ClipboardSortMode: CollectionChipView] = [:]
private var customCollectionButtons: [String: CollectionChipView] = [:]
@@ -206,13 +286,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
itemsStack.orientation = .horizontal
itemsStack.alignment = .top
itemsStack.spacing = Metrics.cardSpacing
itemsStack.edgeInsets = NSEdgeInsets(
top: Metrics.cardStackInset,
left: Metrics.cardStackInset,
bottom: Metrics.cardStackInset,
right: Metrics.cardStackInset
)
applyCardDensity()
itemsStack.translatesAutoresizingMaskIntoConstraints = true
scrollView.documentView = itemsStack
scrollView.hasHorizontalScroller = true
@@ -223,7 +297,8 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
scrollView.borderType = .noBorder
scrollView.setContentHuggingPriority(.required, for: .vertical)
scrollView.setContentCompressionResistancePriority(.required, for: .vertical)
scrollView.heightAnchor.constraint(equalToConstant: Metrics.cardRailHeight).isActive = true
scrollViewHeightConstraint = scrollView.heightAnchor.constraint(equalToConstant: cardDensity.railHeight)
scrollViewHeightConstraint?.isActive = true
statusLabel.font = .systemFont(ofSize: NSFont.systemFontSize - 1)
statusLabel.textColor = .secondaryLabelColor
@@ -474,6 +549,29 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
}
}
private func applyCardDensity() {
itemsStack.spacing = cardDensity.cardSpacing
let inset = cardDensity.cardStackInset
itemsStack.edgeInsets = NSEdgeInsets(
top: inset,
left: inset,
bottom: inset,
right: inset
)
scrollViewHeightConstraint?.constant = cardDensity.railHeight
}
@discardableResult
private func updateCardDensityForCurrentWidth() -> Bool {
let targetDensity = CardDensity.fitting(width: bounds.width)
guard targetDensity != cardDensity else { return false }
cardDensity = targetDensity
applyCardDensity()
reloadItems()
return true
}
private func contentInsets() -> NSEdgeInsets {
NSEdgeInsets(
top: Metrics.panelTopInset,
@@ -531,11 +629,13 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
scrollView.documentView = itemsStack
}
let collectionNames = viewModel.collectionNames
let layout = cardDensity.layout
for (index, item) in items.enumerated() {
let card = ClipboardItemCardView(
item: item,
thumbnail: viewModel.thumbnail(for: item),
index: index,
layout: layout,
collectionNames: collectionNames,
isStacked: viewModel.isItemStacked(at: index),
stackCount: viewModel.stackCount
@@ -689,7 +789,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
itemsStack.layoutSubtreeIfNeeded()
let frame = card.convert(card.bounds, to: itemsStack)
let paddedFrame = frame.insetBy(dx: -Metrics.cardSpacing, dy: 0)
let paddedFrame = frame.insetBy(dx: -cardDensity.cardSpacing, dy: 0)
itemsStack.scrollToVisible(paddedFrame)
scrollView.reflectScrolledClipView(scrollView.contentView)
}
@@ -780,8 +880,8 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
}
private func emptyStateView() -> NSView {
let width = max(760, scrollView.contentView.bounds.width)
let container = NSView(frame: NSRect(x: 0, y: 0, width: width, height: Metrics.cardRailHeight))
let width = max(cardDensity.emptyStateMinimumWidth, scrollView.contentView.bounds.width)
let container = NSView(frame: NSRect(x: 0, y: 0, width: width, height: cardDensity.railHeight))
let copy = emptyStateCopy()
let title = NSTextField(labelWithString: copy.title)
title.font = .systemFont(ofSize: 14, weight: .medium)
@@ -811,9 +911,9 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
private func sizeItemsDocument(itemCount: Int) {
let count = CGFloat(itemCount)
let contentWidth = (count * Metrics.cardWidth)
+ max(0, count - 1) * Metrics.cardSpacing
+ (Metrics.cardStackInset * 2)
let contentWidth = (count * cardDensity.layout.width)
+ max(0, count - 1) * cardDensity.cardSpacing
+ (cardDensity.cardStackInset * 2)
let width = max(scrollView.contentView.bounds.width, contentWidth)
lastScrollContentWidth = width
itemsStack.frame = NSRect(x: 0, y: 0, width: width, height: currentListHeight())
@@ -822,7 +922,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
}
private func currentListHeight() -> CGFloat {
Metrics.cardHeight + (Metrics.cardStackInset * 2)
cardDensity.layout.height + (cardDensity.cardStackInset * 2)
}
private func emptyStateCopy() -> (title: String, detail: String) {
@@ -897,6 +997,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
override func layout() {
super.layout()
_ = updateCardDensityForCurrentWidth()
let collectionViewportWidth = collectionScrollView.contentView.bounds.width
if collectionViewportWidth != lastCollectionViewportWidth {
lastCollectionViewportWidth = collectionViewportWidth
@@ -913,7 +1014,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
if cardViews.isEmpty {
guard let documentView = scrollView.documentView else { return }
documentView.frame.size = NSSize(
width: max(760, scrollView.contentView.bounds.width),
width: max(cardDensity.emptyStateMinimumWidth, scrollView.contentView.bounds.width),
height: currentListHeight()
)
return
@@ -997,6 +1098,14 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
cardViews.map(\.debugPreviewStyle)
}
var debugCardDensity: String {
cardDensity.rawValue
}
var debugCardSizes: [NSSize] {
cardViews.map { $0.frame.size }
}
var debugCardHeaderBadgeSymbols: [String] {
cardViews.map(\.debugHeaderBadgeSymbol)
}
@@ -1464,15 +1573,6 @@ private final class AspectFillImageView: NSView {
private final class ClipboardItemCardView: NSView, NSDraggingSource {
private enum Metrics {
static let width: CGFloat = 320
static let height: CGFloat = 244
static let inset: CGFloat = 16
static let headerHeight: CGFloat = 56
static let bodyHeight: CGFloat = 152
static let footerHeight: CGFloat = 36
static let actionButtonSize: CGFloat = 24
static let primaryActionButtonSize: CGFloat = 30
static let actionRailHeight: CGFloat = 34
static let dragThreshold: CGFloat = 4
}
private enum Palette {
@@ -1507,6 +1607,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
private let index: Int
private let itemID: UUID
private let layout: ClipboardItemCardLayout
private let itemKind: ClipboardItemKind
private let itemIsPinned: Bool
private let itemIsStacked: Bool
@@ -1531,12 +1632,14 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
item: ClipboardItem,
thumbnail: NSImage?,
index: Int,
layout: ClipboardItemCardLayout = .regular,
collectionNames: [String] = [],
isStacked: Bool = false,
stackCount: Int = 0
) {
self.index = index
self.itemID = item.id
self.layout = layout
self.itemKind = item.kind
self.itemIsPinned = item.isPinned
self.itemIsStacked = isStacked
@@ -1895,7 +1998,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
actionRail.spacing = 4
actionRail.edgeInsets = NSEdgeInsets(top: 2, left: 6, bottom: 2, right: 6)
actionRail.wantsLayer = true
actionRail.layer?.cornerRadius = Metrics.actionRailHeight / 2
actionRail.layer?.cornerRadius = layout.actionRailHeight / 2
actionRail.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.44).cgColor
actionRail.layer?.borderWidth = 0.5
actionRail.layer?.borderColor = NSColor.white.withAlphaComponent(0.18).cgColor
@@ -1904,7 +2007,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
actionRail.layer?.shadowRadius = 10
actionRail.layer?.shadowOffset = NSSize(width: 0, height: 4)
actionRail.translatesAutoresizingMaskIntoConstraints = false
actionRail.heightAnchor.constraint(equalToConstant: Metrics.actionRailHeight).isActive = true
actionRail.heightAnchor.constraint(equalToConstant: layout.actionRailHeight).isActive = true
actionRail.setContentHuggingPriority(.required, for: .horizontal)
actionRail.setContentCompressionResistancePriority(.required, for: .horizontal)
@@ -1934,8 +2037,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
}
let buttonCount = CGFloat(actionRailButtons.count)
let secondaryCount = CGFloat(max(0, actionRailButtons.count - 1))
let contentWidth = Metrics.primaryActionButtonSize
+ secondaryCount * Metrics.actionButtonSize
let contentWidth = layout.primaryActionButtonSize
+ secondaryCount * layout.actionButtonSize
+ max(0, buttonCount - 1) * actionRail.spacing
+ actionRail.edgeInsets.left
+ actionRail.edgeInsets.right
@@ -1957,7 +2060,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
button.imageScaling = .scaleProportionallyDown
button.isBordered = false
button.wantsLayer = true
let size = isPrimary ? Metrics.primaryActionButtonSize : Metrics.actionButtonSize
let size = isPrimary ? layout.primaryActionButtonSize : layout.actionButtonSize
button.layer?.cornerRadius = size / 2
button.layer?.backgroundColor = isPrimary
? NSColor.controlAccentColor.cgColor
@@ -2094,8 +2197,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
setAccessibilityRole(.button)
setAccessibilityLabel(accessibilityTitle(for: item))
setAccessibilityHelp("Selects this clipboard item. Double-click to paste.")
widthAnchor.constraint(equalToConstant: Metrics.width).isActive = true
heightAnchor.constraint(equalToConstant: Metrics.height).isActive = true
widthAnchor.constraint(equalToConstant: layout.width).isActive = true
heightAnchor.constraint(equalToConstant: layout.height).isActive = true
contentView.wantsLayer = true
contentView.layer?.cornerRadius = 8
@@ -2142,17 +2245,17 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
let header = NSView()
header.wantsLayer = true
header.layer?.backgroundColor = accentColor(for: item.kind).cgColor
header.heightAnchor.constraint(equalToConstant: Metrics.headerHeight).isActive = true
header.heightAnchor.constraint(equalToConstant: layout.headerHeight).isActive = true
let kind = NSTextField(labelWithString: kindLabel(for: item.kind))
kind.font = .systemFont(ofSize: 16, weight: .bold)
kind.font = .systemFont(ofSize: layout.isCompact ? 15 : 16, weight: .bold)
kind.textColor = .white
kind.lineBreakMode = .byTruncatingTail
kind.maximumNumberOfLines = 1
kind.toolTip = kind.stringValue
let source = NSTextField(labelWithString: Self.relativeDateText(for: item.createdAt))
source.font = .systemFont(ofSize: 11, weight: .regular)
source.font = .systemFont(ofSize: layout.isCompact ? 10 : 11, weight: .regular)
source.textColor = NSColor.white.withAlphaComponent(0.72)
source.lineBreakMode = .byTruncatingTail
source.maximumNumberOfLines = 1
@@ -2191,15 +2294,15 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
source.setContentHuggingPriority(.defaultLow, for: .horizontal)
var constraints: [NSLayoutConstraint] = [
labelStack.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: Metrics.inset),
labelStack.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: layout.inset),
labelStack.centerYAnchor.constraint(equalTo: header.centerYAnchor),
labelStack.trailingAnchor.constraint(lessThanOrEqualTo: badge.leadingAnchor, constant: -12),
badge.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -Metrics.inset),
badge.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -layout.inset),
badge.centerYAnchor.constraint(equalTo: header.centerYAnchor),
badge.widthAnchor.constraint(equalToConstant: 42),
badge.heightAnchor.constraint(equalToConstant: 42),
separator.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: Metrics.inset),
separator.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -Metrics.inset),
badge.widthAnchor.constraint(equalToConstant: layout.isCompact ? 36 : 42),
badge.heightAnchor.constraint(equalToConstant: layout.isCompact ? 36 : 42),
separator.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: layout.inset),
separator.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -layout.inset),
separator.bottomAnchor.constraint(equalTo: header.bottomAnchor),
separator.heightAnchor.constraint(equalToConstant: 1)
]
@@ -2246,7 +2349,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
let body = NSView()
body.wantsLayer = true
body.layer?.backgroundColor = Palette.bodyBackground
body.heightAnchor.constraint(equalToConstant: Metrics.bodyHeight).isActive = true
body.heightAnchor.constraint(equalToConstant: layout.bodyHeight).isActive = true
let content = previewView(for: item, thumbnail: thumbnail)
body.addSubview(content)
@@ -2311,8 +2414,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
title.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
detail.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Metrics.inset),
stack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -Metrics.inset),
stack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
stack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
stack.topAnchor.constraint(equalTo: container.topAnchor, constant: 16),
stack.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor, constant: -14)
])
@@ -2377,8 +2480,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
globe.widthAnchor.constraint(equalToConstant: 28),
globe.heightAnchor.constraint(equalToConstant: 28),
host.widthAnchor.constraint(lessThanOrEqualTo: hero.widthAnchor, constant: -48),
textStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Metrics.inset),
textStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -Metrics.inset),
textStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
textStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
textStack.topAnchor.constraint(equalTo: hero.bottomAnchor, constant: 11),
textStack.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor, constant: -10),
title.widthAnchor.constraint(equalTo: textStack.widthAnchor),
@@ -2427,8 +2530,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
imageView.topAnchor.constraint(equalTo: container.topAnchor),
hostPill.leadingAnchor.constraint(equalTo: imageView.leadingAnchor, constant: 12),
hostPill.bottomAnchor.constraint(equalTo: imageView.bottomAnchor, constant: -10),
textStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Metrics.inset),
textStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -Metrics.inset),
textStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
textStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
textStack.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 10),
textStack.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor, constant: -8),
title.widthAnchor.constraint(equalTo: textStack.widthAnchor),
@@ -2495,8 +2598,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
container.addSubview(row)
NSLayoutConstraint.activate([
row.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Metrics.inset),
row.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -Metrics.inset),
row.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
row.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
row.centerYAnchor.constraint(equalTo: container.centerYAnchor),
iconBox.widthAnchor.constraint(equalToConstant: thumbnail == nil ? 72 : 96),
iconBox.heightAnchor.constraint(equalToConstant: thumbnail == nil ? 84 : 104),
@@ -2567,8 +2670,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
note.heightAnchor.constraint(equalToConstant: 28),
waveform.centerXAnchor.constraint(equalTo: container.centerXAnchor),
waveform.topAnchor.constraint(equalTo: note.bottomAnchor, constant: 8),
labels.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Metrics.inset),
labels.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -Metrics.inset),
labels.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
labels.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
labels.topAnchor.constraint(equalTo: waveform.bottomAnchor, constant: 10),
title.widthAnchor.constraint(equalTo: labels.widthAnchor),
detail.widthAnchor.constraint(equalTo: labels.widthAnchor)
@@ -2668,7 +2771,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
let footer = NSView()
footer.wantsLayer = true
footer.layer?.backgroundColor = Palette.footerBackground
footer.heightAnchor.constraint(equalToConstant: Metrics.footerHeight).isActive = true
footer.heightAnchor.constraint(equalToConstant: layout.footerHeight).isActive = true
let source = NSTextField(labelWithString: sourceText(for: item))
source.font = .systemFont(ofSize: NSFont.smallSystemFontSize, weight: .medium)
@@ -2707,12 +2810,12 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
footer.addSubview(divider)
footer.addSubview(stack)
NSLayoutConstraint.activate([
divider.leadingAnchor.constraint(equalTo: footer.leadingAnchor, constant: Metrics.inset),
divider.trailingAnchor.constraint(equalTo: footer.trailingAnchor, constant: -Metrics.inset),
divider.leadingAnchor.constraint(equalTo: footer.leadingAnchor, constant: layout.inset),
divider.trailingAnchor.constraint(equalTo: footer.trailingAnchor, constant: -layout.inset),
divider.topAnchor.constraint(equalTo: footer.topAnchor),
divider.heightAnchor.constraint(equalToConstant: 1),
stack.leadingAnchor.constraint(equalTo: footer.leadingAnchor, constant: Metrics.inset),
stack.trailingAnchor.constraint(equalTo: footer.trailingAnchor, constant: -Metrics.inset),
stack.leadingAnchor.constraint(equalTo: footer.leadingAnchor, constant: layout.inset),
stack.trailingAnchor.constraint(equalTo: footer.trailingAnchor, constant: -layout.inset),
stack.centerYAnchor.constraint(equalTo: footer.centerYAnchor)
])
return footer

View File

@@ -59,6 +59,25 @@ final class ClipboardPanelViewTests: XCTestCase {
XCTAssertEqual(fixture.view.debugCardPreviewStyles, ["text-preview"])
}
func testCompactCardsFitTwoItemsOnNarrowDockShelf() {
let fixture = makePanelFixture()
fixture.window.setFrame(NSRect(x: 0, y: 0, width: 620, height: 520), display: true)
fixture.store.upsert(makeTextItem("Compact first", store: fixture.store))
fixture.store.upsert(makeTextItem("Compact second", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.view.debugCardDensity, "compact")
XCTAssertEqual(fixture.view.debugVisibleCardCount, 2)
XCTAssertEqual(fixture.view.debugCardSizes.count, 2)
XCTAssertEqual(fixture.view.debugCardSizes.first?.width ?? 0, 264, accuracy: 0.5)
XCTAssertEqual(fixture.view.debugCardSizes.first?.height ?? 0, 220, accuracy: 0.5)
XCTAssertLessThanOrEqual(
fixture.view.debugDocumentViewFrame.width,
fixture.view.debugCardRailVisibleRect.width + 1
)
}
func testCardsShowQuickPasteNumberBadgesForFirstNineItems() {
let fixture = makePanelFixture()
for index in 0..<10 {