UI: one shared popover/floating-surface interface #100

Closed
opened 2026-09-25 17:22:38 +00:00 by kayg · 8 comments
Owner

Goal

Define one shared Popover/Preview interface for anchored preview surfaces. It must supply header, body, chips and actions snippets plus shared hover, pin and close state. Keep menu, listbox, tooltip and modal semantics intact.

Current evidence

At revision 0e6c428992, Menu and Inspector use FloatingSurface. CalendarPopover independently repeats positioning, material and motion. Calendar route owns hover timers while CalendarPopover owns the pinned boolean. SearchPreview reuses ItemPreview content but does not use a shared preview shell. Recovery-key and slash suggestions have bespoke popup materials.

The Calendar pin-after-hover-leave failure reproduced against a local server running the production SPA. In a 1440 × 900 Playwright capture, the Log entry title was 73 px tall with no clamp; Open day was visible text and had no Tooltip. Calendar material measured 46% surface tint plus 18 px blur in Paper and 55% plus 18 px blur in Tokyo Night.

Implementation plan

  1. Add packages/ui/src/components/PopoverSurface.svelte. Use named snippets for header, body, chips and actions. Provide an optional arrow and one hover/pin/close controller. Hover closes on leave or Escape. Pin cancels hover cleanup and shows X. X closes and unpins. Phone presentation delegates to OverlaySurface sheet and has no pin or X.
  2. Extend packages/ui/src/components/menu/FloatingSurface.svelte with non-modal/no-scrim behavior for previews and active-theme-aware material, preserving Menu and Inspector defaults.
  3. Replace CalendarPopover’s custom placement, resize observer, global pointer/Escape handlers and copied material/motion CSS. Wire calendar route preview/composer to the shared interface.
  4. Adapt ItemPreview to the common anatomy, add two-line title clamp and icon-only Open day/Open note actions with accessible names and warm Tooltip. Keep full title available to assistive technology.
  5. Reuse the content anatomy in SearchPreview and the search window’s phone peek. Keep Search and the peek on OverlaySurface.
  6. Use the common placement/material shell for RecoveryKeyEntry suggestions while retaining combobox/listbox roles and input focus.
  7. Port the slash menu’s shell and positioning from packages/editor/src/slash.ts and apps/web/src/lib/notes/NoteEditorSurface.svelte to the common menu/surface implementation. Preserve listbox semantics and keyboard handling.
  8. Compose Inspector with the shared shell while retaining its opaque reading material, beside arrow, focus contract and phone sheet.
  9. Add browser coverage in apps/web/e2e/calendar.mjs and apps/web/e2e/search.mjs for hover pin persistence, X/unpin semantics, long titles, Tooltip labels, dark/light material and phone sheet behavior.
  10. Remove packages/ui/src/components/DialogChrome.svelte and its packages/ui/src/index.ts export after confirming no package consumer uses the exported orphan.

Duplicate code to remove

  • CalendarPopover’s placement function, MARGIN/GAP constants, resize observer, document/window listeners and duplicate material/transition recipe.
  • RecoveryKeyEntry’s separate surface tint, border, radius, blur and absolute position after adoption.
  • Slash menu’s manual fixed top/left placement and duplicate popup shell CSS after adoption.
  • The route’s separate pinned flag and hover-leave cleanup once PopoverSurface owns the state.
  • DialogChrome’s unused exported modal shell after consumer verification.

Keep OverlaySurface for modal sheets/dialogs and keep Menu/Tooltip/listbox roles distinct. Do not make every floating surface use the preview card’s content structure.

Estimated size: medium refactor, about 10 implementation files plus two browser e2e files, one new UI component, and roughly 350–500 changed lines. Land in small slices: shared shell/state, Calendar integration, content anatomy/actions/text, then search/auth/editor integrations and cleanup.

