diff --git a/docs/bug-reproductions/inline-reference-status/README.md b/docs/bug-reproductions/inline-reference-status/README.md new file mode 100644 index 00000000000..6c85e5be9c7 --- /dev/null +++ b/docs/bug-reproductions/inline-reference-status/README.md @@ -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. diff --git a/docs/bug-reproductions/inline-reference-status/after.gif b/docs/bug-reproductions/inline-reference-status/after.gif new file mode 100644 index 00000000000..53ec525d199 Binary files /dev/null and b/docs/bug-reproductions/inline-reference-status/after.gif differ diff --git a/docs/bug-reproductions/inline-reference-status/after.png b/docs/bug-reproductions/inline-reference-status/after.png new file mode 100644 index 00000000000..29db21db9b2 Binary files /dev/null and b/docs/bug-reproductions/inline-reference-status/after.png differ diff --git a/docs/bug-reproductions/inline-reference-status/before.png b/docs/bug-reproductions/inline-reference-status/before.png new file mode 100644 index 00000000000..27e2c6a33b5 Binary files /dev/null and b/docs/bug-reproductions/inline-reference-status/before.png differ diff --git a/docs/bug-reproductions/inline-reference-status/comparison/README.md b/docs/bug-reproductions/inline-reference-status/comparison/README.md new file mode 100644 index 00000000000..6c1e45e2b4b --- /dev/null +++ b/docs/bug-reproductions/inline-reference-status/comparison/README.md @@ -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) diff --git a/docs/bug-reproductions/inline-reference-status/comparison/dark-comparison.png b/docs/bug-reproductions/inline-reference-status/comparison/dark-comparison.png new file mode 100644 index 00000000000..bf8e73c95bd Binary files /dev/null and b/docs/bug-reproductions/inline-reference-status/comparison/dark-comparison.png differ diff --git a/docs/bug-reproductions/inline-reference-status/comparison/light-comparison.png b/docs/bug-reproductions/inline-reference-status/comparison/light-comparison.png new file mode 100644 index 00000000000..072f17ec7a5 Binary files /dev/null and b/docs/bug-reproductions/inline-reference-status/comparison/light-comparison.png differ diff --git a/docs/bug-reproductions/inline-reference-status/comparison/text-shimmer.gif b/docs/bug-reproductions/inline-reference-status/comparison/text-shimmer.gif new file mode 100644 index 00000000000..2ce29fb39b7 Binary files /dev/null and b/docs/bug-reproductions/inline-reference-status/comparison/text-shimmer.gif differ diff --git a/resources/licenses/shadcn/LICENSE-MIT b/resources/licenses/shadcn/LICENSE-MIT new file mode 100644 index 00000000000..fad4d887a68 --- /dev/null +++ b/resources/licenses/shadcn/LICENSE-MIT @@ -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. diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index cd7361aa5a3..6dc469142f7 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -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 { diff --git a/src/renderer/src/assets/text-shimmer.css b/src/renderer/src/assets/text-shimmer.css new file mode 100644 index 00000000000..687dd85e98c --- /dev/null +++ b/src/renderer/src/assets/text-shimmer.css @@ -0,0 +1,112 @@ +/* Source: https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/src/tailwind.css (MIT). */ +@property --shimmer-angle { + syntax: ''; + 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; + } +} diff --git a/src/renderer/src/components/sidebar/WorktreeCardLinkedItemsSection.tsx b/src/renderer/src/components/sidebar/WorktreeCardLinkedItemsSection.tsx index c6c195002bd..7eff5d03ed3 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardLinkedItemsSection.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardLinkedItemsSection.tsx @@ -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 ? (
- {detail?.stale ? ( - - {translate('workspace.links.lastKnownStatus', 'Last known status')} - - ) : null} {live && 'baseRefName' in live && live.baseRefName ? ( {translate('workspace.links.reviewTarget', 'Target: {{branch}}', { diff --git a/src/renderer/src/components/sidebar/WorktreeCardReferenceRow.tsx b/src/renderer/src/components/sidebar/WorktreeCardReferenceRow.tsx index c77ca916f3f..51a4d81c63b 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardReferenceRow.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardReferenceRow.tsx @@ -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 ( -
+
+ {stale ? ( + + {translate('workspace.links.lastKnownStatus', 'Last known status')} + + ) : null}
{review ? ( @@ -113,9 +121,9 @@ export function WorktreeCardReferenceRow({ ) : null} - {label} + {label} {review?.status ? : null} - + {provider === 'GitHub' ? null : `· ${provider}`} {source ? ` · ${source}` : ''}