← back to Ticket System
Make desktop ticket bar tiles interactive
bffd86b79314a314639d8ca954beb58f0e9a44b0 · 2026-09-02 14:36:19 -0700 · Steve Abrams
Files touched
M desktop-bar/TicketBar.swift
Diff
commit bffd86b79314a314639d8ca954beb58f0e9a44b0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 2 14:36:19 2026 -0700
Make desktop ticket bar tiles interactive
---
desktop-bar/TicketBar.swift | 134 +++++++++++++++++++++++++++++++++++++++++---
1 file changed, 126 insertions(+), 8 deletions(-)
diff --git a/desktop-bar/TicketBar.swift b/desktop-bar/TicketBar.swift
index 94cbc446..76c11dc8 100644
--- a/desktop-bar/TicketBar.swift
+++ b/desktop-bar/TicketBar.swift
@@ -11,11 +11,54 @@ private struct TicketState {
var updated = ""
}
+private final class BarTile: NSView {
+ let key: String
+ let label: NSTextField
+ var menuProvider: (() -> NSMenu)?
+ var dragHandler: ((String, CGFloat) -> Void)?
+ private var dragged = false
+ var widthConstraint: NSLayoutConstraint!
+
+ init(key: String, label: NSTextField, width: CGFloat) {
+ self.key = key
+ self.label = label
+ super.init(frame: .zero)
+ wantsLayer = true
+ layer?.cornerRadius = 6
+ layer?.backgroundColor = NSColor(calibratedWhite: 0.11, alpha: 0.9).cgColor
+ label.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(label)
+ NSLayoutConstraint.activate([
+ label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 9),
+ label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -9),
+ label.centerYAnchor.constraint(equalTo: centerYAnchor)
+ ])
+ widthConstraint = widthAnchor.constraint(equalToConstant: width)
+ widthConstraint.isActive = true
+ toolTip = "Drag to reorder · click for options"
+ }
+
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
+ override func mouseDown(with event: NSEvent) { dragged = false }
+ override func mouseDragged(with event: NSEvent) {
+ dragged = true
+ dragHandler?(key, event.locationInWindow.x)
+ }
+ override func mouseUp(with event: NSEvent) {
+ guard !dragged, let menu = menuProvider?() else { return }
+ menu.popUp(positioning: nil, at: NSPoint(x: 4, y: bounds.height - 2), in: self)
+ }
+}
+
private final class BarController: NSObject, NSApplicationDelegate {
private var panel: NSPanel!
private let counts = NSTextField(labelWithString: "Tickets · loading…")
private let working = NSTextField(labelWithString: "Working · —")
private let latest = NSTextField(labelWithString: "Latest · —")
+ private var stack: NSStackView!
+ private var tiles: [BarTile] = []
+ private var workingDetails: [String] = []
+ private var latestDetail = "No activity yet"
private var timer: Timer?
func applicationDidFinishLaunching(_ notification: Notification) {
@@ -35,9 +78,6 @@ private final class BarController: NSObject, NSApplicationDelegate {
panel.hasShadow = true
panel.hidesOnDeactivate = false
- let open = NSButton(title: "Open Fleet ↗", target: self, action: #selector(openViewer))
- open.bezelStyle = .inline
- open.contentTintColor = .white
for label in [counts, working, latest] {
label.textColor = .white
label.font = .systemFont(ofSize: 12, weight: label === counts ? .bold : .medium)
@@ -47,20 +87,96 @@ private final class BarController: NSObject, NSApplicationDelegate {
working.textColor = NSColor(calibratedRed: 0.45, green: 0.78, blue: 1, alpha: 1)
latest.textColor = NSColor(calibratedWhite: 0.72, alpha: 1)
- let stack = NSStackView(views: [counts, divider(), working, divider(), latest, open])
+ let savedWidths = UserDefaults.standard.dictionary(forKey: "ticketBarWidths") as? [String: CGFloat] ?? [:]
+ let definitions: [(String, NSTextField, CGFloat)] = [
+ ("counts", counts, 285), ("working", working, 620), ("latest", latest, 560)
+ ]
+ let savedOrder = UserDefaults.standard.stringArray(forKey: "ticketBarOrder") ?? definitions.map(\.0)
+ let byKey = Dictionary(uniqueKeysWithValues: definitions.map { ($0.0, $0) })
+ let ordered = savedOrder.compactMap { byKey[$0] } + definitions.filter { !savedOrder.contains($0.0) }
+ tiles = ordered.map { key, label, defaultWidth in
+ let tile = BarTile(key: key, label: label, width: savedWidths[key] ?? defaultWidth)
+ tile.dragHandler = { [weak self] key, x in self?.moveTile(key, toward: x) }
+ tile.menuProvider = { [weak self] in self?.menu(for: key) ?? NSMenu() }
+ return tile
+ }
+ let open = NSButton(title: "Open Fleet ↗", target: self, action: #selector(openViewer))
+ open.bezelStyle = .inline
+ open.contentTintColor = .white
+ stack = NSStackView(views: tiles + [open])
stack.orientation = .horizontal
- stack.spacing = 12
+ stack.spacing = 6
stack.alignment = .centerY
stack.edgeInsets = NSEdgeInsets(top: 0, left: 14, bottom: 0, right: 10)
- counts.setContentHuggingPriority(.required, for: .horizontal)
- working.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
- latest.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+ for tile in tiles { tile.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) }
open.setContentHuggingPriority(.required, for: .horizontal)
panel.contentView = stack
reposition()
panel.orderFrontRegardless()
}
+ private func moveTile(_ key: String, toward windowX: CGFloat) {
+ guard let source = tiles.firstIndex(where: { $0.key == key }) else { return }
+ var target = source
+ for (index, tile) in tiles.enumerated() where tile.key != key {
+ let mid = tile.convert(tile.bounds, to: nil).midX
+ if windowX > mid { target = index }
+ }
+ if windowX < tiles.first?.convert(tiles.first!.bounds, to: nil).midX ?? 0 { target = 0 }
+ guard target != source else { return }
+ let tile = tiles.remove(at: source)
+ target = min(target, tiles.count)
+ tiles.insert(tile, at: target)
+ stack.removeArrangedSubview(tile)
+ tile.removeFromSuperview()
+ stack.insertArrangedSubview(tile, at: target)
+ UserDefaults.standard.set(tiles.map(\.key), forKey: "ticketBarOrder")
+ }
+
+ private func menu(for key: String) -> NSMenu {
+ let menu = NSMenu(title: key)
+ let details: [String]
+ if key == "counts" {
+ details = [counts.stringValue.replacingOccurrences(of: " ", with: " • ")]
+ } else if key == "working" {
+ details = workingDetails.isEmpty ? ["No tickets currently marked doing"] : workingDetails
+ } else {
+ details = [latestDetail]
+ }
+ for detail in details.prefix(12) {
+ let item = NSMenuItem(title: detail, action: nil, keyEquivalent: "")
+ item.isEnabled = false
+ menu.addItem(item)
+ }
+ menu.addItem(.separator())
+ for (title, action) in [("Move Left", #selector(moveLeft(_:))), ("Move Right", #selector(moveRight(_:))), ("Wider", #selector(wider(_:))), ("Narrower", #selector(narrower(_:)))] {
+ let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
+ item.target = self
+ item.representedObject = key
+ menu.addItem(item)
+ }
+ return menu
+ }
+
+ @objc private func moveLeft(_ sender: NSMenuItem) { shift(sender.representedObject as? String, by: -1) }
+ @objc private func moveRight(_ sender: NSMenuItem) { shift(sender.representedObject as? String, by: 1) }
+ private func shift(_ key: String?, by delta: Int) {
+ guard let key, let index = tiles.firstIndex(where: { $0.key == key }) else { return }
+ let target = max(0, min(tiles.count - 1, index + delta))
+ guard target != index else { return }
+ let tile = tiles.remove(at: index)
+ tiles.insert(tile, at: target)
+ stack.removeArrangedSubview(tile); tile.removeFromSuperview(); stack.insertArrangedSubview(tile, at: target)
+ UserDefaults.standard.set(tiles.map(\.key), forKey: "ticketBarOrder")
+ }
+ @objc private func wider(_ sender: NSMenuItem) { resize(sender.representedObject as? String, by: 80) }
+ @objc private func narrower(_ sender: NSMenuItem) { resize(sender.representedObject as? String, by: -80) }
+ private func resize(_ key: String?, by delta: CGFloat) {
+ guard let tile = tiles.first(where: { $0.key == key }) else { return }
+ tile.widthConstraint.constant = max(180, min(900, tile.widthConstraint.constant + delta))
+ UserDefaults.standard.set(Dictionary(uniqueKeysWithValues: tiles.map { ($0.key, $0.widthConstraint.constant) }), forKey: "ticketBarWidths")
+ }
+
private func divider() -> NSView {
let view = NSView(frame: NSRect(x: 0, y: 0, width: 1, height: 18))
view.wantsLayer = true
@@ -109,6 +225,8 @@ private final class BarController: NSObject, NSApplicationDelegate {
self.counts.stringValue = "Tickets OPEN \(open) BLOCKED \(blocked) DOING \(doing.count)"
self.working.stringValue = "Working · \(workText.isEmpty ? "none" : workText)"
self.latest.stringValue = "Latest · \(latestText)"
+ self.workingDetails = doing.map { "\(self.shortID($0.id)) · \($0.title)" }
+ self.latestDetail = latestText
self.panel.orderFrontRegardless()
}
} catch {
← c73300b3 Record desktop ticket bar proof
·
back to Ticket System
·
Route ticket tile clicks to menus 84f666e5 →