## Goal Define one shared Popover/Preview interface for anchored preview surfaces. It must supply header, body, chips and actions snippets plus shared hover, pin and close state. Keep menu, listbox, tooltip and modal semantics intact. ## Current evidence At revision 0e6c4289929dd450e2784f64359d6910f2912cde, Menu and Inspector use FloatingSurface. CalendarPopover independently repeats positioning, material and motion. Calendar route owns hover timers while CalendarPopover owns the pinned boolean. SearchPreview reuses ItemPreview content but does not use a shared preview shell. Recovery-key and slash suggestions have bespoke popup materials. The Calendar pin-after-hover-leave failure reproduced against a local server running the production SPA. In a 1440 × 900 Playwright capture, the Log entry title was 73 px tall with no clamp; Open day was visible text and had no Tooltip. Calendar material measured 46% surface tint plus 18 px blur in Paper and 55% plus 18 px blur in Tokyo Night. ## Implementation plan 1. Add packages/ui/src/components/PopoverSurface.svelte. Use named snippets for header, body, chips and actions. Provide an optional arrow and one hover/pin/close controller. Hover closes on leave or Escape. Pin cancels hover cleanup and shows X. X closes and unpins. Phone presentation delegates to OverlaySurface sheet and has no pin or X. 2. Extend packages/ui/src/components/menu/FloatingSurface.svelte with non-modal/no-scrim behavior for previews and active-theme-aware material, preserving Menu and Inspector defaults. 3. Replace CalendarPopover’s custom placement, resize observer, global pointer/Escape handlers and copied material/motion CSS. Wire calendar route preview/composer to the shared interface. 4. Adapt ItemPreview to the common anatomy, add two-line title clamp and icon-only Open day/Open note actions with accessible names and warm Tooltip. Keep full title available to assistive technology. 5. Reuse the content anatomy in SearchPreview and the search window’s phone peek. Keep Search and the peek on OverlaySurface. 6. Use the common placement/material shell for RecoveryKeyEntry suggestions while retaining combobox/listbox roles and input focus. 7. Port the slash menu’s shell and positioning from packages/editor/src/slash.ts and apps/web/src/lib/notes/NoteEditorSurface.svelte to the common menu/surface implementation. Preserve listbox semantics and keyboard handling. 8. Compose Inspector with the shared shell while retaining its opaque reading material, beside arrow, focus contract and phone sheet. 9. Add browser coverage in apps/web/e2e/calendar.mjs and apps/web/e2e/search.mjs for hover pin persistence, X/unpin semantics, long titles, Tooltip labels, dark/light material and phone sheet behavior. 10. Remove packages/ui/src/components/DialogChrome.svelte and its packages/ui/src/index.ts export after confirming no package consumer uses the exported orphan. ## Duplicate code to remove - CalendarPopover’s placement function, MARGIN/GAP constants, resize observer, document/window listeners and duplicate material/transition recipe. - RecoveryKeyEntry’s separate surface tint, border, radius, blur and absolute position after adoption. - Slash menu’s manual fixed top/left placement and duplicate popup shell CSS after adoption. - The route’s separate pinned flag and hover-leave cleanup once PopoverSurface owns the state. - DialogChrome’s unused exported modal shell after consumer verification. Keep OverlaySurface for modal sheets/dialogs and keep Menu/Tooltip/listbox roles distinct. Do not make every floating surface use the preview card’s content structure. Estimated size: medium refactor, about 10 implementation files plus two browser e2e files, one new UI component, and roughly 350–500 changed lines. Land in small slices: shared shell/state, Calendar integration, content anatomy/actions/text, then search/auth/editor integrations and cleanup.
Author
Owner

Audit context: worktree popover-audit was detached at dev, base and current source SHA 0e6c428992. This audit made no tracked changes.

Findings with evidence:

  • #101: Playwright pinned a hovered Log entry preview, moved the pointer away, and observed it disappear (stillPresentAfterLeave=false). The route’s 220 ms hover-leave callback clears preview.hover without checking pinned.
  • #102: Playwright found visible Open day text and no data-tooltip in the Calendar preview. ItemPreview renders that action as a text pill without tip().
  • #103: the long title measured 73 px at 18.2 px line-height, with line-clamp none and overflow visible. It wraps, so the capture did not clip it, but it violates the two-line title policy.
  • #104: FloatingSurface uses prefers-color-scheme for its dark recipe while the app signals explicit dark themes with :root.is-dark.
Audit context: worktree popover-audit was detached at dev, base and current source SHA 0e6c4289929dd450e2784f64359d6910f2912cde. This audit made no tracked changes. Findings with evidence: - #101: Playwright pinned a hovered Log entry preview, moved the pointer away, and observed it disappear (stillPresentAfterLeave=false). The route’s 220 ms hover-leave callback clears preview.hover without checking pinned. - #102: Playwright found visible Open day text and no data-tooltip in the Calendar preview. ItemPreview renders that action as a text pill without tip(). - #103: the long title measured 73 px at 18.2 px line-height, with line-clamp none and overflow visible. It wraps, so the capture did not clip it, but it violates the two-line title policy. - #104: FloatingSurface uses prefers-color-scheme for its dark recipe while the app signals explicit dark themes with :root.is-dark.
Author
Owner

Audit finished.

Source head SHA: 0e6c428992. Worktree remains detached at dev with no tracked source or lockfile changes and no commit.

Build output:
✓ built in 25.45s

Using @sveltejs/adapter-static
Wrote site to "build"

The production SPA was served by the local server and Playwright captured 1440 × 900 and 390 × 844 Calendar previews in Paper and Tokyo Night. Pin-after-mouse-leave reproduced as stillPresentAfterLeave=false.

No cargo fmt, clippy, cargo test, bun check or bun test gates were run for this read-only audit. Cargo cleanup completed: Removed 6877 files, 4.2GiB total.

Report and screenshots are in /home/kayg/Developer/calternal/target/popover-audit/.

Audit finished. Source head SHA: 0e6c4289929dd450e2784f64359d6910f2912cde. Worktree remains detached at dev with no tracked source or lockfile changes and no commit. Build output: ✓ built in 25.45s > Using @sveltejs/adapter-static Wrote site to "build" The production SPA was served by the local server and Playwright captured 1440 × 900 and 390 × 844 Calendar previews in Paper and Tokyo Night. Pin-after-mouse-leave reproduced as stillPresentAfterLeave=false. No cargo fmt, clippy, cargo test, bun check or bun test gates were run for this read-only audit. Cargo cleanup completed: Removed 6877 files, 4.2GiB total. Report and screenshots are in /home/kayg/Developer/calternal/target/popover-audit/.
Author
Owner

Owner decision after this audit (2026-09-25), which supersedes the X/pin semantics in this plan: there is NO pin and NO ✕ on previews. Hover shows a transient preview (it closes on leave or Esc). Selecting an item (click/tap/Enter) keeps its popover open until a click outside, another selection or Esc. On the Calendar (not the sidebar), the selected popover can be dragged by any non-text area, clamped to the viewport, with keyboard arrows. Action pills are icon-only with the shared warm tooltip (label + shortcut). Long names: 2 lines, then truncated. Build the consolidated Popover/Preview surface on these rules. This job starts after job/polish-menus-search merges, which implements the rules in the current primitives.

Owner decision after this audit (2026-09-25), which supersedes the X/pin semantics in this plan: there is NO pin and NO ✕ on previews. Hover shows a transient preview (it closes on leave or Esc). Selecting an item (click/tap/Enter) keeps its popover open until a click outside, another selection or Esc. On the Calendar (not the sidebar), the selected popover can be dragged by any non-text area, clamped to the viewport, with keyboard arrows. Action pills are icon-only with the shared warm tooltip (label + shortcut). Long names: 2 lines, then truncated. Build the consolidated Popover/Preview surface on these rules. This job starts after job/polish-menus-search merges, which implements the rules in the current primitives.
Author
Owner

Starting issue #100 on branch job/popovers, based on dev at 7c1d6c82ea98ac3772117a534fbb923825a60bab (HEAD and dev matched at start).

The audit plan calls for preview pin/X controls. The current owner rules in docs/DESIGN.md §34 and §38 require previews without either control; hover previews close on leave or Escape, and selected previews remain until outside click, another selection, or Escape. I will follow the owner rules and apply the same shared interface to the requested floating surfaces.

Starting issue #100 on branch `job/popovers`, based on `dev` at `7c1d6c82ea98ac3772117a534fbb923825a60bab` (HEAD and dev matched at start). The audit plan calls for preview pin/X controls. The current owner rules in docs/DESIGN.md §34 and §38 require previews without either control; hover previews close on leave or Escape, and selected previews remain until outside click, another selection, or Escape. I will follow the owner rules and apply the same shared interface to the requested floating surfaces.
Author
Owner

The first real-server e2e confirmed the #103 defect: for a long Calendar Log entry title, Chromium computed -webkit-line-clamp: none; the new assertion expected 2 and failed at apps/web/e2e/popovers.mjs:44.

Source review found a related state violation: the Calendar route's select() calls closePreview() when the already selected item is selected again. The owner contract keeps it open until an outside click, another item, or Escape. I am adding regression coverage and will make repeated selection a no-op.

The #102 action contract is already present in this base: Open day/Open note controls in ItemPreview.svelte are icon-only, carry aria-label, and use tip(). The #104 theme contract is also present in tokens.css: :root.is-dark changes --glass-clear-alpha based on the active app theme. I will retain and browser-check both.

The first real-server e2e confirmed the #103 defect: for a long Calendar Log entry title, Chromium computed `-webkit-line-clamp: none`; the new assertion expected `2` and failed at `apps/web/e2e/popovers.mjs:44`. Source review found a related state violation: the Calendar route's `select()` calls `closePreview()` when the already selected item is selected again. The owner contract keeps it open until an outside click, another item, or Escape. I am adding regression coverage and will make repeated selection a no-op. The #102 action contract is already present in this base: Open day/Open note controls in `ItemPreview.svelte` are icon-only, carry `aria-label`, and use `tip()`. The #104 theme contract is also present in `tokens.css`: `:root.is-dark` changes `--glass-clear-alpha` based on the active app theme. I will retain and browser-check both.
Author
Owner

Finding: after moving Calendar previews onto FloatingSurface, the existing Calendar E2E failed at apps/web/e2e/calendar.mjs:107: preview text is selectable was false. FloatingSurface applies .no-select-callout, but the shared preview content had not opted into the app's .allow-select carve-out. I added that marker to PopoverSurface content, which restores the documented CSS and selection.ts behavior. Production rebuild and E2E rerun are in progress.

Finding: after moving Calendar previews onto FloatingSurface, the existing Calendar E2E failed at `apps/web/e2e/calendar.mjs:107`: `preview text is selectable` was false. FloatingSurface applies `.no-select-callout`, but the shared preview content had not opted into the app's `.allow-select` carve-out. I added that marker to PopoverSurface content, which restores the documented CSS and selection.ts behavior. Production rebuild and E2E rerun are in progress.
Author
Owner

Starting continuation on branch job/popovers at HEAD 5011ca16ca8daed193b72ed1a62d7ad620c80e25, based on dev merge-base 7c1d6c82ea98ac3772117a534fbb923825a60bab. I am reviewing the inherited uncommitted #100 changes before finishing them, then I will implement the shared overlay glass material and capture the requested production-build screenshots.

Starting continuation on branch `job/popovers` at HEAD `5011ca16ca8daed193b72ed1a62d7ad620c80e25`, based on `dev` merge-base `7c1d6c82ea98ac3772117a534fbb923825a60bab`. I am reviewing the inherited uncommitted #100 changes before finishing them, then I will implement the shared overlay glass material and capture the requested production-build screenshots.
Author
Owner

Merged into dev at 19676b44 after Claude's screenshot review (shared overlay glass, lighter in both themes). Deploy status on #203.

Merged into dev at 19676b44 after Claude's screenshot review (shared overlay glass, lighter in both themes). Deploy status on #203.
kayg closed this issue 2026-09-26 20:39:07 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#100
No description provided.