fix: shimmer stale reference metadata without layout shifts (#32949)

* fix: keep stale linked-reference status inline

* fix: shimmer stale reference rows without a clock

* docs: compare row and shadcn text shimmer treatments

* fix: shimmer only stale reference metadata

* fix: remove obsolete row shimmer class

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-23 16:54:09 -07:00
committed by GitHub
co-authored by OrcaWin
parent 01652255f0
commit 5ee5765c44
13 changed files with 212 additions and 10 deletions
@@ -0,0 +1,31 @@
# Inline reference status
“Last known status” added a third line to stale linked references. When a refresh
cleared it, each affected row shrank by 20px and moved the content below it.
Stale references now shimmer only on the reference number and repository/provider
text. Titles, check icons, and the row background stay steady. Screen readers
retain the status label, and reduced motion renders the metadata normally.
| Before | After |
| --------------------------------------------------------- | --------------------------------------------------------- |
| ![Stale references with an extra status line](before.png) | ![Stale references with metadata-only shimmer](after.png) |
![Animated metadata-only shimmer](after.gif)
The selected text effect uses shadcn's [official CSS shimmer utility](https://ui.shadcn.com/docs/utils/shimmer).
Its MIT license is retained in `resources/licenses/shadcn/LICENSE-MIT`.
The [temporary comparison Gist](https://gist.github.com/OrcaWin/5e39ef4c8975d13bda6a2c79a6c904a7)
and [comparison captures](comparison/README.md) preserve the reviewed alternatives.
The selected option is C, metadata-only shimmer.
Captured with Playwright CDP from a hidden Electron renderer on an isolated Xvfb
display, rendering the actual `WorktreeCardLinkedItemsSection` and application CSS
with five seeded PRs and two stale references. These are component captures, not
screenshots of a signed-in app session.
Verified in light and dark themes at 320px and 430px widths: switching those two
references from stale to fresh reduced the original card from 407px to 367px.
Both final states measure 367px, with identical row positions and no horizontal
overflow. Only metadata text in stale rows animates. Reduced motion renders the
text normally, RTL reverses the sweep, and reference links remain clickable.
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

@@ -0,0 +1,34 @@
# Stale-reference animation options
[View the temporary image comparison Gist](https://gist.github.com/OrcaWin/5e39ef4c8975d13bda6a2c79a6c904a7).
These captures record the options reviewed before implementation. **Option C,
metadata-only shimmer, was selected.**
| Option | Treatment | Tradeoff |
| ---------------- | ------------------------------- | -------------------------------------------------------------------- |
| A | Current whole-row shimmer | Most noticeable; also animates the surface. |
| B | Title and metadata text shimmer | Closest to the requested effect; keeps icons and the surface steady. |
| **C — selected** | **Metadata text shimmer** | Quieter; the title stays steady while scanning. |
| D | Still baseline | Least distracting; no visible freshness cue. |
B and C use shadcn's [official shimmer utility](https://ui.shadcn.com/docs/utils/shimmer),
a pure-CSS text effect with RTL and reduced-motion support. Its
[MIT-licensed source](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/src/tailwind.css)
was checked on 23 October 2026. This does not require a React animation component.
[AI Elements Shimmer](https://ai-sdk.dev/elements/components/shimmer) is a more
configurable React/Motion alternative. The third-party
[shadcn.io Shimmering Text](https://www.shadcn.io/text/shimmering-text) uses character
animations and optional waves, which are unnecessary for these truncated titles.
The comparison Gist preserves the earlier row and text treatments. The final
implementation uses metadata-only shimmer. The standalone HTML report was a
throw-away design artifact and is excluded from the final change.
All four previews were checked through Playwright CDP in a hidden Electron
renderer: identical 367px card heights before/after clearing stale state, light/dark
at 320px and 430px, controls, OS/manual reduced motion, and no page overflow at
375px. The two stale rows alone animate. Text variants leave check icons unchanged.
![Dark comparison](dark-comparison.png)
![Light comparison](light-comparison.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 shadcn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1
View File
@@ -8,6 +8,7 @@
@import './markdown-preview.css';
@import './terminal.css';
@import './mobile-page.css';
@import './text-shimmer.css';
@import '../../../shared/styles/scrollbar-sleek.css';
@font-face {
+112
View File
@@ -0,0 +1,112 @@
/* Source: https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/src/tailwind.css (MIT). */
@property --shimmer-angle {
syntax: '<angle>';
inherits: true;
initial-value: 20deg;
}
@property --shimmer-image {
syntax: '*';
inherits: false;
}
@property --shimmer-text-fill {
syntax: '*';
inherits: false;
}
@theme inline {
@keyframes tw-shimmer {
from {
background-position: 100% 0;
}
to {
background-position: 0 0;
}
}
}
@utility shimmer {
--_spread: var(--shimmer-spread, calc(3ch + 40px));
--_base: currentColor;
--_highlight: var(--shimmer-color, oklch(from currentColor l c h / calc(alpha* 0.2)));
background-image: var(
--shimmer-image,
linear-gradient(
calc(90deg + var(--shimmer-angle)),
var(--_base) calc(50% - var(--_spread)),
color-mix(in oklch, var(--_highlight), var(--_base) 50%) calc(50% - var(--_spread) * 0.5),
var(--_highlight) 50%,
color-mix(in oklch, var(--_highlight), var(--_base) 50%) calc(50% + var(--_spread) * 0.5),
var(--_base) calc(50% + var(--_spread))
)
);
background-repeat: no-repeat;
background-size: calc(200% + var(--_spread) * 2) 100%;
background-position: 0 0;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: var(--shimmer-text-fill, transparent);
animation: tw-shimmer var(--shimmer-duration, 2s) linear infinite;
@variant dark {
--_highlight: var(
--shimmer-color,
oklch(from currentColor max(0.8, calc(l + 0.4)) c h / calc(alpha + 0.4))
);
}
&:where([dir='rtl'], [dir='rtl'] *) {
animation-direction: reverse;
}
@media (prefers-reduced-motion: reduce) {
animation: none;
background-image: none;
-webkit-text-fill-color: currentColor;
}
}
@utility shimmer-once {
animation-iteration-count: 1;
}
@utility shimmer-reverse {
animation-direction: reverse;
}
@utility shimmer-none {
--shimmer-image: none;
--shimmer-text-fill: currentColor;
}
@utility shimmer-color-* {
--shimmer-color: --value(--color, [color]);
--shimmer-color: color-mix(
in oklch,
--value(--color, [color]) calc(--modifier(integer) * 1%),
transparent
);
}
@utility shimmer-duration-* {
--shimmer-duration: calc(--value(integer) * 1ms);
}
@utility shimmer-spread-* {
--shimmer-spread: calc(var(--spacing) * --value(integer));
--shimmer-spread: --value([length], [percentage]);
}
@utility shimmer-angle-* {
--shimmer-angle: calc(--value(integer) * 1deg);
}
/* Kept for backwards compatibility. Unlayered, so it still wins over other
utilities on a plain .shimmer element. Variants are handled in the utility. */
@media (prefers-reduced-motion: reduce) {
.shimmer {
animation: none;
background-image: none;
-webkit-text-fill-color: currentColor;
}
}
@@ -109,6 +109,7 @@ export function WorktreeCardLinkedItemsSection({
label={label}
provider={workspaceAttachmentProviderLabel(item)}
source={source}
stale={detail?.stale}
url={url}
onOpen={(url, event) => open({ ...item, url }, event)}
onManage={onManage ? (event) => open(item, event) : undefined}
@@ -116,16 +117,10 @@ export function WorktreeCardLinkedItemsSection({
item.type !== 'issue' ? renderReviewActions?.(item, live) : undefined
}
/>
{detail?.stale ||
(live && 'baseRefName' in live && live.baseRefName) ||
{(live && 'baseRefName' in live && live.baseRefName) ||
detail?.stateName ||
detail?.issueState ? (
<div className="flex flex-wrap items-center gap-1 pl-5">
{detail?.stale ? (
<span className="text-2xs text-muted-foreground">
{translate('workspace.links.lastKnownStatus', 'Last known status')}
</span>
) : null}
{live && 'baseRefName' in live && live.baseRefName ? (
<span className="text-2xs text-muted-foreground">
{translate('workspace.links.reviewTarget', 'Target: {{branch}}', {
@@ -4,6 +4,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { GitHubIcon } from '@/components/icons/GitHubIcon'
import { FilePathCursorTooltip } from '@/components/file-path-cursor-tooltip'
import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
import { ReviewChecksIndicator, reviewStateLabel } from './WorktreeCardMetadataStatusBadges'
import { getReviewLabel, ReviewIcon } from './worktree-review-helpers'
import type { WorktreeCardPrDisplay } from './worktree-card-pr-display'
@@ -26,6 +27,7 @@ export function WorktreeCardReferenceRow({
label,
provider,
source,
stale,
url,
onOpen,
onManage,
@@ -37,13 +39,19 @@ export function WorktreeCardReferenceRow({
label: string
provider: string
source?: string | null
stale?: boolean
url?: string
onOpen: (url: string, event: MouseEvent) => void
onManage?: (event: MouseEvent) => void
actions?: ReactNode
}): React.JSX.Element {
return (
<div className="flex min-w-0 items-start gap-2">
<div className="flex min-w-0 items-start gap-2" data-stale={stale || undefined}>
{stale ? (
<span className="sr-only">
{translate('workspace.links.lastKnownStatus', 'Last known status')}
</span>
) : null}
<div className="mt-0.5 flex w-3.5 shrink-0 items-center">
{review ? (
<Tooltip>
@@ -113,9 +121,9 @@ export function WorktreeCardReferenceRow({
<GitHubIcon className="size-3" />
</span>
) : null}
<span className="shrink-0">{label}</span>
<span className={cn('shrink-0', stale && 'shimmer shimmer-duration-2400')}>{label}</span>
{review?.status ? <ReviewChecksIndicator status={review.status} /> : null}
<span className="min-w-0 flex-1 truncate">
<span className={cn('min-w-0 flex-1 truncate', stale && 'shimmer shimmer-duration-2400')}>
{provider === 'GitHub' ? null : `· ${provider}`}
{source ? ` · ${source}` : ''}
</span>