native macOS codings agent orchestrator
5
fork

Configure Feed

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

fix(repo-appearance): clean up color-swatch selection ring sizing

The selected swatch's ring was drawn via a `.padding(2)` inside the
swatch's own frame, so half the stroke landed on top of the colored
fill — visually the ring looked clipped and "fused" to the dot
edge instead of giving the swatch a clean macOS-style halo.

Restructure each swatch into a fixed 28pt slot with a ZStack:

- Color dot: 20pt (slightly tighter than the previous 22pt to leave
room for the ring outside)
- Selection ring: 26pt circle stroked at 1.5pt — drawn outside the
dot inside the slot so a 3pt transparent gap separates dot edge
from ring inner edge, matching macOS native color pickers
- Slot: 28pt fixed width whether or not selected, so changing the
selection doesn't reflow the row of swatches

`noColorSwatch` and the colored swatches share the new
`swatchSlot(isSelected:content:)` helper so the ring chrome stays
consistent across all 11 swatches.

+58 -28
+58 -28
supacode/Features/RepositorySettings/Views/RepositoryAppearancePickerView.swift
··· 22 22 @State private var isHoveringIconTile = false 23 23 24 24 private let previewSize: CGFloat = 40 25 - private let dotSize: CGFloat = 22 25 + /// Diameter of the colored swatch itself. 26 + private let swatchDotSize: CGFloat = 20 27 + /// Diameter of the "selected" ring drawn around the swatch. The 28 + /// (ring − dot) / 2 gap (3pt) lives between the dot's edge and the 29 + /// ring's inside, matching macOS-native color pickers. 30 + private let swatchRingSize: CGFloat = 26 31 + /// Outer slot every swatch occupies in the HStack so the row layout 32 + /// stays stable whether or not a swatch is selected (without a fixed 33 + /// slot the selection ring would expand the cell and shove its 34 + /// neighbors aside on hover/select). 35 + private let swatchSlotSize: CGFloat = 28 36 + private let swatchRingLineWidth: CGFloat = 1.5 26 37 27 38 var body: some View { 28 39 VStack(alignment: .leading, spacing: 12) { ··· 229 240 Button { 230 241 store.send(.setAppearanceColor(choice)) 231 242 } label: { 232 - Circle() 233 - .fill(choice.color) 234 - .frame(width: dotSize, height: dotSize) 235 - .overlay { 236 - Circle() 237 - .stroke(Color.primary, lineWidth: isSelected ? 2 : 0) 238 - .padding(2) 239 - } 240 - .help(choice.displayName) 241 - .accessibilityLabel(choice.displayName) 242 - .accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : .isButton) 243 + swatchSlot(isSelected: isSelected) { 244 + Circle() 245 + .fill(choice.color) 246 + .frame(width: swatchDotSize, height: swatchDotSize) 247 + } 248 + .help(choice.displayName) 249 + .accessibilityLabel(choice.displayName) 250 + .accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : .isButton) 243 251 } 244 252 .buttonStyle(.plain) 245 253 } ··· 250 258 Button { 251 259 store.send(.setAppearanceColor(nil)) 252 260 } label: { 253 - Circle() 254 - .stroke(Color.secondary.opacity(0.5), style: StrokeStyle(lineWidth: 1, dash: [2, 2])) 255 - .frame(width: dotSize, height: dotSize) 256 - .overlay { 257 - Image(systemName: "slash.circle") 258 - .font(.system(size: 12)) 259 - .foregroundStyle(.secondary) 260 - } 261 - .overlay { 262 - Circle() 263 - .stroke(Color.primary, lineWidth: isSelected ? 2 : 0) 264 - .padding(2) 265 - } 266 - .help("No color") 267 - .accessibilityLabel("No color") 268 - .accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : .isButton) 261 + swatchSlot(isSelected: isSelected) { 262 + Circle() 263 + .stroke( 264 + Color.secondary.opacity(0.5), 265 + style: StrokeStyle(lineWidth: 1, dash: [2, 2]) 266 + ) 267 + .frame(width: swatchDotSize, height: swatchDotSize) 268 + .overlay { 269 + Image(systemName: "slash.circle") 270 + .font(.system(size: 11)) 271 + .foregroundStyle(.secondary) 272 + .accessibilityHidden(true) 273 + } 274 + } 275 + .help("No color") 276 + .accessibilityLabel("No color") 277 + .accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : .isButton) 269 278 } 270 279 .buttonStyle(.plain) 280 + } 281 + 282 + /// Centers the swatch content in a fixed-size slot and overlays a 283 + /// selection ring on top, sized larger than the swatch so a 3pt 284 + /// transparent gap separates the swatch's edge from the ring's 285 + /// inside — mirroring macOS-native color picker selection chrome. 286 + /// The slot bounds stay constant whether selected or not so swatch 287 + /// row layout doesn't reflow on selection change. 288 + @ViewBuilder 289 + private func swatchSlot<Content: View>( 290 + isSelected: Bool, @ViewBuilder content: () -> Content 291 + ) -> some View { 292 + ZStack { 293 + content() 294 + if isSelected { 295 + Circle() 296 + .stroke(Color.primary, lineWidth: swatchRingLineWidth) 297 + .frame(width: swatchRingSize, height: swatchRingSize) 298 + } 299 + } 300 + .frame(width: swatchSlotSize, height: swatchSlotSize) 271 301 } 272 302 273 303 // MARK: - Error banner