Skip to content

Showcase autocomplete + inline emoji on the final onboarding screen#459

Merged
FuJacob merged 1 commit into
mainfrom
feat/onboarding-feature-showcase
May 31, 2026
Merged

Showcase autocomplete + inline emoji on the final onboarding screen#459
FuJacob merged 1 commit into
mainfrom
feat/onboarding-feature-showcase

Conversation

@FuJacob

@FuJacob FuJacob commented May 31, 2026

Copy link
Copy Markdown
Owner

Summary

The final onboarding step ("You're all set") was a static checkmark and reassurance copy that never showed what Cotabby actually does. It now demonstrates the two flagship features with self-playing animations a new user sees right before they click "Start Using Cotabby":

  • Autocomplete: a mock field types "Thanks for the", a gray ghost continuation (" quick reply!") fades in with a "Tab" keycap, then the ghost turns solid (accepted).
  • Inline emoji: a mock field types ":smi", a candidate popup (:smile: 😄 highlighted / :smiley: 😁) appears with a "Tab" keycap, then 😄 is inserted.

Both loop continuously and respect the system Reduce Motion setting (falling back to a static accepted state). Layout is stacked under the existing headline; the .done window grows from 500x380 to 520x672 to fit the two cards.

Implementation notes

  • New Cotabby/UI/Onboarding/OnboardingFeatureShowcase.swift: a self-contained, zero-argument view with two demo cards. It is purely decorative and built from local @State + hardcoded strings only. It never touches the suggestion pipeline, SuggestionSettingsModel, EmojiPickerController, OverlayController, Accessibility, or the emoji catalog, so it cannot steal focus or affect real suggestions.
  • Each card owns a .task(id: reduceMotion)-driven loop using the repo's while !Task.isCancelled { ... try? await Task.sleep(...) } idiom (no Timer/TimelineView). .task auto-cancels on disappear, so the loops can't leak.
  • Each card has a fixed height and the emoji popup is drawn as a ZStack overlay over reserved space, so the showcase block's height is constant across the animation. That keeps preferredWindowSize stable and prevents the window from pulsing taller/shorter each loop.
  • The ghost-text "Tab" keycap and the emoji popup/keycap replicate the exact specs from OverlayController's GhostKeycap and EmojiPickerView's EmojiKeycap. Those originals are private, so they are replicated (not extracted) to avoid making a hot AppKit/suggestion-path file internal for a decorative demo. Light/dark fidelity comes from the same Color(white:) + colorScheme and semantic-color mix as the originals.
  • WelcomeView.swift: inserts OnboardingFeatureShowcase() into doneStep and bumps the .done preferredWindowSize.

Validation

xcodegen generate   # new file added to the project
xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build
# ** BUILD SUCCEEDED **  (project treats warnings as errors)

xcodebuild test -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO
# ** TEST SUCCEEDED **  566 tests, 0 failures (4 skipped)

swiftlint lint --strict --quiet Cotabby/UI/Onboarding/OnboardingFeatureShowcase.swift Cotabby/UI/WelcomeView.swift
# exit 0

Visual pass not yet done. This is an animated UI change verified by build, strict lint, and the full test suite (no onboarding test asserts on doneStep or the window size). I could not drive the wizard to the final step headlessly (it is gated behind the cotabbyOnboardingCompleted default and several click-through steps). Recommended manual check before merge: run onboarding to "You're all set" and confirm both demos animate and loop; toggle System Settings > Accessibility > Display > Reduce Motion (static accepted state, no loop); toggle Light/Dark; confirm the window does not pulse in height across loops.

Linked issues

None.

Risk / rollout notes

  • Onboarding-only, and the new view is decorative and dependency-free, so blast radius is limited to the final screen's layout. No behavior change to suggestions, settings, or any service.
  • First use of @Environment(\.accessibilityReduceMotion) in the repo; handled per-card with a static fallback.
  • Cotabby.xcodeproj was regenerated with XcodeGen to pick up the new file (no project.yml change).
  • Tuning knobs if review wants adjustments: the emoji popup's horizontal offset (x: 84) and the per-card contentHeight/.done window height are the values most likely to want a nudge after the visual pass.

Greptile Summary

This PR adds OnboardingFeatureShowcase, a new decorative SwiftUI view placed on the final onboarding "You're all set" screen that self-plays looping animations demonstrating Cotabby's autocomplete ghost-text and inline emoji-picker features. The .done window height is bumped from 380 to 672 to accommodate the new cards.

  • New file OnboardingFeatureShowcase.swift (343 lines): two independent demo cards (GhostTextDemoCard, EmojiPickerDemoCard), each using @State + a .task-driven while !Task.isCancelled loop; accessibilityReduceMotion is respected with a static fallback; popup and keycap widgets replicate their private originals without touching the real suggestion pipeline.
  • WelcomeView.swift: inserts OnboardingFeatureShowcase() into doneStep and adjusts preferredWindowSize for .done.
  • project.pbxproj: new file reference and build file entry added via XcodeGen.

Confidence Score: 4/5

Safe to merge — change is confined to the final onboarding screen and is purely decorative with no connection to the suggestion pipeline or any shared state.

Both cards are well-structured: loops use the repo's established cancellation idiom, reduceMotion is respected, and the fixed contentHeight prevents window resizing during animation. The two findings are cosmetic: decorative content that skips accessibilityHidden(true) will surface meaningless animating text to VoiceOver users, and GhostTextDemoCard snaps its typed text to blank between loops rather than fading, inconsistent with the emoji card. Neither affects real suggestions, settings, or any other screen.

