Fix reading-safety and accessibility majors from review

- Transcript: drag suspends auto-follow; long-press selection suspends
  following; Latest is a trailing outlined capsule that re-enables follow
- Lightwell: pulse via isolated TimelineView (2.4s triangle wave), pauses
  when offscreen; Reduce Motion uses static geometry (no level resizing)
- Increase Contrast: solid Rosewood lightwell fill, 2pt Rosewood selected
  outlines on mode buttons
- Haptics: success only on entering ready, error only on entering failure
This commit is contained in:
snowspeeder committed 2026-09-12 08:27:48 -04:00
1 parent f043d6b078
commit afe37ad5f4
3 files changed
+121 -55

No files matched your search

@@ -7,9 +7,15 @@ struct TranscriptListView: View {
var onClear: () -> Void
@State private var confirmClear = false
@State private var followingLatest = true
@State private var selectionActive = false
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.dynamicTypeSize) private var typeSize
/// Auto-follow eligibility: never scroll while the user is selecting
/// text or reading history.
private var isFollowingEligible: Bool {
followingLatest && !selectionActive
}
private static let deliveryMarkers = #"\[(?:appreciative|calm|concerned|curious|encouraging|excited|happy|joking|quiet|reassuring|reflective|sad|serious|surprised|sympathetic|urgent|whispering|laughter|sighing|breathing|neutral)\]\s*"#
var body: some View {
@@ -39,8 +45,6 @@ struct TranscriptListView: View {
}
ForEach(events) { event in transcriptRow(event) }
Color.clear.frame(height: 1).id("latest")
.onAppear { followingLatest = true }
.onDisappear { followingLatest = false }
}
.padding(.horizontal, Nearlight.s20)
.padding(.top, Nearlight.s16)
@@ -49,23 +53,43 @@ struct TranscriptListView: View {
}
.scrollDismissesKeyboard(.interactively)
.defaultScrollAnchor(.top)
.simultaneousGesture(DragGesture().onChanged { _ in
// Any manual drag suspends automatic following.
followingLatest = false
})
.onChange(of: events.last?.text) { _, _ in
if followingLatest { proxy.scrollTo("latest", anchor: .bottom) }
guard isFollowingEligible else { return }
proxy.scrollTo("latest", anchor: .bottom)
}
.onChange(of: events.count) { _, _ in
if followingLatest { proxy.scrollTo("latest", anchor: .bottom) }
guard isFollowingEligible else { return }
proxy.scrollTo("latest", anchor: .bottom)
}
.onChange(of: selectionActive) { _, active in
// Beginning text selection suspends auto-follow so the
// user's reading position is never disturbed.
if active { followingLatest = false }
}
if !followingLatest && !events.isEmpty {
Button {
withAnimation(reduceMotion ? nil : Nearlight.standard) {
proxy.scrollTo("latest", anchor: .bottom)
HStack {
Spacer()
Button {
followingLatest = true
withAnimation(reduceMotion ? nil : Nearlight.standard) {
proxy.scrollTo("latest", anchor: .bottom)
}
} label: {
Label("Latest", systemImage: "arrow.down")
.font(NearlightFont.support(typeSize))
.padding(.horizontal, Nearlight.s12)
.frame(minHeight: 44)
.background(Nearlight.surface, in: Capsule())
.overlay(Capsule().strokeBorder(Nearlight.quiet, lineWidth: 1))
}
} label: {
Label("Latest messages", systemImage: "arrow.down")
.font(NearlightFont.support(typeSize))
.frame(minHeight: 44)
.accessibilityIdentifier("talk.latest")
}
.accessibilityIdentifier("talk.latest")
.padding(.horizontal, Nearlight.s12)
.padding(.bottom, Nearlight.s12)
}
}
}
@@ -104,6 +128,11 @@ struct TranscriptListView: View {
.fixedSize(horizontal: false, vertical: true)
}
.frame(maxWidth: .infinity, alignment: .leading)
.onLongPressGesture(minimumDuration: 0.05, pressing: { pressing in
// Text-selection interaction begins with long-press on the row;
// suspend auto-follow while pressing/held.
if pressing { selectionActive = true }
}, perform: {})
.contextMenu {
Button("Copy message", systemImage: "doc.on.doc") { UIPasteboard.general.string = text }
}