fix: stabilize portfolio scrolling and loading
Some checks failed
Test and deploy / Verify production build (push) Has been cancelled
Test and deploy / Deploy production (push) Has been cancelled

Remove skeleton swaps and deferred layout estimates, restore reveal state, and use native proximity scrolling for project cards. Also make the Docker build npm registry configurable.
This commit is contained in:
2026-09-09 17:53:48 +03:30
parent f34bea05cd
commit b3cfcfe057
27 changed files with 180 additions and 567 deletions

View File

@@ -269,7 +269,7 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
};
};
export function AboutRiverScene({ onReady }: { onReady?: () => void }) {
export function AboutRiverScene() {
const { theme } = useTheme();
return <ThreeCanvas className={styles.aboutScene} createScene={createAboutScene} theme={theme} onReady={onReady} />;
return <ThreeCanvas className={styles.aboutScene} createScene={createAboutScene} theme={theme} />;
}

View File

@@ -10,7 +10,6 @@ const AboutRiverScene = lazy(() =>
export function DeferredAboutRiverScene() {
const hostRef = useRef<HTMLDivElement>(null);
const [shouldLoad, setShouldLoad] = useState(false);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const host = hostRef.current;
@@ -28,11 +27,10 @@ export function DeferredAboutRiverScene() {
}, [shouldLoad]);
return (
<div ref={hostRef} className={styles.aboutSceneBoundary} aria-busy={!isReady} data-looping-motion>
{!isReady && <span className={styles.sceneSkeleton} aria-hidden="true" />}
<div ref={hostRef} className={styles.aboutSceneBoundary}>
{shouldLoad && (
<Suspense fallback={null}>
<AboutRiverScene onReady={() => setIsReady(true)} />
<AboutRiverScene />
</Suspense>
)}
</div>

View File

@@ -51,22 +51,6 @@
overflow: hidden;
}
.sceneSkeleton {
position: absolute;
inset: 0;
background:
linear-gradient(100deg, transparent 24%, color-mix(in srgb, var(--bg-raised) 68%, transparent) 50%, transparent 76%),
var(--bg-page);
background-size: 200% 100%, auto;
animation: sceneSkeletonShimmer 1.5s ease-in-out infinite;
}
@keyframes sceneSkeletonShimmer {
to {
background-position: -200% 0, 0 0;
}
}
.aboutScene::after {
content: '';
position: absolute;
@@ -114,9 +98,3 @@
width: 100vw;
}
}
@media (prefers-reduced-motion: reduce) {
.sceneSkeleton {
animation: none;
}
}

View File

@@ -26,7 +26,6 @@ interface ThreeCanvasProps {
className: string;
createScene: SceneFactory;
theme: SceneTheme;
onReady?: () => void;
}
function disposeObject(object: THREE.Object3D) {
@@ -50,13 +49,8 @@ function disposeObject(object: THREE.Object3D) {
});
}
export function ThreeCanvas({ className, createScene, theme, onReady }: ThreeCanvasProps) {
export function ThreeCanvas({ className, createScene, theme }: ThreeCanvasProps) {
const hostRef = useRef<HTMLDivElement>(null);
const onReadyRef = useRef(onReady);
useEffect(() => {
onReadyRef.current = onReady;
}, [onReady]);
useEffect(() => {
const host = hostRef.current;
@@ -70,7 +64,6 @@ export function ThreeCanvas({ className, createScene, theme, onReady }: ThreeCan
let frame = 0;
let visible = true;
let pageVisible = !document.hidden;
let hasRendered = false;
renderer.setClearColor(0x000000, 0);
renderer.outputColorSpace = THREE.SRGBColorSpace;
@@ -81,10 +74,6 @@ export function ThreeCanvas({ className, createScene, theme, onReady }: ThreeCan
const render = () => {
renderer.render(controller.scene, controller.camera);
if (!hasRendered) {
hasRendered = true;
onReadyRef.current?.();
}
};
const resize = () => {
const bounds = host.getBoundingClientRect();