Stop the library grid from fighting scroll-up by dropping content-visibility and matching virtual row height to the cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-02 21:05:32 -05:00
co-authored by Cursor
parent 71e2e4b326
commit 642bffeaaa
2 changed files with 21 additions and 17 deletions
-9
View File
@@ -106,15 +106,6 @@ textarea {
#09090b; #09090b;
} }
.library-thumb {
content-visibility: auto;
contain-intrinsic-size: auto 15rem;
}
.studio-library-card {
contain: none;
}
.family-nav { .family-nav {
display: flex; display: flex;
height: 2.75rem; height: 2.75rem;
+21 -8
View File
@@ -136,7 +136,7 @@
<div <div
v-else v-else
ref="scroller" ref="scroller"
class="mt-3 min-h-[24rem] flex-1 overflow-y-auto" class="mt-3 min-h-[24rem] flex-1 overflow-y-auto overscroll-y-contain [overflow-anchor:none]"
@scroll="onScroll" @scroll="onScroll"
> >
<div :style="{ height: `${gridHeight}px`, position: 'relative' }"> <div :style="{ height: `${gridHeight}px`, position: 'relative' }">
@@ -210,7 +210,7 @@
</section> </section>
<aside <aside
class="glass overflow-y-auto p-4 sm:p-5" class="glass overflow-y-auto overscroll-y-contain [overflow-anchor:none] p-4 sm:p-5"
:class="focused :class="focused
? 'fixed inset-0 z-[60] rounded-none lg:static lg:z-auto lg:h-full lg:w-[min(38rem,42vw)] lg:shrink-0 lg:rounded-3xl' ? 'fixed inset-0 z-[60] rounded-none lg:static lg:z-auto lg:h-full lg:w-[min(38rem,42vw)] lg:shrink-0 lg:rounded-3xl'
: 'hidden w-full shrink-0 rounded-3xl lg:block lg:h-full lg:w-[22rem]'" : 'hidden w-full shrink-0 rounded-3xl lg:block lg:h-full lg:w-[22rem]'"
@@ -453,8 +453,8 @@ import { clipSettingsRows, stillSettingsRows } from '~/utils/libraryGeneration'
const CARD_MIN = 156 const CARD_MIN = 156
const GAP = 10 const GAP = 10
const ROW_H = 248 const CARD_CAPTION = 50
const BUFFER_ROWS = 3 const BUFFER_ROWS = 4
const DRAG_MIME = 'application/x-aigen-library' const DRAG_MIME = 'application/x-aigen-library'
const filterOptions = [ const filterOptions = [
{ id: 'all' as const, label: 'All' }, { id: 'all' as const, label: 'All' },
@@ -555,11 +555,16 @@ const emptyLabel = computed(() => {
return 'Nothing in this folder yet.' return 'Nothing in this folder yet.'
}) })
const cols = computed(() => Math.max(2, Math.floor((viewportW.value + GAP) / (CARD_MIN + GAP)))) const cols = computed(() => Math.max(2, Math.floor((viewportW.value + GAP) / (CARD_MIN + GAP))))
const rowH = computed(() => {
const width = Math.max(CARD_MIN, (viewportW.value - GAP * (cols.value - 1)) / cols.value)
return Math.round(width * 5 / 4) + CARD_CAPTION
})
const rowStride = computed(() => rowH.value + GAP)
const rowCount = computed(() => Math.ceil(entries.value.length / cols.value) || 0) const rowCount = computed(() => Math.ceil(entries.value.length / cols.value) || 0)
const gridHeight = computed(() => rowCount.value * ROW_H + Math.max(0, rowCount.value - 1) * GAP) const gridHeight = computed(() => rowCount.value * rowH.value + Math.max(0, rowCount.value - 1) * GAP)
const startRow = computed(() => Math.max(0, Math.floor(scrollTop.value / (ROW_H + GAP)) - BUFFER_ROWS)) const startRow = computed(() => Math.max(0, Math.floor(scrollTop.value / rowStride.value) - BUFFER_ROWS))
const visibleRowCount = computed(() => Math.ceil(viewportH.value / (ROW_H + GAP)) + BUFFER_ROWS * 2) const visibleRowCount = computed(() => Math.ceil(viewportH.value / rowStride.value) + BUFFER_ROWS * 2)
const offsetY = computed(() => startRow.value * (ROW_H + GAP)) const offsetY = computed(() => startRow.value * rowStride.value)
const visibleEntries = computed(() => { const visibleEntries = computed(() => {
const start = startRow.value * cols.value const start = startRow.value * cols.value
const end = start + visibleRowCount.value * cols.value const end = start + visibleRowCount.value * cols.value
@@ -1100,6 +1105,14 @@ watch([folderId, filter], () => {
watch(search, () => { watch(search, () => {
if (scroller.value) scroller.value.scrollTop = 0 if (scroller.value) scroller.value.scrollTop = 0
}) })
watch([cols, rowH], ([nextCols, nextRowH], [prevCols, prevRowH]) => {
const el = scroller.value
if (!el || !prevRowH) return
if (nextCols === prevCols && nextRowH === prevRowH) return
const index = Math.floor(el.scrollTop / (prevRowH + GAP)) * prevCols
el.scrollTop = Math.floor(index / nextCols) * (nextRowH + GAP)
scrollTop.value = el.scrollTop
})
watch(focusedKey, (key) => { watch(focusedKey, (key) => {
if (!key) { if (!key) {
detailsHidden.value = false detailsHidden.value = false