← back to Ticket System
Ticket desktop bar: dock-position toggle — top/left/right/off with hamburger collapse
3c6638a45cf9304fab1e88322b4953503e9cd0d2 · 2026-09-03 10:50:01 -0700 · Steve Abrams
Adds a position button (▔/▏/▕) that cycles Top → Left → Right → Off:
- Top: full-width horizontal bar (unchanged default).
- Left/Right: same tiles docked vertically against that screen edge (stack flips to
vertical orientation, tiles set to a uniform width).
- Off: collapses the whole bar to a 46x32 ☰ hamburger at the top-left; clicking it
reopens to Top. Off detaches the stack from the view so its width can't force the
panel wide (the fix for the panel refusing to shrink).
Position persists in ticketBarPosition; vertical drag-slide is gated to Top mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XNcHvXXepEQar7Qsb7GGy
Files touched
M desktop-bar/TicketBar.swift
Diff
commit 3c6638a45cf9304fab1e88322b4953503e9cd0d2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 3 10:50:01 2026 -0700
Ticket desktop bar: dock-position toggle — top/left/right/off with hamburger collapse
Adds a position button (▔/▏/▕) that cycles Top → Left → Right → Off:
- Top: full-width horizontal bar (unchanged default).
- Left/Right: same tiles docked vertically against that screen edge (stack flips to
vertical orientation, tiles set to a uniform width).
- Off: collapses the whole bar to a 46x32 ☰ hamburger at the top-left; clicking it
reopens to Top. Off detaches the stack from the view so its width can't force the
panel wide (the fix for the panel refusing to shrink).
Position persists in ticketBarPosition; vertical drag-slide is gated to Top mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XNcHvXXepEQar7Qsb7GGy
---
desktop-bar/TicketBar.swift | 112 +++++++++++++++++++++++++++++++++++++-------
1 file changed, 94 insertions(+), 18 deletions(-)
diff --git a/desktop-bar/TicketBar.swift b/desktop-bar/TicketBar.swift
index d84402a0..f523b331 100644
--- a/desktop-bar/TicketBar.swift
+++ b/desktop-bar/TicketBar.swift
@@ -77,7 +77,14 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
private var tiles: [BarTile] = []
private var openButton: NSButton!
private var themeButton: NSButton!
+ private var posButton: NSButton!
+ private var hamburger: NSButton!
private var isNight = true
+ private var barPosition = "top" // top | left | right | off
+ private var horizWidths: [String: CGFloat] = [:] // tile widths captured for horizontal (top) mode
+ private let vbarWidth: CGFloat = 300
+ private var contentContainer: NSView!
+ private var stackConstraints: [NSLayoutConstraint] = []
private var statusDetails: [String: [String]] = [:]
private var workingDetails: [String] = []
private var latestDetail = "No activity yet"
@@ -109,6 +116,7 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
panel.hidesOnDeactivate = false
isNight = UserDefaults.standard.object(forKey: "ticketBarNight") as? Bool ?? true
+ barPosition = UserDefaults.standard.string(forKey: "ticketBarPosition") ?? "top"
let countLabels = [openLabel, blockedLabel, doingLabel]
for label in countLabels + [working, latest] {
label.font = .systemFont(ofSize: 12, weight: countLabels.contains(label) ? .bold : .medium)
@@ -138,6 +146,7 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
// "Open Control Window" item and from the "Open Fleet ↗" button.
return tile
}
+ horizWidths = Dictionary(uniqueKeysWithValues: tiles.map { ($0.key, $0.widthConstraint.constant) })
let open = NSButton(title: "Open Fleet ↗", target: self, action: #selector(openViewer))
open.bezelStyle = .inline
openButton = open
@@ -145,7 +154,11 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
themeButton.bezelStyle = .inline
themeButton.isBordered = false
themeButton.toolTip = "Toggle night / day"
- stack = NSStackView(views: tiles + [open, themeButton])
+ posButton = NSButton(title: "▔", target: self, action: #selector(cyclePosition))
+ posButton.bezelStyle = .inline
+ posButton.isBordered = false
+ posButton.toolTip = "Dock position: top → left → right → off"
+ stack = NSStackView(views: tiles + [open, themeButton, posButton])
stack.orientation = .horizontal
stack.spacing = 6
stack.alignment = .centerY
@@ -153,21 +166,94 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
for tile in tiles { tile.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) }
open.setContentHuggingPriority(.required, for: .horizontal)
themeButton.setContentHuggingPriority(.required, for: .horizontal)
+ posButton.setContentHuggingPriority(.required, for: .horizontal)
- // Center the tile group horizontally in the full-width bar (was left-anchored).
- let container = NSView()
+ // Center the tile group in the bar. Collapsed hamburger (off-mode) lives in the
+ // top-left corner and is shown only when barPosition == "off". In off mode the
+ // stack is REMOVED from the view so its width can't force the panel wide.
+ contentContainer = NSView()
+ let container = contentContainer!
stack.translatesAutoresizingMaskIntoConstraints = false
+ container.clipsToBounds = true
container.addSubview(stack)
- NSLayoutConstraint.activate([
+ stackConstraints = [
stack.centerXAnchor.constraint(equalTo: container.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: container.centerYAnchor),
stack.leadingAnchor.constraint(greaterThanOrEqualTo: container.leadingAnchor, constant: 14),
stack.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor, constant: -10)
+ ]
+ NSLayoutConstraint.activate(stackConstraints)
+ hamburger = NSButton(title: "☰", target: self, action: #selector(openFromHamburger))
+ hamburger.isBordered = false
+ hamburger.font = .systemFont(ofSize: 18, weight: .bold)
+ hamburger.contentTintColor = .white
+ hamburger.toolTip = "Show ticket bar"
+ hamburger.translatesAutoresizingMaskIntoConstraints = false
+ hamburger.isHidden = true
+ container.addSubview(hamburger)
+ NSLayoutConstraint.activate([
+ hamburger.centerXAnchor.constraint(equalTo: container.centerXAnchor),
+ hamburger.centerYAnchor.constraint(equalTo: container.centerYAnchor)
])
panel.contentView = container
installDragMonitor()
applyTheme()
- reposition()
+ applyPosition()
+ panel.orderFrontRegardless()
+ }
+
+ private static let positionOrder = ["top", "left", "right", "off"]
+ @objc private func cyclePosition() {
+ let i = (BarController.positionOrder.firstIndex(of: barPosition) ?? 0) + 1
+ setPosition(BarController.positionOrder[i % BarController.positionOrder.count])
+ }
+ @objc private func openFromHamburger() { setPosition("top") }
+ private func setPosition(_ p: String) {
+ barPosition = p
+ UserDefaults.standard.set(p, forKey: "ticketBarPosition")
+ applyPosition()
+ }
+
+ // Dock the bar top (horizontal), left/right (vertical), or collapse to a ☰ hamburger.
+ private func applyPosition() {
+ guard let screen = panel.screen ?? NSScreen.main ?? NSScreen.screens.first else { return }
+ let v = screen.visibleFrame
+ if barPosition == "off" {
+ if stack.superview != nil {
+ NSLayoutConstraint.deactivate(stackConstraints)
+ stack.removeFromSuperview()
+ }
+ hamburger.isHidden = false
+ let w: CGFloat = 46, h: CGFloat = 32
+ panel.setFrame(NSRect(x: v.minX + 8, y: v.maxY - h, width: w, height: h), display: true)
+ panel.orderFrontRegardless()
+ return
+ }
+ if stack.superview == nil {
+ contentContainer.addSubview(stack)
+ NSLayoutConstraint.activate(stackConstraints)
+ }
+ stack.isHidden = false
+ hamburger.isHidden = true
+ let vertical = (barPosition == "left" || barPosition == "right")
+ stack.orientation = vertical ? .vertical : .horizontal
+ stack.alignment = vertical ? .leading : .centerY
+ // Tile widths: uniform in a vertical bar; restore the horizontal widths on top.
+ for tile in tiles {
+ tile.widthConstraint.constant = vertical ? (vbarWidth - 44) : (horizWidths[tile.key] ?? tile.widthConstraint.constant)
+ }
+ switch barPosition {
+ case "left":
+ panel.setFrame(NSRect(x: v.minX, y: v.minY, width: vbarWidth, height: v.height), display: true)
+ case "right":
+ panel.setFrame(NSRect(x: v.maxX - vbarWidth, y: v.minY, width: vbarWidth, height: v.height), display: true)
+ default: // top
+ let barHeight: CGFloat = 38
+ let requested = CGFloat(UserDefaults.standard.double(forKey: "ticketBarYOffset"))
+ let offset = min(max(0, requested), max(0, v.height - barHeight))
+ panel.setFrame(NSRect(x: v.minX, y: v.maxY - barHeight - offset, width: v.width, height: barHeight), display: true)
+ }
+ posButton.title = ["top": "▔", "left": "▏", "right": "▕"][barPosition] ?? "▔"
panel.orderFrontRegardless()
}
@@ -218,8 +304,8 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
case .leftMouseDragged:
let dy = NSEvent.mouseLocation.y - (self.dragStartMouseY ?? NSEvent.mouseLocation.y)
let dx = event.locationInWindow.x - (self.dragStartX ?? event.locationInWindow.x)
- // Vertical drag → SLIDE the whole bar up/down. Horizontal drag on a tile → reorder.
- if self.barSlid || (abs(dy) > 4 && abs(dy) >= abs(dx)) {
+ // Vertical drag → SLIDE the whole bar up/down (Top mode only). Horizontal drag on a tile → reorder.
+ if self.barPosition == "top", self.barSlid || (abs(dy) > 4 && abs(dy) >= abs(dx)) {
self.barSlid = true
self.didDrag = true
self.slideBar(toPanelY: (self.dragStartPanelY ?? self.panel.frame.origin.y) + dy)
@@ -369,17 +455,7 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
return view
}
- @objc private func reposition() {
- guard let screen = NSScreen.main ?? NSScreen.screens.first else { return }
- let frame = screen.visibleFrame
- let barHeight: CGFloat = 38
- let requested = CGFloat(UserDefaults.standard.double(forKey: "ticketBarYOffset")) // px below the top
- let maxOffset = max(0, frame.height - barHeight)
- let offset = min(max(0, requested), maxOffset)
- if offset != requested { UserDefaults.standard.set(Double(offset), forKey: "ticketBarYOffset") }
- let y = frame.maxY - barHeight - offset
- panel.setFrame(NSRect(x: frame.minX, y: y, width: frame.width, height: barHeight), display: true)
- }
+ @objc private func reposition() { applyPosition() }
@objc private func openViewer() { NSWorkspace.shared.open(viewerURL) }
← 59e3c395 Ticket desktop bar: show priority rank/score + created date/
·
back to Ticket System
·
record bounded yoloforever monitoring cycle c92cb5c9 →