From 870e284fbdab4c7fc84c02e7a3ea71bd72dfb4e3 Mon Sep 17 00:00:00 2001 From: Akshay Kolli Date: Tue, 30 Jun 2026 03:11:40 -0700 Subject: [PATCH] WIP: add compact shelf cards --- README.md | 2 +- docs/ROADMAP.md | 1 - docs/SMOKE_TEST.md | 1 + .../clipbored/views/ClipboardPanelView.swift | 227 +++++++++++++----- .../ClipboardPanelViewTests.swift | 19 ++ 5 files changed, 186 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 5e5b590..b7a9878 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7e9ebd5..fc1d536 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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 diff --git a/docs/SMOKE_TEST.md b/docs/SMOKE_TEST.md index 7f9ae3d..3f0c952 100644 --- a/docs/SMOKE_TEST.md +++ b/docs/SMOKE_TEST.md @@ -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 diff --git a/sources/clipbored/views/ClipboardPanelView.swift b/sources/clipbored/views/ClipboardPanelView.swift index 120a62d..56dc599 100644 --- a/sources/clipbored/views/ClipboardPanelView.swift +++ b/sources/clipbored/views/ClipboardPanelView.swift @@ -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 diff --git a/tests/clipboredtests/ClipboardPanelViewTests.swift b/tests/clipboredtests/ClipboardPanelViewTests.swift index 5bf1ab7..a3f1d97 100644 --- a/tests/clipboredtests/ClipboardPanelViewTests.swift +++ b/tests/clipboredtests/ClipboardPanelViewTests.swift @@ -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 {