chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,147 @@
'use client'
import { type RefObject, useEffect, useLayoutEffect, useRef } from 'react'
import { usePathname } from 'next/navigation'
/** Namespace prefix so restoration keys never collide with other tab state. */
const STORAGE_PREFIX = 'integrations-scroll:' as const
/**
* True when the most recent navigation was a Back/Forward history traversal.
*
* A single module-level `popstate` listener flips this before the destination
* page mounts; each restore reads it once and clears it. Fresh push navigations
* (a sidebar link, a typed URL) never fire `popstate`, so the flag stays false
* and those visits open at the top instead of jumping to a stale position.
* Registered at module load — a per-hook listener would attach too late to see
* the `popstate` that triggered its own mount.
*/
let lastNavWasTraversal = false
if (typeof window !== 'undefined') {
window.addEventListener('popstate', () => {
lastNavWasTraversal = true
})
}
interface UseScrollRestorationOptions {
/**
* Flag that flips to `true` once the async content the scroll height depends
* on has settled (e.g. the credentials query is no longer pending). A late
* restore is re-attempted when this transitions so we do not clamp against a
* too-short container while data is still loading.
*/
ready?: boolean
}
/**
* Restores the scroll position of an inner scroll container across browser
* Back/Forward navigation within the same tab.
*
* Next.js App Router only restores WINDOW scroll, so a page whose content
* scrolls inside a nested `overflow-y-auto` element loses its position on Back.
* This hook persists `scrollTop` in `sessionStorage` (per-pathname, tab-scoped)
* and re-applies it — only on history traversals — once the container has laid
* out.
*
* Programmatic vs. user scrolls are told apart by VALUE, not a one-shot event
* flag: a restore assigns `scrollTop === lastAppliedRef`, so the echoed scroll
* event compares equal and is ignored (it is never persisted, so a clamped
* value cannot overwrite the saved target). This avoids the race where the
* programmatic scroll event fires before the listener attaches and a stuck flag
* drops the user's first real scroll. The restore itself latches only on a full
* (non-clamped) apply, so a late `ready` retry can complete a position that was
* clamped against still-loading content, and it stops the moment the user
* scrolls away from the last applied value so it never fights them.
*
* @param containerRef Ref to the scrollable container element.
* @param options `ready` marks async content as settled for a late retry.
*/
export function useScrollRestoration(
containerRef: RefObject<HTMLDivElement | null>,
{ ready = true }: UseScrollRestorationOptions = {}
): void {
const pathname = usePathname()
const storageKey = `${STORAGE_PREFIX}${pathname}`
const storageKeyRef = useRef(storageKey)
const hasRestoredRef = useRef(false)
/** Last `scrollTop` this hook assigned, so its echo scroll event is ignored. */
const lastAppliedRef = useRef(-1)
/** Latest user-initiated `scrollTop`, flushed to storage on unmount. */
const latestUserScrollRef = useRef<number | null>(null)
const rafRef = useRef<number | null>(null)
/** Captured once per mount: did we arrive here via Back/Forward? */
const shouldRestoreRef = useRef<boolean | null>(null)
useEffect(() => {
storageKeyRef.current = storageKey
}, [storageKey])
useEffect(() => {
const el = containerRef.current
if (!el) return
const persist = (value: number) => {
try {
sessionStorage.setItem(storageKeyRef.current, String(value))
} catch {}
}
const onScroll = () => {
if (el.scrollTop === lastAppliedRef.current) return
latestUserScrollRef.current = el.scrollTop
if (rafRef.current !== null) return
rafRef.current = requestAnimationFrame(() => {
rafRef.current = null
persist(el.scrollTop)
})
}
el.addEventListener('scroll', onScroll, { passive: true })
return () => {
el.removeEventListener('scroll', onScroll)
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current)
rafRef.current = null
}
if (latestUserScrollRef.current !== null) persist(latestUserScrollRef.current)
}
}, [containerRef])
useLayoutEffect(() => {
const el = containerRef.current
if (!el || hasRestoredRef.current) return
if (shouldRestoreRef.current === null) {
shouldRestoreRef.current = lastNavWasTraversal
lastNavWasTraversal = false
}
if (!shouldRestoreRef.current) {
hasRestoredRef.current = true
return
}
if (lastAppliedRef.current !== -1 && el.scrollTop !== lastAppliedRef.current) {
hasRestoredRef.current = true
return
}
let target = 0
try {
const raw = sessionStorage.getItem(storageKeyRef.current)
target = raw ? Number(raw) : 0
} catch {}
if (!Number.isFinite(target) || target <= 0) {
hasRestoredRef.current = true
return
}
const maxScroll = el.scrollHeight - el.clientHeight
if (maxScroll <= 0) return
lastAppliedRef.current = Math.min(target, maxScroll)
el.scrollTop = lastAppliedRef.current
if (maxScroll >= target) hasRestoredRef.current = true
}, [containerRef, ready])
}