Skip to content

Redesign emoji + suggestion popups as a shared minimal dark HUD#753

Open
FuJacob wants to merge 1 commit into
mainfrom
emoji-picker-minimal
Open

Redesign emoji + suggestion popups as a shared minimal dark HUD#753
FuJacob wants to merge 1 commit into
mainfrom
emoji-picker-minimal

Conversation

@FuJacob

@FuJacob FuJacob commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Makes the two floating popups feel like ephemeral system overlays instead of bright cards competing with the document. The :emoji: picker and the suggestion popup card (mirror mode) each carried their own .regularMaterial styling and had drifted apart; both now route through one shared, committed-dark HUD (PopupChrome) that reads the same dark over a white host as over a dark one. The emoji picker is also restructured from a tall vertical list into a compact two-row layout: the live :query on top, a horizontal ribbon of ranked glyphs below, moved with the arrow keys.

Validation

xcodebuild ... build           -> ** BUILD SUCCEEDED **
xcodebuild ... build-for-testing -> ** TEST BUILD SUCCEEDED **
xcodebuild ... test -only-testing:CotabbyTests/EmojiPickerPanelLayoutTests \
  -only-testing:CotabbyTests/EmojiPickerControllerTests \
  -only-testing:CotabbyTests/MirrorOverlayLayoutTests CODE_SIGNING_ALLOWED=NO
  -> ** TEST SUCCEEDED **  (incl. 3 new arrow-key nav tests)
swiftlint lint --quiet <changed files> -> exit 0

Rendered the redesigned popups over both a light and a dark host with an ImageRenderer harness (since removed): the committed-dark chrome, hairline, and light text are identical across host appearances, and the ribbon's soft selection chip lands on the highlighted glyph. (ImageRenderer does not lay out ScrollView subviews, so the real ribbon row snapshots blank; verified the cells via a non-scrolling proxy. The live NSHostingView panel scrolls normally.)

Linked issues

None.

Risk / rollout notes

  • New file Cotabby/UI/PopupChrome.swift -> ran xcodegen generate; the project.pbxproj change is only the added file reference.
  • Emoji nav behavior change: Left/Right arrows now move the ribbon selection (previously they dismissed the picker). Up/Down still navigate. With no matches, all arrows still pass through so the host caret moves and the capture closes. Covered by new controller tests.
  • Committed dark: the suggestion popup card and the macro preview are now dark in every host appearance (previously adaptive light/dark). The mirror card is also ~4pt slimmer (vertical padding 6 -> 4); MirrorOverlayLayout and its geometry tests were updated in lockstep.
  • Onboarding's emoji replica (DemoEmojiPopup) was updated to the new ribbon so onboarding doesn't show a stale design.
  • No settings, schema, or persistence migrations.

Greptile Summary

This PR unifies three previously independent floating popups (emoji picker, mirror suggestion card, macro inline preview) under a single committed-dark PopupChrome module, and simultaneously redesigns the emoji picker from a vertical list into a compact horizontal ribbon navigable with all four arrow keys.

  • PopupChrome.swift (new): introduces PopupTheme color tokens and PopupHUDChrome — a deterministic charcoal-gradient backdrop with hairline border, forced .dark color scheme, and shared corner radius — consumed by EmojiPickerView, OverlayController, and InlinePreviewView via .popupHUDChrome().
  • Emoji picker redesign: EmojiPickerMetrics switches from fixed-width list constants to ribbon geometry; EmojiPickerView becomes a two-row layout (:query header + horizontal ScrollView of EmojiRibbonCells); Left/Right arrows now navigate the selection instead of dismissing, covered by three new controller tests.
  • MirrorOverlayLayout vertical padding trimmed from 6 → 4 pt in lockstep with the view and all geometry tests; onboarding DemoEmojiPopup updated to match the live ribbon design.

Confidence Score: 4/5

Safe to merge — all behavior changes have test coverage and the geometry is validated end-to-end.

The migration is thorough: shared chrome is consistent across all three surfaces, metrics and layout tests were updated in lockstep, and the arrow-key behavior change has three new targeted tests. The mirror overlay card's drop shadow did not carry over to PopupHUDChrome and is not re-applied at the call site — on dark-colored host windows the card may be harder to distinguish. The onboarding demo cell also hardcodes its dimensions instead of reading from EmojiPickerMetrics, which could silently drift if the metric changes.

