fix(gif): still previews + a11y for recent GIFs

Address findings from 2 review agents on the recent-GIFs row:

- Motion/perf: the Recent row rendered up to 16 full animated GIFs at
  once (autoplaying). Capture a small still image at pick time
  (fixed_width_small_still / *_still) into RecentGif.previewUrl and render
  that for the thumbnail, so recents no longer autoplay. Pre-existing
  recents without a preview fall back to the animated url. Re-send still
  uses the animated url, so the sent m.image is unchanged.

- a11y: the recent buttons all had the identical label "Send recent GIF".
  Give them positional labels ("Send recent GIF N of M") and wrap the grid
  in a role="group" labelled by the "Recent" section heading, so the row
  is a distinguishable, announced group.

Correctness review found no bugs (write-before-unmount, term gating,
dedupe, re-send fidelity all verified).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 12:34:51 -04:00
co-authored by Claude Opus 4.8
parent 5b58e5fe43
commit 9a4796c167
2 changed files with 41 additions and 12 deletions
+39 -12
View File
@@ -19,10 +19,21 @@ type GifPickerInnerProps = {
// Small monospace section header matching the picker's `// GIF_SEARCH` treatment
// (lotusTerminal) / a muted label otherwise.
function SectionLabel({ text, lotusTerminal }: { text: string; lotusTerminal: boolean }) {
const RECENT_LABEL_ID = 'gif-picker-recent-label';
function SectionLabel({
text,
lotusTerminal,
id,
}: {
text: string;
lotusTerminal: boolean;
id?: string;
}) {
if (lotusTerminal) {
return (
<div
id={id}
style={{
padding: '4px 2px',
fontFamily: "'JetBrains Mono', 'Cascadia Code', monospace",
@@ -39,6 +50,7 @@ function SectionLabel({ text, lotusTerminal }: { text: string; lotusTerminal: bo
}
return (
<div
id={id}
style={{
padding: '2px 2px 4px',
fontSize: '11px',
@@ -60,18 +72,22 @@ function RecentGifs({
}: {
recents: RecentGif[];
lotusTerminal: boolean;
onPick: (url: string, width: number, height: number) => void;
onPick: (gif: RecentGif) => void;
}) {
return (
<div style={{ marginBottom: 8 }}>
<SectionLabel text="Recent" lotusTerminal={lotusTerminal} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }}>
{recents.map((g) => (
<SectionLabel text="Recent" lotusTerminal={lotusTerminal} id={RECENT_LABEL_ID} />
<div
role="group"
aria-labelledby={RECENT_LABEL_ID}
style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }}
>
{recents.map((g, i) => (
<button
key={g.url}
type="button"
aria-label="Send recent GIF"
onClick={() => onPick(g.url, g.width, g.height)}
aria-label={`Send recent GIF ${i + 1} of ${recents.length}`}
onClick={() => onPick(g)}
style={{
padding: 0,
border: 'none',
@@ -82,8 +98,10 @@ function RecentGifs({
overflow: 'hidden',
}}
>
{/* A still preview (falls back to the animated url for pre-existing
recents) so the Recent row doesn't autoplay many GIFs at once. */}
<img
src={g.url}
src={g.previewUrl ?? g.url}
alt=""
loading="lazy"
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
@@ -100,9 +118,9 @@ function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInne
const [recents, setRecents] = useAtom(recentGifsAtom);
const sendGif = useCallback(
(url: string, width: number, height: number) => {
setRecents((prev) => addRecentGif(prev, { url, width, height }));
onSelect(url, width, height);
(gif: RecentGif) => {
setRecents((prev) => addRecentGif(prev, gif));
onSelect(gif.url, gif.width, gif.height);
requestClose();
},
[onSelect, requestClose, setRecents],
@@ -112,7 +130,16 @@ function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInne
(gif: IGif, e: React.SyntheticEvent) => {
e.preventDefault();
const r = gif.images.downsized ?? gif.images.original;
sendGif(r.url, Number(r.width) || 200, Number(r.height) || 200);
const previewUrl =
gif.images.fixed_width_small_still?.url ??
gif.images.downsized_still?.url ??
gif.images.original_still?.url;
sendGif({
url: r.url,
width: Number(r.width) || 200,
height: Number(r.height) || 200,
previewUrl,
});
},
[sendGif],
);
+2
View File
@@ -5,6 +5,8 @@ export type RecentGif = {
url: string;
width: number;
height: number;
/** A small still image used for the picker thumbnail (so recents don't all autoplay). */
previewUrl?: string;
};
const STORAGE_KEY = 'cinny_recent_gifs_v1';