Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## 2024-06-03 - Missing tooltips and accessibility on list item actions
**Learning:** Icon-only buttons used for item actions (like 'Edit', 'Delete', 'Remove') within list views often lack tooltips and accessibility labels. This omission makes the UI ambiguous for mouse users on macOS and inaccessible to screen readers.
**Action:** Always add `.help("Action Name")` and `.accessibilityLabel("Action Name Item Type")` to icon-only buttons, especially those located at the trailing edge of list rows.

## 2024-11-20 - Ensure Symmetry in Accessibility and Tooltips for SwiftUI Icon Buttons
**Learning:** In macOS SwiftUI applications, icon-only buttons often have either `.help()` (for hover tooltips) or `.accessibilityLabel()` (for VoiceOver), but frequently lack both. Both are necessary because they serve different user interaction models—`.help` for visual hover feedback and `.accessibilityLabel` for screen readers.
**Action:** When adding or reviewing icon-only buttons in SwiftUI, always ensure symmetry by defining both `.help("Description")` and `.accessibilityLabel("Description")` to cover all accessibility and usability vectors.
## 2026-06-06 - Ensure Symmetry in Accessibility and Tooltips for SwiftUI Icon Buttons
**Learning:** In macOS SwiftUI applications, icon-only buttons often have either `.help()` (for hover tooltips) or `.accessibilityLabel()` (for VoiceOver), but frequently lack both. Both are necessary because they serve different user interaction models—`.help` for visual hover feedback and `.accessibilityLabel` for screen readers.
**Action:** When adding or reviewing icon-only buttons in SwiftUI, always ensure symmetry by defining both `.help("Description")` and `.accessibilityLabel("Description")` to cover all accessibility and usability vectors.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
ControllerKeys - Source Available License

Copyright (c) 2024-2025 Kevin Tang
Copyright (c) 2025-2026 Kevin Tang