Cotabby/Services/UI/OverlayController.swift (missing shadow on mirror card) and Cotabby/UI/Onboarding/OnboardingFeatureShowcase.swift (hardcoded cell dimensions in DemoEmojiRibbonCell).

Important Files Changed

Filename Overview
Cotabby/UI/PopupChrome.swift New shared ViewModifier and theme namespace — well-structured, deterministic dark gradient, forced .dark colorScheme applied correctly as the outermost modifier so all descendants resolve light palette tokens.
Cotabby/Services/UI/OverlayController.swift Mirror overlay card migrated to PopupHUDChrome; color branches removed cleanly. The old background had a subtle drop shadow that is not carried forward by PopupHUDChrome and is not re-added here, leaving the card without visual lift on dark hosts.
Cotabby/UI/EmojiPickerView.swift Redesigned from vertical LazyVStack list to horizontal ScrollView ribbon; EmojiRibbonCell uses EmojiPickerMetrics.cellSize correctly. acceptKeyLabel remains @published but is now unused by the view.
Cotabby/App/Coordinators/EmojiPickerController.swift Left/Right arrows now map to .navigate(.up/.down) to drive the ribbon; Forward-Delete still dismisses. Three new controller tests cover the changed behavior, including the no-matches pass-through path.
Cotabby/Support/EmojiPickerPanelLayout.swift Metrics reworked from fixed-width list constants to ribbon geometry; contentSize formula is correct and tested. File-level doc comment block has a minor duplication of old and new descriptions left over from the edit.
Cotabby/UI/Onboarding/OnboardingFeatureShowcase.swift DemoEmojiPopup updated to ribbon layout; DemoEmojiRibbonCell hardcodes width/height 30 instead of EmojiPickerMetrics.cellSize, risking silent drift if the metric changes.
CotabbyTests/EmojiPickerControllerTests.swift Three new tests cover Right-navigates-when-matches, Left-navigates-when-matches, and arrow-passes-through-with-no-matches; all well-structured and exercise the key behavior change directly.
CotabbyTests/EmojiPickerPanelLayoutTests.swift Layout tests updated to new ribbon metrics; right-edge clamping test uses a caret at x=950 instead of 850 to properly overflow the compact ribbon — correct.
CotabbyTests/MirrorOverlayLayoutTests.swift Panel height expectations updated from 33 to 29 (reflecting 4pt vertical padding × 2 = 8 instead of 12) consistently across all affected test cases.
Cotabby/Support/MirrorOverlayLayout.swift verticalPadding reduced from 6 to 4 in lockstep with the view change; geometry tests updated consistently.
Cotabby/UI/InlinePreviewView.swift regularMaterial replaced with popupHUDChrome; height trimmed from 30 to 28; color tokens updated throughout. Straightforward and self-consistent.
Cotabby/Services/UI/EmojiPickerPanelController.swift Initial panel content rect now uses EmojiPickerMetrics.contentSize(matchCount: 0) — consistent with the new metrics API.
Cotabby.xcodeproj/project.pbxproj PopupChrome.swift added to both the main app target and the test target; file reference and group entry look correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph Before
        A1[EmojiPickerView
.regularMaterial]
        A2[MirrorOverlayView
adaptive backdrop + border]
        A3[InlinePreviewView
.regularMaterial]
    end
    subgraph After
        B1[PopupTheme
colors / gradient / hairline]
        B2[PopupHUDChrome
ViewModifier]
        B1 --> B2
        B2 --> C1[EmojiPickerView
.popupHUDChrome]
        B2 --> C2[MirrorOverlayView
.popupHUDChrome]
        B2 --> C3[InlinePreviewView
.popupHUDChrome]
        B2 --> C4[DemoEmojiPopup
onboarding replica]
    end
    subgraph EmojiPicker Layout Change
        D1[Vertical LazyVStack
fixed 300pt wide] -->|replaced by| D2[Horizontal ScrollView ribbon
cell-width-adaptive]
        D3[Up / Down arrows only] -->|replaced by| D4[Up+Left / Down+Right navigate
no matches: pass through]
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    subgraph Before
        A1[EmojiPickerView
.regularMaterial]
        A2[MirrorOverlayView
adaptive backdrop + border]
        A3[InlinePreviewView
.regularMaterial]
    end
    subgraph After
        B1[PopupTheme
colors / gradient / hairline]
        B2[PopupHUDChrome
ViewModifier]
        B1 --> B2
        B2 --> C1[EmojiPickerView
.popupHUDChrome]
        B2 --> C2[MirrorOverlayView
.popupHUDChrome]
        B2 --> C3[InlinePreviewView
.popupHUDChrome]
        B2 --> C4[DemoEmojiPopup
onboarding replica]
    end
    subgraph EmojiPicker Layout Change
        D1[Vertical LazyVStack
fixed 300pt wide] -->|replaced by| D2[Horizontal ScrollView ribbon
cell-width-adaptive]
        D3[Up / Down arrows only] -->|replaced by| D4[Up+Left / Down+Right navigate
no matches: pass through]
    end
