Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

slab/menuband: drop drum pads, click-drag instrument browse, slimmer popover

- Removed the kick / snare / closed-hat menubar pads. DrumPad enum,
drawDrumPad, .drum HitResult case, AppDelegate's drum click handler,
drumsAreaWidth + drumsTrailingGap layout calcs, drumsOriginX,
drumRect — all gone. KeyboardIconRenderer back to piano + chip.
- Instrument map: click-drag is now sonically browsable. mouseDown
starts a drag, mouseDragged fires onHover for each crossed cell
(preview note plays + restarts in the new program), mouseUp commits
the cell under the cursor. Hover (no click) still previews like
before. Click-with-no-drag commits the same cell.
- Popover insets tightened: top/bottom 10 → 8, left/right 12 → 8.
Stack-width-anchored constraints flipped from -24 → -16 to match.
Whole popover hugs the 224 px instrument grid with ~16 px of total
horizontal padding; less negative space.

Site:
- Bump download cache-buster query string to ?v=0621d1f.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+39 -144
-9
slab/menuband/Sources/MenuBand/AppDelegate.swift
··· 272 272 NSSound(named: NSSound.Name("Tink"))?.play() 273 273 showPopover() 274 274 return 275 - case .drum(let drumNote): 276 - // One-shot trigger — drums don't track-drag like piano keys. 277 - // 100 ms hold then release is plenty for the GM drum samples 278 - // to develop their attack. 279 - menuBand.startTapNote(drumNote, velocity: 110, pan: 64) 280 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.10) { [weak menuBand] in 281 - menuBand?.stopTapNote(drumNote) 282 - } 283 - return 284 275 case .note(let n): 285 276 startNote = n 286 277 case .none:
+31 -1
slab/menuband/Sources/MenuBand/InstrumentMapView.swift
··· 153 153 } 154 154 } 155 155 156 + // mouseDown → mouseDragged → mouseUp lets the user click-drag across 157 + // cells to sonically browse: every cell crossed during the drag fires 158 + // onHover (preview note); release commits the cell under the cursor. 159 + private var dragging = false 160 + 156 161 override func mouseDown(with event: NSEvent) { 157 - if let p = program(at: convert(event.locationInWindow, from: nil)) { 162 + dragging = true 163 + let pt = convert(event.locationInWindow, from: nil) 164 + if let p = program(at: pt) { 165 + // Treat the press itself as a hover-into-this-cell so the 166 + // preview note starts immediately on click, not only on 167 + // dragging away. 168 + if hoveredProgram != UInt8(p) { 169 + let prev = hoveredProgram 170 + hoveredProgram = UInt8(p) 171 + if let prev = prev { setNeedsDisplay(cellRect(program: Int(prev))) } 172 + setNeedsDisplay(cellRect(program: p)) 173 + } 174 + onHover?(p) 175 + } 176 + } 177 + 178 + override func mouseDragged(with event: NSEvent) { 179 + guard dragging else { return } 180 + updateHover(at: convert(event.locationInWindow, from: nil)) 181 + } 182 + 183 + override func mouseUp(with event: NSEvent) { 184 + guard dragging else { return } 185 + dragging = false 186 + let pt = convert(event.locationInWindow, from: nil) 187 + if let p = program(at: pt) { 158 188 onCommit?(p) 159 189 } 160 190 }
+2 -128
slab/menuband/Sources/MenuBand/KeyboardIconRenderer.swift
··· 86 86 // tall enough to drag across easily 87 87 static let pad: CGFloat = 0.5 88 88 89 - // Drum sample pads on the left of the piano — Akai MPC style: rounded 90 - // squarish pads with a subtle gradient + colored center, sized to 91 - // match white-key width so the row reads as one cohesive instrument. 92 - enum DrumPad: Int, CaseIterable { 93 - case kick = 36 // GM Bass Drum 1 94 - case snare = 38 // GM Acoustic Snare 95 - case closedHat = 42 // GM Closed Hi-Hat 96 - 97 - var midiNote: UInt8 { UInt8(rawValue) } 98 - 99 - /// Pad accent color — warm Akai orange→pink for kick/snare, cooler 100 - /// gold for hi-hat to differentiate at a glance. 101 - var color: NSColor { 102 - switch self { 103 - case .kick: return NSColor(calibratedRed: 1.00, green: 0.42, blue: 0.20, alpha: 1.0) 104 - case .snare: return NSColor(calibratedRed: 1.00, green: 0.30, blue: 0.50, alpha: 1.0) 105 - case .closedHat: return NSColor(calibratedRed: 1.00, green: 0.78, blue: 0.20, alpha: 1.0) 106 - } 107 - } 108 - } 109 - static let drumPads: [DrumPad] = [.kick, .snare, .closedHat] 110 - static let drumW: CGFloat = whiteW // match white-key width 111 - static let drumH: CGFloat = whiteH 112 - static let drumPianoGap: CGFloat = 5.0 // breathing room between pads + piano 113 - 114 89 // Settings — simple monochrome music note that reads like a native 115 90 // status-bar icon. Click → popup menu with TYPE / MIDI / Instrument / 116 91 // About. ··· 121 96 enum HitResult: Equatable { 122 97 case openSettings 123 98 case note(UInt8) 124 - case drum(UInt8) 125 99 } 126 100 127 101 // Letter labels keyed by MIDI note, per layout. The renderer picks ··· 167 141 CGFloat(whiteList().count) * whiteW 168 142 } 169 143 170 - /// Width of the drum pad strip — zero in compact display layout 171 - /// (no piano keys, just the chip), full in any layout that draws keys. 172 - private static var drumsAreaWidth: CGFloat { 173 - displayLayout == .compact ? 0 : CGFloat(drumPads.count) * drumW 174 - } 175 - private static var drumsTrailingGap: CGFloat { 176 - drumsAreaWidth > 0 ? drumPianoGap : 0 177 - } 178 - 179 144 static var imageSize: NSSize { 180 - let totalW = ceil(pad + drumsAreaWidth + drumsTrailingGap 181 - + pianoWidth + settingsGap + settingsW + pad) 145 + let totalW = ceil(pad + pianoWidth + settingsGap + settingsW + pad) 182 146 let totalH = ceil(whiteH + pad * 2) 183 147 return NSSize(width: totalW, height: totalH) 184 148 } 185 149 186 - private static var drumsOriginX: CGFloat { pad } 187 - private static var pianoOriginX: CGFloat { 188 - pad + drumsAreaWidth + drumsTrailingGap 189 - } 190 - 191 - private static func drumRect(at idx: Int) -> NSRect { 192 - let x = drumsOriginX + CGFloat(idx) * drumW 193 - return NSRect(x: x, y: pad, width: drumW, height: drumH) 194 - } 150 + private static var pianoOriginX: CGFloat { pad } 195 151 196 152 /// Settings chip's visual rect IS its hit-test rect — they're identical 197 153 /// so the user gets visual feedback wherever the click lands. ··· 289 245 } 290 246 NSGraphicsContext.restoreGraphicsState() 291 247 292 - // Drum sample pads — Akai MPC style. Drawn after piano so any 293 - // overlap (none expected, but cheap insurance) sits on top. 294 - if drumsAreaWidth > 0 { 295 - for (idx, pad) in drumPads.enumerated() { 296 - let r = drumRect(at: idx) 297 - let isLit = litNotes.contains(pad.midiNote) 298 - let isHover = hovered == .drum(pad.midiNote) 299 - drawDrumPad(in: r, pad: pad, lit: isLit, hovered: isHover) 300 - } 301 - } 302 - 303 248 // Single settings chip — glyph + color reflect MIDI/DAW state. 304 249 // MIDI on → `waveform` tinted accent (signal flowing to DAW); 305 250 // MIDI off → `slider.horizontal.3` in label color (generic). ··· 323 268 return NSRect(x: leftX, y: 0, width: rightX - leftX, height: imageSize.height) 324 269 } 325 270 326 - // MARK: - Drum pad drawing 327 - 328 - /// Akai MPC sample-pad look: rounded square, vertical gradient (top 329 - /// brighter → bottom darker), thin border, centered glyph. Lit state 330 - /// flashes the pad accent color full-strength + bright glyph. 331 - private static func drawDrumPad(in rect: NSRect, pad: DrumPad, 332 - lit: Bool, hovered: Bool) { 333 - let inset = rect.insetBy(dx: 1.5, dy: 2.0) 334 - let path = NSBezierPath(roundedRect: inset, xRadius: 3, yRadius: 3) 335 - 336 - let baseColor = pad.color 337 - // Hover/lit shift the pad lightness so feedback reads instantly. 338 - let topColor: NSColor 339 - let botColor: NSColor 340 - if lit { 341 - topColor = baseColor.highlight(withLevel: 0.35) ?? baseColor 342 - botColor = baseColor 343 - } else if hovered { 344 - topColor = baseColor.highlight(withLevel: 0.15) ?? baseColor 345 - botColor = baseColor.shadow(withLevel: 0.10) ?? baseColor 346 - } else { 347 - // Resting: darker so lit feels brighter by contrast. 348 - topColor = baseColor.shadow(withLevel: 0.18) ?? baseColor 349 - botColor = baseColor.shadow(withLevel: 0.45) ?? baseColor 350 - } 351 - NSGradient(starting: topColor, ending: botColor)?.draw(in: path, angle: -90) 352 - 353 - // Inner top sheen — 1 px highlight stripe at the top of the pad 354 - // gives the MPC "lit from above" feel. 355 - let sheen = NSBezierPath(roundedRect: NSRect(x: inset.minX + 0.5, 356 - y: inset.maxY - 1.5, 357 - width: inset.width - 1, 358 - height: 1), 359 - xRadius: 0.5, yRadius: 0.5) 360 - NSColor.white.withAlphaComponent(lit ? 0.55 : 0.30).setFill() 361 - sheen.fill() 362 - 363 - // Border. 364 - NSColor.black.withAlphaComponent(0.45).setStroke() 365 - path.lineWidth = 0.5 366 - path.stroke() 367 - 368 - // Glyph — single-letter abbreviation in monospaced bold, centered. 369 - let letter: String 370 - switch pad { 371 - case .kick: letter = "K" 372 - case .snare: letter = "S" 373 - case .closedHat: letter = "H" 374 - } 375 - let attrs: [NSAttributedString.Key: Any] = [ 376 - .font: NSFont.monospacedSystemFont(ofSize: 9, weight: .heavy), 377 - .foregroundColor: lit 378 - ? NSColor.white 379 - : NSColor.white.withAlphaComponent(0.85), 380 - ] 381 - let str = NSAttributedString(string: letter, attributes: attrs) 382 - let size = str.size() 383 - str.draw(at: NSPoint(x: rect.midX - size.width / 2, 384 - y: rect.midY - size.height / 2 - 0.5)) 385 - } 386 - 387 271 // MARK: - Hit testing 388 272 389 273 static func hit(at point: NSPoint) -> HitResult? { 390 274 if settingsHitRect.contains(point) { return .openSettings } 391 - // Drum pads — checked before piano so the area to the left of the 392 - // keyboard maps to drum hits, not falling through to piano edge 393 - // tolerance. 394 - if drumsAreaWidth > 0 { 395 - for (idx, pad) in drumPads.enumerated() { 396 - if drumRect(at: idx).contains(point) { 397 - return .drum(pad.midiNote) 398 - } 399 - } 400 - } 401 275 let whites = whiteList() 402 276 var whiteIndex: [Int: Int] = [:] 403 277 for (i, m) in whites.enumerated() { whiteIndex[m] = i }
+5 -5
slab/menuband/Sources/MenuBand/MenuBandPopover.swift
··· 83 83 root.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor 84 84 root.translatesAutoresizingMaskIntoConstraints = false 85 85 86 - // Vertical stack of rows. Tight spacing + smaller edge insets so 87 - // the popover doesn't carry a lot of negative space. 86 + // Vertical stack of rows. Tight spacing + small edge insets so the 87 + // popover hugs the 224 px instrument grid with no slack. 88 88 let stack = NSStackView() 89 89 stack.orientation = .vertical 90 90 stack.alignment = .leading 91 91 stack.spacing = 6 92 - stack.edgeInsets = NSEdgeInsets(top: 10, left: 12, bottom: 10, right: 12) 92 + stack.edgeInsets = NSEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) 93 93 stack.translatesAutoresizingMaskIntoConstraints = false 94 94 root.addSubview(stack) 95 95 ··· 178 178 179 179 stack.addArrangedSubview(titleRow) 180 180 titleRow.widthAnchor.constraint(equalTo: stack.widthAnchor, 181 - constant: -24).isActive = true 181 + constant: -16).isActive = true 182 182 183 183 // Update banner — hidden until UpdateChecker reports a newer 184 184 // release. Tinted accent so the user notices it without it feeling ··· 401 401 quitRow.addArrangedSubview(quit) 402 402 stack.addArrangedSubview(quitRow) 403 403 quitRow.widthAnchor.constraint(equalTo: stack.widthAnchor, 404 - constant: -24).isActive = true 404 + constant: -16).isActive = true 405 405 406 406 NSLayoutConstraint.activate([ 407 407 stack.leadingAnchor.constraint(equalTo: root.leadingAnchor),
+1 -1
system/public/menuband/index.html
··· 879 879 <p class="tagline">Built-in macOS instruments, in the menu bar.</p> 880 880 881 881 <div class="button-row"> 882 - <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.1.dmg?v=1fbccdf" download> 882 + <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.1.dmg?v=0621d1f" download> 883 883 Download 884 884 <small>0.1 · Apple Silicon · 1.1 MB</small> 885 885 </a>