mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
@@ -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 |
|
||||
| --------------------------------------------------------- | --------------------------------------------------------- |
|
||||
|  |  |
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||

|
||||
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 |
@@ -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.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user