Adorn hover bar: anchor left when it fits, clamp to right corner radius when it doesn't#4999
Conversation
Preview deploymentsHost Test Results 1 files ±0 1 suites ±0 1h 48m 43s ⏱️ +5s Results for commit a01d6a3. ± Comparison against earlier commit 9da71e1. Realm Server Test Results 1 files ±0 1 suites ±0 12m 9s ⏱️ -9s Results for commit a01d6a3. ± Comparison against earlier commit 9da71e1. |
The type-label tab was anchored to the card's top-left corner, so long card type names overflowed off the right edge. Anchor it to the top-right corner instead so any overhang happens off the left edge of the card. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
d3ae0a2 to
31b3636
Compare
…n it doesn't The previous fix flipped the tab to a right anchor so long names spilled off the left edge instead of the right. Designers wanted a two-phase behavior: the tab should hug the card's top-left corner while it fits and only switch to anchoring against the start of the card's top-right corner radius (with the extra width spilling off the left) once a left anchor would put its right edge into the rounded corner. CSS positioning alone can't express this because alignment direction and overflow direction are tied. The fix: - The velcro offset middleware now also exposes the card's border-top-right-radius as the --card-corner-radius CSS variable on the overlay, so chrome anchored above the card has a single source of truth for where the rounded corner begins. - A ResizeObserver-based modifier on the label computes the right positioning in JS: when the label's natural width fits the available space (overlay clientWidth - corner radius + 4px stroke bleed), it sets left to -4px; otherwise it writes an integer-pixel left value that pins the right edge to the corner-radius point and lets the extra width spill off the card's left edge. The integer-pixel inline left avoids the dropdown-click jitter that a CSS right: calc(...) anchor would inherit from sub-pixel overlay-width recomputations velcro's autoUpdate performs on DOM mutations. - The integration test now asserts the new two-phase behavior: left-anchored when the label fits, right-edge clamped to the corner radius (and overflowing past the overlay's left edge) when it doesn't. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates the Adorn hover type-label tab to use a two-phase anchor: it hugs the card's top-left corner when its content fits, and pins its right edge to the start of the top-right corner radius (overflowing leftward) when it doesn't. The decision is made in JS using a ResizeObserver and integer pixel measurements with hysteresis to avoid sub-pixel flicker, and the velcro overlay now exposes the card's top-right border radius as a --card-corner-radius CSS variable so the label can compute its anchor.
Changes:
- Surface
--card-corner-radius(from the reference card'sborder-top-right-radius) on the floating overlay element in the velcro offset middleware. - Add a
trackLabelOverflowember-modifier on the type-label tab that toggles adata-overflowattribute and writes an integer-pixel inlineleftbased on overlay width, corner radius, and label scrollWidth, with 4px hysteresis. - Add an integration test asserting both the fitting (left-anchored) and overflowing (right-clamped-to-radius) phases.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/host/app/components/operator-mode/overlays.gts | Exposes --card-corner-radius from the reference card via the velcro middleware. |
| packages/host/app/components/operator-mode/operator-mode-overlays.gts | Adds the trackLabelOverflow modifier and updates the adorn-label comments to reflect JS-driven positioning. |
| packages/host/tests/integration/components/overlay-menu-items-test.gts | Adds integration test covering both anchor phases of the hover type-label tab. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
is there a scenario where the tile you are adorning is already on the left side of the isolated view, in that case wouldn't the text get cropped? it feels like you are relying on the geometry of cards grid specifically to prevent that from happening--but that's really just one particular way of rendering tiles such that you always have space on the left. Perhaps something like the kanban board view is not so generous with its left side. its not too hard to conceive of a card that has no space on the left for the adorn title to overflow. |
…pping The Adorn hover-label tab grows leftward off a card's left edge once its content would otherwise push its right edge into the card's top-right corner radius. That spill assumes the card has room to its left. A narrow kanban column with overflow:hidden (typical for vertically scrolling columns) clips the leftward growth at the column boundary, so the type-name truncates before it's fully readable. This card renders a 3-column kanban with narrow overflow:hidden columns and a child card class whose displayName is deliberately long, so the clipping is immediately reproducible by hovering any task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The six ProjectInitiativeTask instance JSONs reference the class by name in adoptsFrom, so it has to be a named export of the module, not a private declaration inside it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ide the containing card Previously the label used its own custom measurement + inline-left logic to pick between a left anchor and a right-at-corner-radius anchor. That kept it on top of the card but couldn't keep it inside the card's container — a long type-name in a tightly-bounded layout (kanban column, dialog content area, top edge of a stack item) spilled into chrome that should not be overlapped. Drive position with @floating-ui/dom instead, anchored to the rendered card element: - The placement decision (left-anchored vs right-anchored at the corner-radius point) is unchanged — short labels still hug the top- left corner, long ones still pin their right edge to the corner- radius point and grow leftward. A 4px hysteresis band still keeps sub-pixel wobble from flipping the decision. - `flip` switches the label below the card when there's no room above. A custom middleware writes a `data-side` attribute on the label; CSS mirrors the clip-path vertically so the slope keeps pointing toward the card. - `shift` slides the label horizontally to stay inside the boundary. - `size` caps `max-width` to whatever's still available after shifting; text-overflow:ellipsis truncates the type-name rather than letting the label spill into surrounding chrome. The boundary is the closest enclosing rendered-card wrapper (`[data-boxel-card-id]`) — i.e. the card this card is embedded in — falling back to `.stack-item-content` for top-level cards. Sibling content within the same containing card is fair game (the long-name label may visually overlap a sibling embedded card); operator-mode chrome around the containing card is not. Drop the `--card-corner-radius` CSS variable that the overlay's offset middleware was writing — floating-ui no longer needs it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…red card Without a limit, shift was happily dragging a long-name label across multiple sibling columns to fit it inside the containing card, leaving the tab's slope pointing at a card the cursor wasn't on. Use limitShift with a 16px budget so the label can still nudge a bit when it's right at the boundary, but beyond that the size middleware truncates with an ellipsis and the slope stays anchored above the hovered card. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…irectly
floating-ui's flip+shift+size middlewares aren't a clean fit for the
right-anchored-with-truncation pattern this label needs:
- limitShift({ offset }) doesn't cap shift distance — its `offset`
is a minimum reference/floating overlap, so for a long label and a
narrow card the resulting bounds let shift drag the label all the
way across the boundary.
- Even if shift were tightly limited, the size middleware reports
the full boundary width as availableWidth whenever shift.enabled.x
is true, so the label still wouldn't truncate.
Compute the placement directly: read the card and boundary rects,
pick top vs bottom from the available vertical space, pick
left-anchored vs right-anchored from whether the natural label
width fits the card's interior, then clamp the un-anchored edge
against the boundary and write `max-width` so CSS
text-overflow:ellipsis truncates the type-name. autoUpdate from
floating-ui is still useful as the re-fire trigger on scroll /
resize / ancestor mutations.
Net effect on the kanban demo: hovering a BACKLOG card now keeps
the label's slope above that card, and the type-name truncates
with an ellipsis rather than spilling across into IN PROGRESS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e card In the !shouldOverflow branch (label fits within the card's interior by construction), we were still doing anchorRightX = min(boundaryRect.right - 4, anchorLeftX + labelWidth) which would shave a few pixels off whenever the boundary's right edge sat just past the card's, triggering text-overflow:ellipsis on labels that obviously had room to spare. When the label fits the card, the label fits the card — no need to also clamp against the boundary on that side. Use the natural width and let it render in full. The overflow case still clamps against the boundary so the ellipsis kicks in when the label would actually spill outside the containing card. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`scrollWidth` is integer-rounded, so a label whose natural width is 141.5px reads back as 141 and writing `max-width: 141px` shaves the sub-pixel remainder off — enough to trigger `text-overflow: ellipsis` on a short label (GARDEN ITEM, ISSUE, THEME) that obviously has room to spare. In the !shouldOverflow branch the browser already knows the natural width better than we can measure it, so let it pick: write `max-width: max-content` and the label sizes to its true intrinsic width without any rounding. The overflow branch still uses a measured pixel value because that's where ellipsis-on-purpose lives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous boundary was the nearest enclosing rendered-card wrapper (`[data-boxel-card-id]`), which for a card embedded inside another card just barely wider than itself ends up clamping the label against the parent-card's left edge — triggering the ellipsis on a label that had plenty of empty space available a few pixels further left, in a sibling card or column. Switch to bounding by `.stack-item-content` — the visible frame of the operator-mode stack item, which is the same panel the card is rendered on. Within that frame the label is free to extend across sibling cards / columns when the hovered card sits near an edge (the overlap case is the one we previously accepted as fine), and the frame still keeps the label out of the chrome around it (sidebar, dialog title bar) which was the original motivation for bounding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Print the values trackLabelOverflow is working with on each update — scrollWidth, card rect, boundary rect + element, computed available width, shouldOverflow decision, and the max-width that gets applied — so we can see which input is wrong when the label ellipsizes a label that obviously had room to spare. To be reverted once the underlying issue is understood. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
So dev-tools console output is copy-paste friendly instead of
collapsed {…} objects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fits in boundary Logged diagnostics on a long-name hover showed scrollWidth=204, unclamped anchorLeft=1381, boundary.left=953 — i.e. the label's natural width fit fully inside the boundary. But my overflow branch unconditionally wrote `max-width: 204px` (the integer-rounded scrollWidth), shaving the sub-pixel remainder and tripping text-overflow:ellipsis on a label that obviously had room to spare. Split the overflow branch: when the natural label width fits within the boundary (unclamped anchorLeft >= boundary.left + padding), use max-content so the browser picks the true intrinsic width. Only when the label actually has to be clamped against the boundary do we write a measured pixel value — that's the case where ellipsis is the desired outcome. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The chip lost its border-radius along the way when the clip-path's in-polygon bevels (from the earlier anti-aliasing-seam fix) were inadvertently kept while the rounded look was actually desired. Restore `border-radius: 5px 0 0 5px` so the left corners are properly rounded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolved overlay-related conflicts in favor of HEAD: - `packages/host/app/components/operator-mode/operator-mode-overlays.gts`: main carried PR #5000's clip-path-with-4px-bevels approach (no border-radius, to eliminate an anti-aliasing seam). HEAD restores `border-radius: 5px 0 0 5px` per the design intent on this branch. Kept HEAD's CSS — restored radius + simpler slope-only clip-path. - `packages/host/tests/integration/components/overlay-menu-items-test.gts`: main's seam-guard test asserts `borderTopLeftRadius === '0px'`, which is the opposite of HEAD's design intent. Kept HEAD's positioning test (anchors-left-while-fits / clamps-to-corner-radius- on-overflow / stays-inside-containing-card) and dropped the seam-guard test, since the rounded corners are intentional. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@habdelra thanks for pushing me on that. I've totally revised it to use floating-ui with middleware to handle these scenarios gracefully. Here's an updated video: Screen.Recording.2026-05-28.at.4.16.20.PM.mov |
…the label
Diagnostic logs on a long-name hover showed scrollWidth going from
192 to 197 exactly when the menu opened — a 5px jump that lines up
exactly with the label's `gap: 5px` flex setting. BoxelDropdown
yields two siblings into the label: the trigger and its content
placeholder. When the menu is closed, the content placeholder has
`display: none` and is not a flex item, so no gap is added. When the
menu opens, the placeholder is replaced by the wormhole-origin div
(non-`display: none`), which becomes a flex item, and the label's
flex layout adds a 5px gap between the trigger and it.
Wrap BoxelDropdown in an inline-flex `<span>` so the trigger and the
portal-origin live inside their own flex container — they're one
flex item of the label, not two, and the label's natural width
doesn't change when the menu opens.
Avoid backticks inside the template's `{{! ... }}` comment — content-
tag's lexer reads them as a template-literal opening and errors with
"Invalid count value: -1".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The trackLabelOverflow positioning bug is fixed (the BoxelDropdown wrapper kept the label's natural width stable across menu open / close); remove the [adorn-debug] log + warn lines that were guiding the diagnosis. Behavior unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@backspace the description was out-of-date (now updated) but the behavior you noted was a re-regression (now fixed). Thanks! |
The integration test for the hover type-label tab waited on
`label.getBoundingClientRect().width > 0` before measuring, but the
label has its natural content width as soon as it's inserted into the
DOM — trackLabelOverflow's initial CSS defaults (`position: fixed;
top: 0; left: 0; max-width: max-content`) already give it a non-zero
width before the JS positioning fires. So the wait could resolve
while the label was still at viewport (0, 0), and `labelRect.left`
read 0 instead of `cardRect.left - 4`.
Wait on `label.style.left.endsWith('px')` instead: the modifier sets
`style.left = '0'` (no unit) synchronously at setup, then writes
`Npx` once `update()` has computed the position, so the px suffix is
a reliable signal that the positioner has landed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The label was `position: fixed` with `left`/`top` written in
viewport coordinates. That broke under any ancestor that creates a
fixed-positioning containing block — most notably the test runner's
`#ember-testing { transform: scale(0.5) }` wrapper, which made the
inline left be interpreted in `#ember-testing`'s scaled coordinate
space (so the label rendered ~hundreds of px off from where the
modifier expected). The fitting-case assertion in
overlay-menu-items-test exposed this.
Switch to `position: absolute` so the offset parent is the
velcro'd `.actions-overlay` div (which already tracks the card),
and convert the viewport anchor into the offset parent's local
coordinate space by reading `parentRect / offsetWidth` for the
scale. Production (scale = 1) is unchanged; the test environment
now writes the correct local-space left/top.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
backspace
left a comment
There was a problem hiding this comment.
pending lint fix of course
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Summary
The Adorn hover type-label tab now uses a two-phase anchor:
Resolves CS-11280.
Implementation
trackLabelOverflowember-modifier on the label computes position directly from the rendered card's rect and the visible stack-item frame's rect, then writesleft,top,max-width, and adata-sideattribute on the label inline..stack-item-content— the visible top-level frame for the panel the card is rendered on. Sibling cards / columns within that frame don't constrain the label; the chrome around it does.left = card.left − 4. Long labels (natural width >card.width − corner-radius + 4) anchor their right edge tocard.right − (corner-radius − 4)and grow leftward, with a 4px hysteresis so sub-pixel wobble can't flip the placement on every layout pass.max-width: max-contentso the browser sizes to the true intrinsic width (writing back the integer-roundedscrollWidthwould shave a sub-pixel remainder and triptext-overflow: ellipsiseven with room to spare). Only when the label actually has to clamp against the boundary do we write a measured pixel value, which is the case where ellipsis is the desired outcome.[data-side="bottom"]attribute drives a CSS rule that mirrors theclip-pathpolygon vertically so the slope still points toward the card.BoxelDropdownwith an inline-flex<span>so the dropdown's trigger and its portal-origin counts as a single flex item of the label — without it the label's natural width grows by one flex-gap when the menu opens (the open-state wormhole-origin becomes a flex item where the closed-state placeholder isdisplay: none), shifting the label on every menu open.autoUpdatefrom@floating-ui/domre-fires the position update on scroll, resize, and ancestor mutations; the placement math is direct (floating-ui'sflip+shift+sizemiddleware aren't a clean fit for the right-anchored-with-truncation pattern, andlimitShiftdoesn't actually cap shift distance the way it reads).Tests
overlay-menu-items-test.gtsasserts both anchor phases (short → left-anchored atcard.left − 4, long → right edge clamped to the corner-radius point and overflowing pastcard.left) and that the label stays inside the containing-card frame either way.Demo card
packages/experiments-realm/adorn-overflow-demo.gtsrenders a 3-column kanban with deliberately long task-card type names so the long-label / flip-below / boundary-clamp behavior can be reproduced by hand.Test plan
Screen.Recording.2026-05-28.at.11.03.23.AM.mov
🤖 Generated with Claude Code