Loading

Comments Outside Diff (1)

  1. Cotabby/Support/EmojiPickerPanelLayout.swift, line 1-12 (link)

    P2 Doubled file-level doc comment after the merge

    The old "Pure geometry for the emoji picker panel: how big it is…" paragraph and the new "Pure geometry for the two-row emoji picker…" paragraph were both left as a single contiguous doc comment block before the EmojiPickerMetrics enum. Only one should be the file overview; the other is now redundant or misplaced as an enum-level doc comment.

    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

Fix All in Codex Fix All in Claude Code

Reviews (1): Last reviewed commit: "Redesign emoji + suggestion popups as a ..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

The :emoji: picker and the suggestion "popup" card each carried their own
.regularMaterial styling and had drifted apart. Route both (plus the macro
preview) through one committed-dark HUD (PopupChrome) so they read as ephemeral
overlays instead of bright cards, identical dark over light and dark hosts.

The emoji picker becomes a compact two-row layout: the live :query on top, a
horizontal ribbon of ranked glyphs below, moved with the arrow keys (Left/Right
now navigate the ribbon; previously they dismissed it). The suggestion card is
darker and a touch slimmer, and onboarding's emoji replica is updated to match.
// forced-dark scheme inside `popupHUDChrome` is what flips `styledSuggestion` and the keycap
// to their light variants, so the popup reads the same dark over a white host as a dark one.
.frame(maxWidth: .infinity, maxHeight: .infinity)
.popupHUDChrome()

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 Mirror card lost its drop shadow

The old background modifier included .shadow(color: .black.opacity(0.12), radius: 6, x: 0, y: 2). PopupHUDChrome has no shadow, and none is re-added after .popupHUDChrome() here. On a dark host (terminal, dark Xcode) the hairline (0.12 white opacity) alone may not separate the charcoal card from dark content, making the card edge hard to see. DemoEmojiPopup in onboarding correctly adds .shadow(color: .black.opacity(0.28), radius: 8, y: 4) after .popupHUDChrome() — a similar call here would restore the visual separation without touching the shared chrome.

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

.padding(.vertical, 2)
Text(glyph)
.font(.system(size: 20))
.frame(width: 30, height: 30)

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 DemoEmojiRibbonCell hardcodes width: 30, height: 30 instead of using EmojiPickerMetrics.cellSize. The real EmojiRibbonCell correctly uses the constant, so if the metric is ever adjusted the demo cell will silently diverge from the live picker.

Suggested change
.frame(width: 30, height: 30)
.frame(width: EmojiPickerMetrics.cellSize, height: EmojiPickerMetrics.cellSize)

Fix in Codex Fix in Claude Code

Comment on lines +18 to 20
/// The accept-word key label. Retained for the panel contract; the minimal ribbon no longer draws
/// a per-cell keycap, so it is currently unused by the view.
@Published var acceptKeyLabel: String?

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 Dead @Published state the controller still maintains

acceptKeyLabel is acknowledged as "currently unused by the view," but it remains @Published, meaning the controller presumably still writes to it on every selection change, triggering unnecessary objectWillChange emissions (and by extension view re-evaluations) for no visible effect. If this property is reserved for a future keycap hint, a plain var with a note would avoid the publishing overhead until it is actually read by the view.

Fix in Codex Fix in Claude Code

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