From dfcb6981160afb113dc82bf0fb32c51a8d8eaaf5 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sat, 19 Sep 2026 17:48:54 -0400 Subject: [PATCH 01/11] feat(docs): pulsing scroll cue on the catalog Tune panel --- docs/snippets/catalog-detail.jsx | 86 ++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/docs/snippets/catalog-detail.jsx b/docs/snippets/catalog-detail.jsx index 5f42c75ec3..a82707c213 100644 --- a/docs/snippets/catalog-detail.jsx +++ b/docs/snippets/catalog-detail.jsx @@ -668,6 +668,7 @@ export const CatalogDetail = ({ font-weight: 600; } .hf-ve-tune-head small { font-weight: 400; font-size: 13px; color: var(--ve-muted); } +.hf-ve-tune-list-wrap { position: relative; flex: 1; min-height: 0; display: flex; } .hf-ve-tune-list { flex: 1; min-height: 0; @@ -679,6 +680,29 @@ export const CatalogDetail = ({ align-content: start; mask-image: linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 12px), transparent 100%); } +.hf-ve-tune-more { + position: absolute; + left: 50%; + bottom: 6px; + transform: translateX(-50%); + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 9999px; + background: var(--ve-hover); + color: var(--ve-muted); + pointer-events: none; + animation: hf-ve-tune-more-pulse 1.6s ease-in-out infinite; +} +@keyframes hf-ve-tune-more-pulse { + 0%, 100% { opacity: 1; transform: translateX(-50%) scale(1); } + 50% { opacity: 0.55; transform: translateX(-50%) scale(1.15); } +} +@media (prefers-reduced-motion: reduce) { + .hf-ve-tune-more { animation: none; } +} .hf-ve-tune-foot { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); @@ -2054,6 +2078,21 @@ export const CatalogDetail = ({ const adapterMissing = webgpu && hasAdapter === false; // Edits reach a mounted player only, so the panel waits for the probe and goes when the clip stands in. const tunePanel = hasTune && !(webgpu && hasAdapter !== true); + const tuneListRef = useRef(null); + const [tuneMoreBelow, setTuneMoreBelow] = useState(false); + // BEGIN hasMoreBelow: true once unseen content sits past the scrolled viewport, an epsilon short of exact to absorb subpixel rounding. + const hasMoreBelow = (scrollHeight, scrollTop, clientHeight) => scrollHeight - scrollTop - clientHeight > 4; + // END hasMoreBelow + const checkTuneMoreBelow = () => { + const el = tuneListRef.current; + if (el) setTuneMoreBelow(hasMoreBelow(el.scrollHeight, el.scrollTop, el.clientHeight)); + }; + useEffect(() => { + if (!tunePanel) return; + checkTuneMoreBelow(); + window.addEventListener("resize", checkTuneMoreBelow); + return () => window.removeEventListener("resize", checkTuneMoreBelow); + }, [tunePanel, variables, values, notes]); let webgpuStage = player; if (webgpu && hasAdapter === null) webgpuStage =
; if (adapterMissing) webgpuStage = recorded; @@ -2141,24 +2180,39 @@ export const CatalogDetail = ({
Tune {variables.length} {variables.length === 1 ? "variable" : "variables"}
-
- {variables.map((v) => ( -
-
- - {readout(v, values[v.id])} +
+
+ {variables.map((v) => ( +
+
+ + {readout(v, values[v.id])} +
+ {control( + v, + values[v.id], + (next) => setValues((prev) => ({ ...prev, [v.id]: next })), + notes[v.id], + (note) => setNotes((prev) => ({ ...prev, [v.id]: note })), + setTyping, + )} + {v.description &&

{v.description}

}
- {control( - v, - values[v.id], - (next) => setValues((prev) => ({ ...prev, [v.id]: next })), - notes[v.id], - (note) => setNotes((prev) => ({ ...prev, [v.id]: note })), - setTyping, - )} - {v.description &&

{v.description}

} + ))} +
+ {tuneMoreBelow && ( + - ))} + )}