fix: skins QA problems + flow change (#6216)

* fix: skins backend bugs + apply flow

* fix: caching structure

* feat: collapse already duplicated skins + fix moj api spam

* fix: doc

* fix: flatten migrations

* feat: remove default cape/cape override concept

* fix: fmt + lint

* feat: remove SelectCapeModal for inline cape list

* feat: qa

* feat: virtualisation of skins sections + fix texture/model cache

* fix: lint

* fix: virt bugs + renderer fixes

* fix: qa bugs

* fix: doc

* fix: re-add click impulse anim from prototypes + re-add interact anim length cap

* fix: regressions

* devex: split up SkinPreviewrenderer

* fix: lint

* fix: introduce dynamic mode in virtual-scroll.ts

* feat: qa

* fix: nametag bug + remove minecon skin pack suffix

* feat: pain (literally)

* feat: user agent on moj reqs

* feat: impl per account flush queue for operations

* fix: breadcrumb

* chore: i18n pass

* fix: lint + prep + check

* fix: misalignments
This commit is contained in:
Calum H.
2026-05-27 22:22:24 +00:00
committed by GitHub
parent 64edf2ddeb
commit 84b91f32f8
55 changed files with 5651 additions and 2138 deletions
+100 -78
View File
@@ -1,6 +1,11 @@
import type { Ref } from 'vue'
import { computed, ref, watch, watchEffect } from 'vue'
export interface ScrollViewportOptions {
onScroll?: () => void
onResize?: () => void
}
export interface VirtualScrollOptions {
itemHeight: number
bufferSize?: number
@@ -10,45 +15,35 @@ export interface VirtualScrollOptions {
nearEndThreshold?: number
}
export function useVirtualScroll<T>(items: Ref<T[]>, options: VirtualScrollOptions) {
const {
itemHeight,
bufferSize = 5,
initialItemCount = 20,
enabled,
onNearEnd,
nearEndThreshold = 0.2,
} = options
export function findScrollableAncestor(element: HTMLElement | null): HTMLElement | Window {
if (!element) return window
let current: HTMLElement | null = element.parentElement
while (current) {
const { overflowY } = getComputedStyle(current)
if (overflowY === 'auto' || overflowY === 'scroll') {
return current
}
current = current.parentElement
}
return window
}
export function getScrollTop(container: HTMLElement | Window): number {
return container instanceof Window ? window.scrollY : container.scrollTop
}
export function getViewportHeight(container: HTMLElement | Window): number {
return container instanceof Window ? window.innerHeight : container.clientHeight
}
export function useScrollViewport(options: ScrollViewportOptions = {}) {
const listContainer = ref<HTMLElement | null>(null)
const scrollContainer = ref<HTMLElement | Window | null>(null)
const scrollTop = ref(0)
const viewportHeight = ref(0)
const containerOffset = ref(0)
const totalHeight = computed(() => items.value.length * itemHeight)
function findScrollableAncestor(element: HTMLElement | null): HTMLElement | Window {
if (!element) return window
let current: HTMLElement | null = element.parentElement
while (current) {
const { overflowY } = getComputedStyle(current)
if (overflowY === 'auto' || overflowY === 'scroll') {
return current
}
current = current.parentElement
}
return window
}
function getScrollTop(container: HTMLElement | Window): number {
return container instanceof Window ? window.scrollY : container.scrollTop
}
function getViewportHeight(container: HTMLElement | Window): number {
return container instanceof Window ? window.innerHeight : container.clientHeight
}
const relativeScrollTop = computed(() => Math.max(0, scrollTop.value - containerOffset.value))
function updateContainerOffset() {
const listEl = listContainer.value
@@ -71,6 +66,77 @@ export function useVirtualScroll<T>(items: Ref<T[]>, options: VirtualScrollOptio
updateContainerOffset()
}
function handleScroll() {
if (scrollContainer.value) {
scrollTop.value = getScrollTop(scrollContainer.value)
updateContainerOffset()
}
options.onScroll?.()
}
function handleResize() {
syncScrollState()
options.onResize?.()
}
watchEffect((onCleanup) => {
if (typeof window === 'undefined') return
const listEl = listContainer.value
if (!listEl) return
const container = findScrollableAncestor(listEl)
scrollContainer.value = container
syncScrollState()
container.addEventListener('scroll', handleScroll, { passive: true })
window.addEventListener('resize', handleResize, { passive: true })
let resizeObserver: ResizeObserver | undefined
if (!(container instanceof Window)) {
resizeObserver = new ResizeObserver(() => {
syncScrollState()
})
resizeObserver.observe(container)
}
onCleanup(() => {
container.removeEventListener('scroll', handleScroll)
window.removeEventListener('resize', handleResize)
resizeObserver?.disconnect()
})
})
return {
containerOffset,
listContainer,
relativeScrollTop,
scrollContainer,
scrollTop,
syncScrollState,
updateContainerOffset,
viewportHeight,
}
}
export function useVirtualScroll<T>(items: Ref<T[]>, options: VirtualScrollOptions) {
const {
itemHeight,
bufferSize = 5,
initialItemCount = 20,
enabled,
onNearEnd,
nearEndThreshold = 0.2,
} = options
const { listContainer, relativeScrollTop, scrollContainer, syncScrollState, viewportHeight } =
useScrollViewport({
onScroll: checkNearEnd,
})
const totalHeight = computed(() => items.value.length * itemHeight)
const visibleRange = computed(() => {
if (enabled && !enabled.value) {
return { start: 0, end: items.value.length }
@@ -80,9 +146,7 @@ export function useVirtualScroll<T>(items: Ref<T[]>, options: VirtualScrollOptio
return { start: 0, end: Math.min(items.value.length, initialItemCount) }
}
const relativeScrollTop = Math.max(0, scrollTop.value - containerOffset.value)
const start = Math.floor(relativeScrollTop / itemHeight)
const start = Math.floor(relativeScrollTop.value / itemHeight)
const visibleCount = Math.ceil(viewportHeight.value / itemHeight)
const rangeSize = visibleCount + bufferSize * 2
@@ -117,52 +181,10 @@ export function useVirtualScroll<T>(items: Ref<T[]>, options: VirtualScrollOptio
}
}
function handleScroll() {
if (scrollContainer.value) {
scrollTop.value = getScrollTop(scrollContainer.value)
updateContainerOffset()
}
checkNearEnd()
}
function handleResize() {
syncScrollState()
}
// Re-sync scroll state when items change to avoid stale scrollTop/offset
watch(items, () => {
syncScrollState()
})
watchEffect((onCleanup) => {
if (typeof window === 'undefined') return
const listEl = listContainer.value
if (!listEl) return
const container = findScrollableAncestor(listEl)
scrollContainer.value = container
syncScrollState()
container.addEventListener('scroll', handleScroll, { passive: true })
window.addEventListener('resize', handleResize, { passive: true })
// Use ResizeObserver for element scroll containers
let resizeObserver: ResizeObserver | undefined
if (!(container instanceof Window)) {
resizeObserver = new ResizeObserver(() => {
syncScrollState()
})
resizeObserver.observe(container)
}
onCleanup(() => {
container.removeEventListener('scroll', handleScroll)
window.removeEventListener('resize', handleResize)
resizeObserver?.disconnect()
})
})
return {
listContainer,
totalHeight,