Redesign emoji + suggestion popups as a shared minimal dark HUD#753
Redesign emoji + suggestion popups as a shared minimal dark HUD#753FuJacob wants to merge 1 commit into
Conversation
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() |
There was a problem hiding this comment.
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!
| .padding(.vertical, 2) | ||
| Text(glyph) | ||
| .font(.system(size: 20)) | ||
| .frame(width: 30, height: 30) |
There was a problem hiding this comment.
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.
| .frame(width: 30, height: 30) | |
| .frame(width: EmojiPickerMetrics.cellSize, height: EmojiPickerMetrics.cellSize) |
| /// 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? |
There was a problem hiding this comment.
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.
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.regularMaterialstyling 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:queryon top, a horizontal ribbon of ranked glyphs below, moved with the arrow keys.Validation
Rendered the redesigned popups over both a light and a dark host with an
ImageRendererharness (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. (ImageRendererdoes not lay outScrollViewsubviews, so the real ribbon row snapshots blank; verified the cells via a non-scrolling proxy. The liveNSHostingViewpanel scrolls normally.)Linked issues
None.
Risk / rollout notes
Cotabby/UI/PopupChrome.swift-> ranxcodegen generate; theproject.pbxprojchange is only the added file reference.MirrorOverlayLayoutand its geometry tests were updated in lockstep.DemoEmojiPopup) was updated to the new ribbon so onboarding doesn't show a stale design.Greptile Summary
This PR unifies three previously independent floating popups (emoji picker, mirror suggestion card, macro inline preview) under a single committed-dark
PopupChromemodule, and simultaneously redesigns the emoji picker from a vertical list into a compact horizontal ribbon navigable with all four arrow keys.PopupChrome.swift(new): introducesPopupThemecolor tokens andPopupHUDChrome— a deterministic charcoal-gradient backdrop with hairline border, forced.darkcolor scheme, and shared corner radius — consumed byEmojiPickerView,OverlayController, andInlinePreviewViewvia.popupHUDChrome().EmojiPickerMetricsswitches from fixed-width list constants to ribbon geometry;EmojiPickerViewbecomes a two-row layout (:queryheader + horizontalScrollViewofEmojiRibbonCells); Left/Right arrows now navigate the selection instead of dismissing, covered by three new controller tests.MirrorOverlayLayoutvertical padding trimmed from 6 → 4 pt in lockstep with the view and all geometry tests; onboardingDemoEmojiPopupupdated 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
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%%{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] endComments Outside Diff (1)
Cotabby/Support/EmojiPickerPanelLayout.swift, line 1-12 (link)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
EmojiPickerMetricsenum. 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!
Reviews (1): Last reviewed commit: "Redesign emoji + suggestion popups as a ..." | Re-trigger Greptile