Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1998fb23c | |||
| 8e611a11e6 |
@@ -4,3 +4,7 @@ dist-ssr
|
|||||||
.vite
|
.vite
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
tsconfig.node.tsbuildinfo
|
||||||
|
tsconfig.app.tsbuildinfo
|
||||||
|
upload_koodo.py
|
||||||
|
|||||||
+98
-13
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useRef, useState } from "react";
|
import { useMemo, useRef, useState, type AnimationEvent } from "react";
|
||||||
import logoUrl from "./assets/logo-universal.png";
|
import logoUrl from "./assets/logo-universal.png";
|
||||||
import ShowcaseVisual from "./ShowcaseVisual";
|
import ShowcaseVisual from "./ShowcaseVisual";
|
||||||
|
|
||||||
@@ -33,6 +33,13 @@ const slides = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type SlideDirection = -1 | 1;
|
||||||
|
|
||||||
|
type SlideTransition = {
|
||||||
|
from: number;
|
||||||
|
direction: SlideDirection;
|
||||||
|
};
|
||||||
|
|
||||||
function Logo() {
|
function Logo() {
|
||||||
return (
|
return (
|
||||||
<div className="brand" aria-label="SnapGo logo">
|
<div className="brand" aria-label="SnapGo logo">
|
||||||
@@ -49,9 +56,40 @@ function Logo() {
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [activeSlide, setActiveSlide] = useState(0);
|
const [activeSlide, setActiveSlide] = useState(0);
|
||||||
|
const [slideTransition, setSlideTransition] = useState<SlideTransition | null>(null);
|
||||||
const dragStartX = useRef<number | null>(null);
|
const dragStartX = useRef<number | null>(null);
|
||||||
const active = slides[activeSlide];
|
const active = slides[activeSlide];
|
||||||
|
|
||||||
|
function getTransitionClass(role: "entering" | "exiting") {
|
||||||
|
if (!slideTransition) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const edge = slideTransition.direction === 1 ? "right" : "left";
|
||||||
|
return `is-${role}-from-${edge}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishSlideTransition(event: AnimationEvent<HTMLElement>) {
|
||||||
|
if (event.target === event.currentTarget) {
|
||||||
|
setSlideTransition(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSlide(nextSlide: number, direction: SlideDirection) {
|
||||||
|
if (slideTransition || nextSlide === activeSlide) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSlideTransition({ from: activeSlide, direction });
|
||||||
|
setActiveSlide(nextSlide);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDotSlide(nextSlide: number) {
|
||||||
|
const forwardDistance = (nextSlide - activeSlide + slides.length) % slides.length;
|
||||||
|
const backwardDistance = (activeSlide - nextSlide + slides.length) % slides.length;
|
||||||
|
selectSlide(nextSlide, forwardDistance <= backwardDistance ? 1 : -1);
|
||||||
|
}
|
||||||
|
|
||||||
const accentDots = useMemo(
|
const accentDots = useMemo(
|
||||||
() =>
|
() =>
|
||||||
slides.map((slide, index) => (
|
slides.map((slide, index) => (
|
||||||
@@ -60,14 +98,15 @@ export default function App() {
|
|||||||
className={`dot ${index === activeSlide ? "is-active" : ""}`}
|
className={`dot ${index === activeSlide ? "is-active" : ""}`}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`查看 ${slide.title}`}
|
aria-label={`查看 ${slide.title}`}
|
||||||
onClick={() => setActiveSlide(index)}
|
onClick={() => selectDotSlide(index)}
|
||||||
/>
|
/>
|
||||||
)),
|
)),
|
||||||
[activeSlide],
|
[activeSlide, slideTransition],
|
||||||
);
|
);
|
||||||
|
|
||||||
function moveSlide(direction: -1 | 1) {
|
function moveSlide(direction: SlideDirection) {
|
||||||
setActiveSlide((current) => (current + direction + slides.length) % slides.length);
|
const nextSlide = (activeSlide + direction + slides.length) % slides.length;
|
||||||
|
selectSlide(nextSlide, direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePointerEnd(clientX: number) {
|
function handlePointerEnd(clientX: number) {
|
||||||
@@ -148,13 +187,44 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="preview-stage">
|
<div className="preview-stage">
|
||||||
<ShowcaseVisual key={active.eyebrow} activeIndex={activeSlide} />
|
{slideTransition && (
|
||||||
|
<div
|
||||||
|
key={`visual-${slideTransition.from}`}
|
||||||
|
className={`carousel-slide is-exiting ${getTransitionClass("exiting")}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<ShowcaseVisual activeIndex={slideTransition.from} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
key={`visual-${activeSlide}`}
|
||||||
|
className={`carousel-slide ${slideTransition ? `is-entering ${getTransitionClass("entering")}` : ""}`}
|
||||||
|
onAnimationEnd={finishSlideTransition}
|
||||||
|
>
|
||||||
|
<ShowcaseVisual activeIndex={activeSlide} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="slide-panel" key={active.title}>
|
<div className="slide-panel-viewport">
|
||||||
<p>{active.eyebrow}</p>
|
{slideTransition && (
|
||||||
<h2>{active.title}</h2>
|
<div
|
||||||
<span>{active.body}</span>
|
key={`copy-${slideTransition.from}`}
|
||||||
|
className={`slide-panel is-exiting ${getTransitionClass("exiting")}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<p>{slides[slideTransition.from].eyebrow}</p>
|
||||||
|
<h2>{slides[slideTransition.from].title}</h2>
|
||||||
|
<span>{slides[slideTransition.from].body}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
key={`copy-${activeSlide}`}
|
||||||
|
className={`slide-panel ${slideTransition ? `is-entering ${getTransitionClass("entering")}` : ""}`}
|
||||||
|
>
|
||||||
|
<p>{active.eyebrow}</p>
|
||||||
|
<h2>{active.title}</h2>
|
||||||
|
<span>{active.body}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -164,9 +234,24 @@ export default function App() {
|
|||||||
<path d="m12 5-5 5 5 5" />
|
<path d="m12 5-5 5 5 5" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div className="slide-meta" aria-live="polite">
|
<div className="slide-meta-viewport" aria-live="polite">
|
||||||
<strong>{active.metric}</strong>
|
{slideTransition && (
|
||||||
<span>{active.feature}</span>
|
<div
|
||||||
|
key={`meta-${slideTransition.from}`}
|
||||||
|
className={`slide-meta is-exiting ${getTransitionClass("exiting")}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<strong>{slides[slideTransition.from].metric}</strong>
|
||||||
|
<span>{slides[slideTransition.from].feature}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
key={`meta-${activeSlide}`}
|
||||||
|
className={`slide-meta ${slideTransition ? `is-entering ${getTransitionClass("entering")}` : ""}`}
|
||||||
|
>
|
||||||
|
<strong>{active.metric}</strong>
|
||||||
|
<span>{active.feature}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dots" aria-label="轮播分页">
|
<div className="dots" aria-label="轮播分页">
|
||||||
{accentDots}
|
{accentDots}
|
||||||
|
|||||||
+81
-23
@@ -339,8 +339,6 @@ h1 {
|
|||||||
.preview-stage {
|
.preview-stage {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 74px clamp(22px, 4vw, 52px) 154px;
|
inset: 74px clamp(22px, 4vw, 52px) 154px;
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 34px;
|
border-radius: 34px;
|
||||||
background:
|
background:
|
||||||
@@ -350,6 +348,14 @@ h1 {
|
|||||||
background-size: 34px 34px, 34px 34px, auto;
|
background-size: 34px 34px, 34px 34px, auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.carousel-slide {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
.scene-parallax {
|
.scene-parallax {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -361,7 +367,6 @@ h1 {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: #142033;
|
color: #142033;
|
||||||
animation: scene-enter 360ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.visual-scene button {
|
.visual-scene button {
|
||||||
@@ -1623,13 +1628,20 @@ h1 {
|
|||||||
box-shadow: 0 0 0 1px rgba(38, 52, 73, 0.12);
|
box-shadow: 0 0 0 1px rgba(38, 52, 73, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-panel {
|
.slide-panel-viewport {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: clamp(22px, 4vw, 52px);
|
right: clamp(22px, 4vw, 52px);
|
||||||
bottom: 34px;
|
bottom: 34px;
|
||||||
left: clamp(22px, 4vw, 52px);
|
left: clamp(22px, 4vw, 52px);
|
||||||
|
display: grid;
|
||||||
min-height: 92px;
|
min-height: 92px;
|
||||||
animation: fade-slide 260ms ease both;
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-panel {
|
||||||
|
position: relative;
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-panel p {
|
.slide-panel p {
|
||||||
@@ -1687,12 +1699,22 @@ h1 {
|
|||||||
background 180ms ease;
|
background 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slide-meta-viewport {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
height: 28px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.slide-meta {
|
.slide-meta {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-meta strong {
|
.slide-meta strong {
|
||||||
@@ -1733,15 +1755,59 @@ h1 {
|
|||||||
background: #0a84ff;
|
background: #0a84ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scene-enter {
|
.is-entering-from-right {
|
||||||
|
animation: carousel-enter-from-right 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-exiting-from-right {
|
||||||
|
animation: carousel-exit-to-left 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-entering-from-left {
|
||||||
|
animation: carousel-enter-from-left 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-exiting-from-left {
|
||||||
|
animation: carousel-exit-to-right 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes carousel-enter-from-right {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
transform: translate3d(100%, 0, 0);
|
||||||
transform: translateY(9px) scale(0.985);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
transform: translate3d(0, 0, 0);
|
||||||
transform: translateY(0) scale(1);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes carousel-exit-to-left {
|
||||||
|
from {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translate3d(-100%, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes carousel-enter-from-left {
|
||||||
|
from {
|
||||||
|
transform: translate3d(-100%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes carousel-exit-to-right {
|
||||||
|
from {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translate3d(100%, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2020,18 +2086,6 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-slide {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
@@ -2112,7 +2166,7 @@ h1 {
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-panel {
|
.slide-panel-viewport {
|
||||||
right: 22px;
|
right: 22px;
|
||||||
bottom: 22px;
|
bottom: 22px;
|
||||||
left: 22px;
|
left: 22px;
|
||||||
@@ -2535,6 +2589,10 @@ h1 {
|
|||||||
gap: 1px;
|
gap: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slide-meta-viewport {
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
.slide-meta strong {
|
.slide-meta strong {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user