TERMS AND CONDITIONS

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ struct CommandWheelAction: Codable, Identifiable, Equatable, ExecutableAction {
return NSWorkspace.shared.icon(forFile: url.path)
}
case .openLink(let url):
if let data = FaviconCache.shared.loadCachedFavicon(for: url) {
if let data = FaviconCache.shared.memoryCachedFavicon(for: url) {
return NSImage(data: data)
}
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,88 +448,92 @@ enum ControllerButton: String, Codable, CaseIterable, Identifiable, Sendable {

/// SF Symbol name appropriate for the controller type
func systemImageName(forDualSense isDualSense: Bool) -> String? {
if isDualSense {
switch self {
case .dpadUp: return "arrowtriangle.up.fill"
case .dpadDown: return "arrowtriangle.down.fill"
case .dpadLeft: return "arrowtriangle.left.fill"
case .dpadRight: return "arrowtriangle.right.fill"
case .leftStickUp, .rightStickUp: return "arrow.up.circle"
case .leftStickDown, .rightStickDown: return "arrow.down.circle"
case .leftStickLeft, .rightStickLeft: return "arrow.left.circle"
case .leftStickRight, .rightStickRight: return "arrow.right.circle"
case .leftStickUpLeft, .rightStickUpLeft: return "arrow.up.left.circle"
case .leftStickUpRight, .rightStickUpRight: return "arrow.up.right.circle"
case .leftStickDownLeft, .rightStickDownLeft: return "arrow.down.left.circle"
case .leftStickDownRight, .rightStickDownRight: return "arrow.down.right.circle"
case .menu: return "line.3.horizontal"
case .view: return "square.and.arrow.up" // Create/Share button (upload icon)
case .share: return "square.and.arrow.up"
case .xbox: return "playstation.logo" // PS button
case .leftThumbstick: return "l3.button.angledbottom.horizontal.left"
case .rightThumbstick: return "r3.button.angledbottom.horizontal.right"
case .touchpadButton: return "hand.point.up.left"
case .touchpadTwoFingerButton: return nil // Use text label "2P"
case .touchpadTap: return "hand.tap"
case .touchpadTwoFingerTap: return "hand.tap"
case .micMute: return "mic.slash"
case .leftTouchpadButton, .rightTouchpadButton: return "hand.point.up.left"
case .leftTouchpadTap, .rightTouchpadTap: return "hand.tap"
case .touchpadRegionTopLeftClick, .touchpadRegionTopLeftTouch,
.touchpadRegionTopRightClick, .touchpadRegionTopRightTouch,
.touchpadRegionBottomLeftClick, .touchpadRegionBottomLeftTouch,
.touchpadRegionBottomRightClick, .touchpadRegionBottomRightTouch:
// Custom 2×2 quadrant indicator drawn by ButtonIconView; no
// SF Symbol used for these.
return nil
case .leftPaddle: return "l.button.roundedbottom.horizontal"
case .rightPaddle: return "r.button.roundedbottom.horizontal"
case .leftFunction: return "button.horizontal.top.press"
case .rightFunction: return "button.horizontal.top.press"
// Face buttons use text symbols for DualSense, not SF Symbols
case .a, .b, .x, .y: return nil
default: return nil
}
} else {
switch self {
case .dpadUp: return "arrowtriangle.up.fill"
case .dpadDown: return "arrowtriangle.down.fill"
case .dpadLeft: return "arrowtriangle.left.fill"
case .dpadRight: return "arrowtriangle.right.fill"
case .leftStickUp, .rightStickUp: return "arrow.up.circle"
case .leftStickDown, .rightStickDown: return "arrow.down.circle"
case .leftStickLeft, .rightStickLeft: return "arrow.left.circle"
case .leftStickRight, .rightStickRight: return "arrow.right.circle"
case .leftStickUpLeft, .rightStickUpLeft: return "arrow.up.left.circle"
case .leftStickUpRight, .rightStickUpRight: return "arrow.up.right.circle"
case .leftStickDownLeft, .rightStickDownLeft: return "arrow.down.left.circle"
case .leftStickDownRight, .rightStickDownRight: return "arrow.down.right.circle"
case .menu: return "line.3.horizontal"
case .view: return "rectangle.on.rectangle"
case .share: return "square.and.arrow.up"
case .xbox: return "xbox.logo"
case .siri: return "mic.fill"
case .appleTVRemotePower: return "power"
case .appleTVRemoteVolumeUp: return "speaker.wave.3.fill"
case .appleTVRemoteVolumeDown: return "speaker.wave.1.fill"
case .appleTVRemoteMute: return "speaker.slash.fill"
case .leftThumbstick: return "l.circle"
case .rightThumbstick: return "r.circle"
case .a: return "a.circle"
case .b: return "b.circle"
case .x: return "x.circle"
case .y: return "y.circle"
case .touchpadButton: return "hand.point.up.left"
case .touchpadTap: return "hand.tap"
case .leftTouchpadButton, .rightTouchpadButton: return "hand.point.up.left"
case .leftTouchpadTap, .rightTouchpadTap: return "hand.tap"
case .micMute: return "mic.slash"
case .xboxPaddle1: return "l.button.roundedbottom.horizontal"
case .xboxPaddle2: return "r.button.roundedbottom.horizontal"
case .xboxPaddle3: return "l.button.roundedbottom.horizontal.fill"
case .xboxPaddle4: return "r.button.roundedbottom.horizontal.fill"
default: return nil
}
return isDualSense ? dualSenseSystemImageName : xboxSystemImageName
}

private var dualSenseSystemImageName: String? {
switch self {
case .dpadUp: return "arrowtriangle.up.fill"
case .dpadDown: return "arrowtriangle.down.fill"
case .dpadLeft: return "arrowtriangle.left.fill"
case .dpadRight: return "arrowtriangle.right.fill"
case .leftStickUp, .rightStickUp: return "arrow.up.circle"
case .leftStickDown, .rightStickDown: return "arrow.down.circle"
case .leftStickLeft, .rightStickLeft: return "arrow.left.circle"
case .leftStickRight, .rightStickRight: return "arrow.right.circle"
case .leftStickUpLeft, .rightStickUpLeft: return "arrow.up.left.circle"
case .leftStickUpRight, .rightStickUpRight: return "arrow.up.right.circle"
case .leftStickDownLeft, .rightStickDownLeft: return "arrow.down.left.circle"
case .leftStickDownRight, .rightStickDownRight: return "arrow.down.right.circle"
case .menu: return "line.3.horizontal"
case .view: return "square.and.arrow.up" // Create/Share button (upload icon)
case .share: return "square.and.arrow.up"
case .xbox: return "playstation.logo" // PS button
case .leftThumbstick: return "l3.button.angledbottom.horizontal.left"
case .rightThumbstick: return "r3.button.angledbottom.horizontal.right"
case .touchpadButton: return "hand.point.up.left"
case .touchpadTwoFingerButton: return nil // Use text label "2P"
case .touchpadTap: return "hand.tap"
case .touchpadTwoFingerTap: return "hand.tap"
case .micMute: return "mic.slash"
case .leftTouchpadButton, .rightTouchpadButton: return "hand.point.up.left"
case .leftTouchpadTap, .rightTouchpadTap: return "hand.tap"
case .touchpadRegionTopLeftClick, .touchpadRegionTopLeftTouch,
.touchpadRegionTopRightClick, .touchpadRegionTopRightTouch,
.touchpadRegionBottomLeftClick, .touchpadRegionBottomLeftTouch,
.touchpadRegionBottomRightClick, .touchpadRegionBottomRightTouch:
// Custom 2×2 quadrant indicator drawn by ButtonIconView; no
// SF Symbol used for these.
return nil
case .leftPaddle: return "l.button.roundedbottom.horizontal"
case .rightPaddle: return "r.button.roundedbottom.horizontal"
case .leftFunction: return "button.horizontal.top.press"
case .rightFunction: return "button.horizontal.top.press"
// Face buttons use text symbols for DualSense, not SF Symbols
case .a, .b, .x, .y: return nil
default: return nil
}
}

private var xboxSystemImageName: String? {
switch self {
case .dpadUp: return "arrowtriangle.up.fill"
case .dpadDown: return "arrowtriangle.down.fill"
case .dpadLeft: return "arrowtriangle.left.fill"
case .dpadRight: return "arrowtriangle.right.fill"
case .leftStickUp, .rightStickUp: return "arrow.up.circle"
case .leftStickDown, .rightStickDown: return "arrow.down.circle"
case .leftStickLeft, .rightStickLeft: return "arrow.left.circle"
case .leftStickRight, .rightStickRight: return "arrow.right.circle"
case .leftStickUpLeft, .rightStickUpLeft: return "arrow.up.left.circle"
case .leftStickUpRight, .rightStickUpRight: return "arrow.up.right.circle"
case .leftStickDownLeft, .rightStickDownLeft: return "arrow.down.left.circle"
case .leftStickDownRight, .rightStickDownRight: return "arrow.down.right.circle"
case .menu: return "line.3.horizontal"
case .view: return "rectangle.on.rectangle"
case .share: return "square.and.arrow.up"
case .xbox: return "xbox.logo"
case .siri: return "mic.fill"
case .appleTVRemotePower: return "power"
case .appleTVRemoteVolumeUp: return "speaker.wave.3.fill"
case .appleTVRemoteVolumeDown: return "speaker.wave.1.fill"
case .appleTVRemoteMute: return "speaker.slash.fill"
case .leftThumbstick: return "l.circle"
case .rightThumbstick: return "r.circle"
case .a: return "a.circle"
case .b: return "b.circle"
case .x: return "x.circle"
case .y: return "y.circle"
case .touchpadButton: return "hand.point.up.left"
case .touchpadTap: return "hand.tap"
case .leftTouchpadButton, .rightTouchpadButton: return "hand.point.up.left"
case .leftTouchpadTap, .rightTouchpadTap: return "hand.tap"
case .micMute: return "mic.slash"
case .xboxPaddle1: return "l.button.roundedbottom.horizontal"
case .xboxPaddle2: return "r.button.roundedbottom.horizontal"
case .xboxPaddle3: return "l.button.roundedbottom.horizontal.fill"
case .xboxPaddle4: return "r.button.roundedbottom.horizontal.fill"
default: return nil
}
}

Expand Down
Loading