native macOS codings agent orchestrator
6
fork

Configure Feed

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

fix(repo-appearance): unify spine header height and fold dot into icon

Spines with a repo icon were rendering 20pt taller than icon-less
spines (icon 14pt + 6pt vstack spacing), so the rotated titles drifted
out of alignment across the shelf row — the most visible artifact in
the screenshot from manual QA.

Restructure `ShelfSpineHeader` around a constant-sized 18pt slot at
the top of every spine:

- **Reserved slot**: a `ZStack` with `Color.clear.frame(18×18)` always
rendered, so the slot occupies the same space whether or not it
hosts content. Header height becomes constant
(8 top + 18 slot + 8 vstack + 160 title = 194pt) and titles
realign across spines.

- **Notification folded into icon**: when an icon is set and there's
an unseen notification, the orange dot renders as a 6pt corner
badge at the icon's top-trailing (offset 3,-3) with a thin
`black.opacity(0.25)` stroke for contrast on tinted spines —
macOS app-icon style. The badge sits inside the 18pt slot and
doesn't push layout.

- **Standalone dot retained**: when there's no icon but a
notification exists, fall back to the original standalone
`aggregatedDotSize` orange dot centered in the slot. Behavior for
appearance-less repos is unchanged.

- **More breathing room**: VStack spacing 6 → 8, top padding 0 → 8,
so the slot sits a bit lower from the spine's top edge and reads
less crowded against the header. The two extra pixels around the
icon ring (slot 18pt vs icon 14pt) give the icon and badge their
own margin.

onevcat 7d782a53 c4b3e907

+63 -11
+63 -11
supacode/Features/Shelf/Views/ShelfSpineView.swift
··· 269 269 let iconTint: Color 270 270 let repositoryRootURL: URL 271 271 272 + /// Reserved slot for the top decoration (icon and/or notification), 273 + /// sized at the maximum expected configuration (14pt icon plus a 274 + /// 6pt badge nudged 3pt outward at the top-trailing corner). 275 + /// Holding the slot at a constant size — whether or not an icon is 276 + /// set — keeps every spine's header at the same total height so the 277 + /// rotated titles align horizontally across the shelf row. When the 278 + /// repo has no icon AND no notification, the slot is just empty 279 + /// reserved space. 280 + private let slotSize: CGFloat = 18 281 + private let iconSize: CGFloat = 14 282 + private let badgeSize: CGFloat = 6 283 + private let badgeOffset: CGFloat = 3 284 + 272 285 var body: some View { 273 - VStack(spacing: 6) { 286 + VStack(spacing: 8) { 287 + slot 288 + rotatedTitle 289 + } 290 + .padding(.top, 8) 291 + } 292 + 293 + /// Three rendering paths driven by the (icon, notification) matrix: 294 + /// - icon set: render the icon, hang the notification on it as a 295 + /// small badge in the top-trailing corner (macOS app-icon style). 296 + /// - no icon, has notification: fall back to the original 297 + /// standalone orange dot, centered in the slot. 298 + /// - no icon, no notification: slot stays empty but reserved. 299 + @ViewBuilder 300 + private var slot: some View { 301 + ZStack { 302 + Color.clear 303 + .frame(width: slotSize, height: slotSize) 304 + 274 305 if let icon { 275 306 RepositoryIconImage( 276 307 icon: icon, 277 308 repositoryRootURL: repositoryRootURL, 278 309 tintColor: iconTint, 279 - size: 14 310 + size: iconSize 280 311 ) 281 - .padding(.top, 6) 312 + .overlay(alignment: .topTrailing) { 313 + if hasAggregatedNotification { 314 + notificationBadge 315 + } 316 + } 317 + } else if hasAggregatedNotification { 318 + Circle() 319 + .fill(.orange) 320 + .frame( 321 + width: ShelfMetrics.aggregatedDotSize, 322 + height: ShelfMetrics.aggregatedDotSize 323 + ) 282 324 } 283 - Circle() 284 - .fill(.orange) 285 - .frame(width: ShelfMetrics.aggregatedDotSize, height: ShelfMetrics.aggregatedDotSize) 286 - .opacity(hasAggregatedNotification ? 1 : 0) 287 - .accessibilityLabel("Unread notifications") 288 - .accessibilityHidden(!hasAggregatedNotification) 289 - .padding(.top, icon == nil ? 6 : 0) 290 - rotatedTitle 291 325 } 326 + .accessibilityElement() 327 + .accessibilityLabel(hasAggregatedNotification ? "Unread notifications" : "") 328 + .accessibilityHidden(!hasAggregatedNotification) 329 + } 330 + 331 + /// Notification dot rendered as a corner badge over the icon. The 332 + /// thin dark stroke keeps the orange visible on light spine 333 + /// backgrounds; without it the badge would disappear on 334 + /// orange-tinted repos. 335 + @ViewBuilder 336 + private var notificationBadge: some View { 337 + Circle() 338 + .fill(.orange) 339 + .frame(width: badgeSize, height: badgeSize) 340 + .overlay { 341 + Circle().stroke(Color.black.opacity(0.25), lineWidth: 0.5) 342 + } 343 + .offset(x: badgeOffset, y: -badgeOffset) 292 344 } 293 345 294 346 /// Composed title rendered vertically (top-to-bottom reading direction).