OnboardingFeatureShowcase.swift — accessibility opt-out and the loop-reset animation inconsistency between the two demo cards.

Important Files Changed

Filename Overview
Cotabby/UI/Onboarding/OnboardingFeatureShowcase.swift New 343-line decorative view with two self-animating demo cards; loops are well-structured with cancellation guards; minor issues: accessibility tree not opted out, and typedCount resets without animation in GhostTextDemoCard causing a visual snap between loops.
Cotabby/UI/WelcomeView.swift Minimal two-line change: inserts OnboardingFeatureShowcase() into doneStep and bumps the .done window size from 500x380 to 520x672; no logic changes.
Cotabby.xcodeproj/project.pbxproj XcodeGen-regenerated entries for OnboardingFeatureShowcase.swift; new PBXFileReference, PBXBuildFile, and PBXGroup for the Onboarding subfolder — all look correct.

Sequence Diagram

sequenceDiagram
    participant SW as SwiftUI .task
    participant GC as GhostTextDemoCard
    participant EC as EmojiPickerDemoCard
    participant WV as WelcomeView (doneStep)

    WV->>GC: init (OnboardingFeatureShowcase inserted)
    WV->>EC: init

    GC->>SW: ".task(id: reduceMotion) -> runLoop()"
    EC->>SW: ".task(id: reduceMotion) -> runLoop() + 700ms offset"

    loop every ~4s (GhostText)
        SW->>GC: typedCount 0 to 14 (55ms/char)
        SW->>GC: "showGhost = true (fade in ghost + Tab keycap)"
        SW->>GC: "accepted = true (ghost turns solid)"
        SW->>GC: "showGhost/accepted = false (fade out)"
    end

    loop every ~4.4s (EmojiPicker)
        SW->>EC: triggerCount 0 to 4 (70ms/char)
        SW->>EC: "showPopup = true (popup + Tab keycap)"
        SW->>EC: "showPopup=false, committed=true (emoji inserted)"
        SW->>EC: "committed=false, triggerCount=0 (fade out)"
    end

    Note over GC,EC: reduceMotion=true -> static accepted state, no loop
    Note over GC,EC: view disappears -> .task auto-cancelled
Loading

Fix All in Codex Fix All in Claude Code

Reviews (1): Last reviewed commit: "Showcase autocomplete + inline emoji on ..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

The "You're all set" step now demonstrates Cotabby's two flagship features with
self-playing, hardcoded animations: inline autocomplete ghost text (type, gray
ghost suggestion with a Tab hint, accept) and the inline :emoji: picker (type
:smi, candidate popup, insert 😄). Both loop continuously and fall back to a
static accepted state when Reduce Motion is on.

The demos are purely decorative: a new OnboardingFeatureShowcase view built from
local @State and hardcoded strings. It never touches the suggestion pipeline,
settings, Accessibility, or the emoji catalog. Each card owns a .task-driven
loop (auto-cancelled on disappear) and a fixed height so the onboarding window
never resizes mid-animation. The .done window grows to 520x672 to fit the cards.
Comment on lines +17 to +24
struct OnboardingFeatureShowcase: View {
var body: some View {
VStack(spacing: 12) {
GhostTextDemoCard()
EmojiPickerDemoCard()
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The showcase is described as "purely decorative" in both the PR description and the file doc-comment, but it has no accessibility opt-out. VoiceOver will traverse the two cards, reading out animating text fragments ("T", "Th", "Tha"…) and emoji candidate rows that have no actionable meaning for an assistive-technology user. Adding .accessibilityHidden(true) to OnboardingFeatureShowcase suppresses the entire subtree from the accessibility tree in one place.

Suggested change
struct OnboardingFeatureShowcase: View {
var body: some View {
VStack(spacing: 12) {
GhostTextDemoCard()
EmojiPickerDemoCard()
}
}
}
struct OnboardingFeatureShowcase: View {
var body: some View {
VStack(spacing: 12) {
GhostTextDemoCard()
EmojiPickerDemoCard()
}
.accessibilityHidden(true)
}
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

Comment on lines +136 to +140
withAnimation(.easeInOut(duration: 0.30)) {
showGhost = false
accepted = false
}
try? await Task.sleep(nanoseconds: 600 * nsPerMillisecond)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 typedCount resets without animation between loops

At the top of each loop iteration typedCount is set to 0 instantly, so "Thanks for the" snaps to blank before the 350 ms pause. EmojiPickerDemoCard avoids this by resetting triggerCount = 0 inside its closing withAnimation(.easeInOut(duration: 0.30)) block. Adding typedCount = 0 to the GhostTextDemoCard equivalent block (lines 136–139) would make both cards fade out consistently.

Fix in Codex Fix in Claude Code

Divider()

VStack(spacing: 0) {
ForEach(Array(candidates.enumerated()), id: \.offset) { index, candidate in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Using id: \.offset (array index) as the ForEach identity is a SwiftUI antipattern: if the array were ever reordered, SwiftUI would animate the wrong rows. Since candidates has a stable alias string per item, that makes a better stable identifier.

Suggested change
ForEach(Array(candidates.enumerated()), id: \.offset) { index, candidate in
ForEach(Array(candidates.enumerated()), id: \.element.alias) { index, candidate in

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

@FuJacob FuJacob merged commit 5bd0cb8 into main May 31, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant