feat(web): pet corner anchor setting#2300
Conversation
Add a corner picker to pet settings letting users pin the floating overlay to any of the four viewport corners (top-left, top-right, bottom-left, bottom-right). Default stays bottom-right. The corner field is persisted in the PetConfig localStorage payload. Older configs without the field are normalised to 'bottom-right' at read time via normalizePet, so no migration script is needed. PetOverlay now derives top/bottom/left/right inline styles from the active corner instead of hardcoding right+bottom; the other two axes are left undefined so existing drag behaviour and CSS rules are unaffected. Red spec: apps/web/tests/components/pet-corner.test.tsx (7 tests)
For left-anchored corners (top-left, bottom-left) the .pet-overlay flex container and .pet-bubble::after tail arrow were both right-aligned, causing the bubble to float off to the right of the sprite instead of sitting flush above it. Fix: expose the active corner as data-corner on .pet-overlay and add CSS overrides that switch align-items to flex-start and relocate the bubble tail arrow to the left side of the speech bubble when the pet is in a left-anchored corner.
- docs/codex-pets.md: add 'Corner anchor' section explaining the four-way picker, the web-only persistence boundary, and the inline-style + CSS approach used by PetOverlay + index.css for left-anchored corners. - AGENTS.md i18n section: correct '18 locale files' to '19 locale files' and add 'it' to the explicit locale list (the Italian locale was added but the count and list were never updated).
PerishCode
left a comment
There was a problem hiding this comment.
@neogenix — thanks for the careful corner-anchor work. The PetConfig.corner type, the VALID_CORNERS validation in normalizePet, the legacy {right, bottom} → {x, y} migration in loadPosition, the corner-aware drag math, and the i18n + types.ts updates across all 19 locales all look solid. The Vitest + Playwright coverage of quadrant positioning and the left-anchor bubble-tail regression is nicely scoped.
One non-blocking observation about the top-anchored corners is in the inline comment below — the rest of the change is in good shape.
🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.
| /* Left-anchored corners: move the tail to the left side of the bubble. */ | ||
| .pet-overlay[data-corner='top-left'] .pet-bubble::after, | ||
| .pet-overlay[data-corner='bottom-left'] .pet-bubble::after { | ||
| right: auto; | ||
| left: 18px; | ||
| } |
There was a problem hiding this comment.
Top-anchored corners get a bubble in the corner, not a sprite. This new rule and the align-items: flex-start rule a few blocks up flip the horizontal alignment + tail side for data-corner='*-left', but there is no parallel vertical override for data-corner='top-*'. Because .pet-bubble is the first child of .pet-overlay in JSX (apps/web/src/components/pet/PetOverlay.tsx:470-504) and the container uses flex-direction: column, the bubble always renders above the sprite.
For bottom-anchored corners this is fine (the container's bottom edge is pinned by bottom: pos.y, so the bubble grows upward from a stationary sprite). For top-anchored corners the container's top edge is pinned by top: pos.y — so when the bubble auto-opens on mount the bubble takes the corner the user just picked and the sprite is pushed down by bubble_height + 10px gap. A bare greeting bubble adds ~80–120px; once the pet-task-list panel populates, the sprite can sit 200+px below the top corner. The pet also visibly jumps down whenever the bubble re-opens after the 4s auto-tuck (PetOverlay.tsx:179-185).
Why this matters for the stated goal. The PR is motivated by "the pet eats clicks on Save-to-disk / status pill / dropdown" — i.e. the sprite is the click target you are trying to move out of the way. A user who picks top-right to dodge a top-bar control still sees the sprite drift ~100px below the top edge whenever the bubble is open, which partially undoes the move.
Suggested fix (mirror of the left-anchor pattern, in the same file):
.pet-overlay[data-corner='top-left'],
.pet-overlay[data-corner='top-right'] {
flex-direction: column-reverse;
}
.pet-overlay[data-corner='top-left'] .pet-bubble::after,
.pet-overlay[data-corner='top-right'] .pet-bubble::after {
bottom: auto;
top: -6px;
border-bottom: 0;
border-right: 0;
border-top: 1px solid var(--pet-accent);
border-left: 1px solid var(--pet-accent);
}Worth noting that the existing Playwright quadrant positioning tests in e2e/ui/pet-corner-anchor.test.ts assert against overlay.boundingBox(), which spans both bubble and sprite — they stay green even when the visible sprite ends up well below the top corner. Adding a .pet-sprite bounding-box assertion (sprite top edge < QUAD_THRESHOLD for top corners) would lock the sprite to the chosen corner regardless of bubble state.
🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.
… e2e sprite assertion
Why
The pet overlay anchors to the bottom-right corner of the screen by
default. In several layouts that corner happens to sit over real UI —
the "Save to disk" button on the artifact preview, the running-agent
status pill, and the design-system picker dropdown on smaller windows.
The pet eats those clicks unless the user drags it (which is awkward
once and forgotten).
Letting the user pick which corner the pet anchors to solves it at the
source. The drag-to-reposition mechanic is preserved within the chosen
corner; the corner choice persists across reloads.
What users will see
A new Position row in Pet Settings (Settings → Pet) with a four-way
segmented picker: top-left, top-right, bottom-left, bottom-right.
Default is bottom-right, matching today's behavior, so existing users
notice nothing on upgrade until they open the picker.
Picking a different corner moves the pet immediately. The speech-bubble
alignment and tail flip to the correct side of the sprite for
left-anchored corners (otherwise the bubble would hang off the wrong
side). The drag-to-reposition mechanic still works inside the new
corner.
Surface area
pet.fieldCorner,pet.corner.top-left,.top-right,.bottom-left,.bottom-right)added to
apps/web/src/i18n/types.tsand all 19 locale filesNo CLI surface needed.
PetConfigis a localStorage-only web pref —the daemon never reads or writes it, and there's no existing
od pet …subcommand. This is consistent with the rest ofPetConfig(
adopted,enabled,petId,custom.*) and with peer prefs likethemeandaccentColorthat also live web-only. The newcornerfield follows that exact precedent. If a future PR adds an
od pet …surface (e.g. for headless adoption),
cornershould be added to it inthe same change.
Screenshots
Settings → Pet now shows a four-segment "Position" picker below the
existing controls. (Screen capture pending; the feature is visible only
in the Pet Settings panel — the rest of the UI is unchanged.)
Bug fix verification
This is a feature addition, not a bug fix. Verification approach:
apps/web/tests/components/pet-corner.test.tsx(7 tests):default value, 4 corner-style assertions (top/right vs bottom/left
inline styles),
normalizePetpreserves stored corner and defaultsto bottom-right for legacy configs without the field.
origin/main; the 7th was a coincidental pass thatis now covered with stronger assertions.
e2e/ui/pet-corner-anchor.test.ts(10 tests):4 quadrant positioning checks (real bounding rect against viewport
size), 4 bubble-tail side checks (
getComputedStyle(bubble, '::after')for left-anchored corners — the regression the 10-pass review caught),
one live-picker-change check, one persistence-across-reload check.
before commit
332a9c39(the left-anchor CSS override).Validation
pnpm guard(clean)pnpm typecheck(clean)pnpm --filter @open-design/web test pet-corner— 7/7 passpnpm --filter @open-design/web test PetOverlay PetSettings— adjacent suites pass with the updated fixture (corner: 'bottom-right')pnpm exec playwright test -c playwright.config.ts pet-corner-anchor— 10/10 pass (browser-verified in Chromium, 1.28s)
Adjacent issues (out of scope)
PetSettings'srole="radiogroup"controls (accent swatches, atlasrows, built-in pet grid, and the new corner picker) do not implement
arrow-key navigation. This is a pre-existing pattern across every
segmented picker in
PetSettings, not introduced here. Worth afollow-up a11y sweep covering all of them at once.
isLeftAnchored/isTopAnchoredinPetOverlay.tsxhave Playwright coverage of the final positioningbut no unit tests for the drag delta calculation itself. The Vitest
suite covers the static corner style; the dynamic drag math relies
on Playwright. Follow-up could add a direct unit test.
AGENTS.md's i18n section said "18 locale files" but the repoactually ships 19 (
it.tswas added but the count and explicitlocale list were never updated). Corrected in the
docs(pet)commitalongside the
docs/codex-pets.mdCorner section.Capability exposure note
Per AGENTS.md "Capability exposure (UI/CLI dual-track)", a user-facing
capability normally requires both a web UI surface and an
odCLIsubcommand in the same PR. The
cornerfield is a purelocalStorage-only display preference with no daemon counterpart;
adding a CLI mirror would require first introducing a daemon-side
PetConfigpersistence surface, which is a much larger change andnot justified by this feature's scope. The new field follows the same
web-only precedent as every other
PetConfigfield. If/when a daemonpetsurface lands,cornershould be wired into it at that time.