From 93d3d5fe0ae61707937d92b4d4395abb4c09c8fb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 21 Sep 2026 18:27:53 +0000
Subject: [PATCH 1/7] Initial plan
From 2327c928388bf8b3af6ef654097b5be62bd49183 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 21 Sep 2026 18:30:33 +0000
Subject: [PATCH 2/7] feat: add homepage motion and route transitions
Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
---
components/Layout/MainNavigator.tsx | 15 ++---
components/Layout/Section.tsx | 6 +-
components/Member/Card.tsx | 6 +-
components/Project/NewCard.tsx | 6 +-
components/TransitionLink.tsx | 68 +++++++++++++++++++
pages/_document.tsx | 20 ++++++
pages/index.tsx | 35 +++++-----
styles/main.css | 101 ++++++++++++++++++++++++++++
8 files changed, 226 insertions(+), 31 deletions(-)
create mode 100644 components/TransitionLink.tsx
diff --git a/components/Layout/MainNavigator.tsx b/components/Layout/MainNavigator.tsx
index 8bff0fc..c5bc070 100644
--- a/components/Layout/MainNavigator.tsx
+++ b/components/Layout/MainNavigator.tsx
@@ -1,9 +1,8 @@
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import { ObservedComponent } from 'mobx-react-helper';
-import Link from 'next/link';
-
import { i18n, I18nContext, LanguageName } from '../../models/Translation';
+import { TransitionLink } from '../TransitionLink';
import { SymbolIcon } from '../Icon';
import { Button } from '../ui/button';
import {
@@ -28,9 +27,9 @@ export class MainNavigator extends ObservedComponent<{ menu: MenuLink[] }, typeo
renderLinks = () =>
this.props.menu.map(({ title, href, target }) => (
));
@@ -83,18 +82,18 @@ export class MainNavigator extends ObservedComponent<{ menu: MenuLink[] }, typeo
-
+
idea2app
-
+
{this.renderI18nSwitch()}
diff --git a/components/Layout/Section.tsx b/components/Layout/Section.tsx
index 163045a..4771ab0 100644
--- a/components/Layout/Section.tsx
+++ b/components/Layout/Section.tsx
@@ -1,8 +1,8 @@
import { observer } from 'mobx-react';
-import Link from 'next/link';
import { FC, PropsWithChildren, useContext } from 'react';
import { I18nContext } from '../../models/Translation';
+import { TransitionLink } from '../TransitionLink';
import { Button } from '../ui/button';
export type SectionProps = PropsWithChildren<
@@ -24,9 +24,9 @@ export const Section: FC
= observer(
{link && (
)}
diff --git a/components/Member/Card.tsx b/components/Member/Card.tsx
index fed8fc7..ac1bd13 100644
--- a/components/Member/Card.tsx
+++ b/components/Member/Card.tsx
@@ -1,10 +1,10 @@
import { marked } from 'marked';
import { observer } from 'mobx-react';
-import Link from 'next/link';
import { ComponentProps, FC } from 'react';
import { Member } from '../../models/Member';
import { GithubIcon } from '../Layout/Svg';
+import { TransitionLink } from '../TransitionLink';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
import { Card } from '../ui/card';
@@ -32,14 +32,14 @@ export const MemberCard: FC = observer(
{github && (
-
+
-
+
)}
{String(nickname)}
diff --git a/components/Project/NewCard.tsx b/components/Project/NewCard.tsx
index 48b6690..5092e89 100644
--- a/components/Project/NewCard.tsx
+++ b/components/Project/NewCard.tsx
@@ -1,10 +1,10 @@
import { Project } from '@idea2app/data-server';
import { observer } from 'mobx-react';
-import Link from 'next/link';
import { FC, useContext } from 'react';
import { I18nContext } from '../../models/Translation';
import type zhCN from '../../translation/zh-CN';
+import { TransitionLink } from '../TransitionLink';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '../ui/card';
@@ -43,7 +43,9 @@ export const ProjectCard: FC = observer(({ id, name, status = 0 }) => {
diff --git a/components/TransitionLink.tsx b/components/TransitionLink.tsx
new file mode 100644
index 0000000..de54fcd
--- /dev/null
+++ b/components/TransitionLink.tsx
@@ -0,0 +1,68 @@
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { ComponentProps, MouseEvent } from 'react';
+
+type ViewTransitionDocument = Document & {
+ startViewTransition?: (updateCallback: () => Promise | unknown) => void;
+};
+
+type TransitionLinkProps = ComponentProps;
+
+export function TransitionLink({
+ href,
+ replace,
+ scroll,
+ shallow,
+ locale,
+ onClick,
+ target,
+ children,
+ ...props
+}: TransitionLinkProps) {
+ const router = useRouter();
+
+ const handleClick = (event: MouseEvent) => {
+ onClick?.(event);
+
+ if (
+ event.defaultPrevented ||
+ event.button !== 0 ||
+ event.metaKey ||
+ event.ctrlKey ||
+ event.shiftKey ||
+ event.altKey ||
+ target ||
+ typeof href !== 'string' ||
+ !href.startsWith('/')
+ )
+ return;
+
+ const documentWithTransition = document as ViewTransitionDocument;
+
+ if (!documentWithTransition.startViewTransition || href === router.asPath) return;
+
+ event.preventDefault();
+
+ const navigate = () =>
+ replace
+ ? router.replace(href, undefined, { scroll, shallow, locale })
+ : router.push(href, undefined, { scroll, shallow, locale });
+
+ documentWithTransition.startViewTransition(() => navigate());
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 1a684cb..5c30ddf 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -26,6 +26,25 @@ const organizationJsonLd = {
email: 'contact@idea2app.cn',
};
+const speculationRules = {
+ prerender: [
+ {
+ where: {
+ and: [{ href_matches: '/*' }, { not: { href_matches: '/dashboard*' } }],
+ },
+ eagerness: 'moderate',
+ },
+ ],
+ prefetch: [
+ {
+ where: {
+ and: [{ href_matches: '/*' }, { not: { href_matches: '/dashboard*' } }],
+ },
+ eagerness: 'moderate',
+ },
+ ],
+};
+
interface CustomDocumentProps {
language: LanguageCode;
colorScheme: ColorScheme;
@@ -67,6 +86,7 @@ export default class CustomDocument extends Document {
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=attach_file_add,chat,code,dark_mode,diversity_3,fingerprint,key,keyboard_arrow_down,language,light_mode,menu,translate,trending_up,visibility&display=swap"
/>
+
diff --git a/pages/index.tsx b/pages/index.tsx
index 5c6501d..e1ca0f9 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -5,6 +5,7 @@ import { FC, useContext } from 'react';
import { Minute, Second } from 'web-utility';
import { Button } from '@/components/ui/button';
+import { TransitionLink } from '@/components/TransitionLink';
import { PartnerOverview } from '../components/Client/Partner';
import { GitListLayout } from '../components/Git';
import { SymbolIcon } from '../components/Icon';
@@ -54,11 +55,13 @@ const HomePage: FC = observer(({ repositories, members }) => {
-
-
-
+
+
+
+
+
-
+
{t('idea2app_summary')}
{t('idea2app_slogan')}
@@ -70,7 +73,7 @@ const HomePage: FC = observer(({ repositories, members }) => {
{service(i18n).map(({ title, summary, icon, buttonText, buttonLink }) => (
@@ -80,13 +83,13 @@ const HomePage: FC = observer(({ repositories, members }) => {
{summary}
))}
@@ -95,7 +98,7 @@ const HomePage: FC = observer(({ repositories, members }) => {
-
+
{/**
* @todo: polish the carousel animation
*/}
@@ -114,7 +117,7 @@ const HomePage: FC = observer(({ repositories, members }) => {
-
+
{members.map(item => (
-
@@ -127,7 +130,9 @@ const HomePage: FC = observer(({ repositories, members }) => {
);
diff --git a/styles/main.css b/styles/main.css
index 5494951..d22b432 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -30,4 +30,105 @@ div:has(.next-error-h1) {
@utility scrollbar-none {
scrollbar-width: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ html {
+ scroll-behavior: smooth;
+ }
+
+ ::view-transition-old(root),
+ ::view-transition-new(root) {
+ animation-duration: 0.35s;
+ animation-timing-function: ease;
+ }
+
+ ::view-transition-old(root) {
+ animation-name: fade-out;
+ }
+
+ ::view-transition-new(root) {
+ animation-name: fade-in;
+ }
+
+ .landing-hero-logo,
+ .landing-hero-copy,
+ .landing-service-card,
+ .landing-section {
+ animation: rise-in 0.7s ease both;
+ }
+
+ .landing-hero-copy {
+ animation-delay: 0.08s;
+ }
+
+ .landing-service-card:nth-child(2) {
+ animation-delay: 0.12s;
+ }
+
+ .landing-service-card:nth-child(3) {
+ animation-delay: 0.18s;
+ }
+
+ .partner-marquee:hover .animate-carousel-scroll {
+ animation-play-state: paused;
+ }
+
+ .landing-service-card {
+ transition:
+ transform 0.25s ease,
+ box-shadow 0.25s ease,
+ background-color 0.25s ease;
+ }
+
+ .landing-service-card:hover {
+ transform: translateY(-0.375rem);
+ box-shadow: 0 1rem 2.5rem color-mix(in srgb, currentColor 10%, transparent);
+ }
+
+ .animate-carousel-scroll {
+ animation: carousel-scroll 24s linear infinite;
+ }
+}
+
+@keyframes rise-in {
+ from {
+ opacity: 0;
+ transform: translateY(1.5rem);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes fade-in {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes fade-out {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ }
+}
+
+@keyframes carousel-scroll {
+ from {
+ transform: translateX(0);
+ }
+
+ to {
+ transform: translateX(-100%);
+ }
}
\ No newline at end of file
From f3c48074f54f2f6f20192c7843506170f8588b6b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 21 Sep 2026 18:31:56 +0000
Subject: [PATCH 3/7] fix: refine motion behavior and prefetch scope
Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
---
components/TransitionLink.tsx | 7 ++++++-
pages/_document.tsx | 8 ++++++--
pages/index.tsx | 4 ++--
styles/main.css | 13 +++++++------
4 files changed, 21 insertions(+), 11 deletions(-)
diff --git a/components/TransitionLink.tsx b/components/TransitionLink.tsx
index de54fcd..c247dee 100644
--- a/components/TransitionLink.tsx
+++ b/components/TransitionLink.tsx
@@ -39,7 +39,12 @@ export function TransitionLink({
const documentWithTransition = document as ViewTransitionDocument;
- if (!documentWithTransition.startViewTransition || href === router.asPath) return;
+ if (!documentWithTransition.startViewTransition) return;
+
+ const nextURL = new URL(href, location.href),
+ currentURL = new URL(router.asPath, location.href);
+
+ if (nextURL.href === currentURL.href) return;
event.preventDefault();
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 5c30ddf..e9d66b9 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -30,7 +30,7 @@ const speculationRules = {
prerender: [
{
where: {
- and: [{ href_matches: '/*' }, { not: { href_matches: '/dashboard*' } }],
+ or: [{ href_matches: '/member*' }, { href_matches: '/open-source*' }],
},
eagerness: 'moderate',
},
@@ -38,7 +38,11 @@ const speculationRules = {
prefetch: [
{
where: {
- and: [{ href_matches: '/*' }, { not: { href_matches: '/dashboard*' } }],
+ or: [
+ { href_matches: '/member*' },
+ { href_matches: '/open-source*' },
+ { href_matches: '/project*' },
+ ],
},
eagerness: 'moderate',
},
diff --git a/pages/index.tsx b/pages/index.tsx
index e1ca0f9..d21f5fd 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -98,14 +98,14 @@ const HomePage: FC = observer(({ repositories, members }) => {
-
+
{/**
* @todo: polish the carousel animation
*/}
{Array.from({ length: 3 }).map((_, index) => (
-
{PARTNERS_INFO(i18n).map(({ name, ...rest }) => (
diff --git a/styles/main.css b/styles/main.css
index d22b432..da50284 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -70,7 +70,11 @@ div:has(.next-error-h1) {
animation-delay: 0.18s;
}
- .partner-marquee:hover .animate-carousel-scroll {
+ .partner-marquee {
+ animation: carousel-scroll 24s linear infinite;
+ }
+
+ .partner-marquee:hover {
animation-play-state: paused;
}
@@ -86,9 +90,6 @@ div:has(.next-error-h1) {
box-shadow: 0 1rem 2.5rem color-mix(in srgb, currentColor 10%, transparent);
}
- .animate-carousel-scroll {
- animation: carousel-scroll 24s linear infinite;
- }
}
@keyframes rise-in {
@@ -129,6 +130,6 @@ div:has(.next-error-h1) {
}
to {
- transform: translateX(-100%);
+ transform: translateX(calc(-100% / 3));
}
-}
\ No newline at end of file
+}
From 83e15ec81e4290e896121c9d8fa275d026bdcc10 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 21 Sep 2026 18:32:51 +0000
Subject: [PATCH 4/7] fix: smooth partner marquee loop
Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
---
pages/index.tsx | 2 +-
styles/main.css | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/pages/index.tsx b/pages/index.tsx
index d21f5fd..c1a546a 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -102,7 +102,7 @@ const HomePage: FC = observer(({ repositories, members }) => {
{/**
* @todo: polish the carousel animation
*/}
- {Array.from({ length: 3 }).map((_, index) => (
+ {Array.from({ length: 2 }).map((_, index) => (
-
Date: Mon, 21 Sep 2026 18:34:03 +0000
Subject: [PATCH 5/7] fix: delegate transitions without breaking routing
Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
---
components/TransitionLink.tsx | 18 ++++++------------
pages/index.tsx | 32 +++++++++++++++++---------------
styles/main.css | 4 ++--
3 files changed, 25 insertions(+), 29 deletions(-)
diff --git a/components/TransitionLink.tsx b/components/TransitionLink.tsx
index c247dee..9a74e29 100644
--- a/components/TransitionLink.tsx
+++ b/components/TransitionLink.tsx
@@ -14,24 +14,18 @@ export function TransitionLink({
scroll,
shallow,
locale,
- onClick,
- target,
- children,
...props
}: TransitionLinkProps) {
const router = useRouter();
- const handleClick = (event: MouseEvent) => {
- onClick?.(event);
-
+ const handleClickCapture = (event: MouseEvent) => {
if (
- event.defaultPrevented ||
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey ||
- target ||
+ props.target ||
typeof href !== 'string' ||
!href.startsWith('/')
)
@@ -44,7 +38,8 @@ export function TransitionLink({
const nextURL = new URL(href, location.href),
currentURL = new URL(router.asPath, location.href);
- if (nextURL.href === currentURL.href) return;
+ if (nextURL.pathname === currentURL.pathname && nextURL.search === currentURL.search)
+ return;
event.preventDefault();
@@ -63,11 +58,10 @@ export function TransitionLink({
scroll={scroll}
shallow={shallow}
locale={locale}
- target={target}
- onClick={handleClick}
+ onClickCapture={handleClickCapture}
{...props}
>
- {children}
+ {props.children}
);
}
diff --git a/pages/index.tsx b/pages/index.tsx
index c1a546a..32b38b6 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -98,21 +98,23 @@ const HomePage: FC = observer(({ repositories, members }) => {
-
- {/**
- * @todo: polish the carousel animation
- */}
- {Array.from({ length: 2 }).map((_, index) => (
- -
- {PARTNERS_INFO(i18n).map(({ name, ...rest }) => (
-
- ))}
-
- ))}
-
+
+
+ {/**
+ * @todo: polish the carousel animation
+ */}
+ {Array.from({ length: 2 }).map((_, index) => (
+ -
+ {PARTNERS_INFO(i18n).map(({ name, ...rest }) => (
+
+ ))}
+
+ ))}
+
+
diff --git a/styles/main.css b/styles/main.css
index e29417b..9762019 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -70,11 +70,11 @@ div:has(.next-error-h1) {
animation-delay: 0.18s;
}
- .partner-marquee {
+ .partner-marquee-track {
animation: carousel-scroll 24s linear infinite;
}
- .partner-marquee:hover {
+ .partner-marquee:hover .partner-marquee-track {
animation-play-state: paused;
}
From 3b19a4120af4c5d7d06cf1604c80884c5a3ffa58 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 21 Sep 2026 18:35:04 +0000
Subject: [PATCH 6/7] fix: tighten transition link typing
Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
---
components/TransitionLink.tsx | 9 ++++++++-
pages/index.tsx | 7 ++++---
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/components/TransitionLink.tsx b/components/TransitionLink.tsx
index 9a74e29..906dcb5 100644
--- a/components/TransitionLink.tsx
+++ b/components/TransitionLink.tsx
@@ -6,7 +6,9 @@ type ViewTransitionDocument = Document & {
startViewTransition?: (updateCallback: () => Promise | unknown) => void;
};
-type TransitionLinkProps = ComponentProps;
+type TransitionLinkProps = Omit, 'href'> & {
+ href: string;
+};
export function TransitionLink({
href,
@@ -14,11 +16,16 @@ export function TransitionLink({
scroll,
shallow,
locale,
+ onClickCapture,
...props
}: TransitionLinkProps) {
const router = useRouter();
const handleClickCapture = (event: MouseEvent) => {
+ onClickCapture?.(event);
+
+ if (event.defaultPrevented) return;
+
if (
event.button !== 0 ||
event.metaKey ||
diff --git a/pages/index.tsx b/pages/index.tsx
index 32b38b6..4d423ab 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -48,7 +48,8 @@ export const getStaticProps: GetStaticProps = async () => {
const HomePage: FC = observer(({ repositories, members }) => {
const i18n = useContext(I18nContext);
- const { t } = i18n;
+ const { t } = i18n,
+ partners = PARTNERS_INFO(i18n);
return (
@@ -108,8 +109,8 @@ const HomePage: FC = observer(({ repositories, members }) => {
key={index}
className="flex min-w-full shrink-0 flex-row flex-nowrap items-center justify-around gap-12"
>
- {PARTNERS_INFO(i18n).map(({ name, ...rest }) => (
-
+ {partners.map(({ name, ...rest }) => (
+
))}
))}
From 8c8e031dfc5ca5dc0033c57e10c10eed488c76e6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 21 Sep 2026 18:36:00 +0000
Subject: [PATCH 7/7] fix: preserve hash navigation and speculation rules JSON
Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
---
components/TransitionLink.tsx | 7 ++++++-
pages/_document.tsx | 5 ++++-
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/components/TransitionLink.tsx b/components/TransitionLink.tsx
index 906dcb5..7d91a0b 100644
--- a/components/TransitionLink.tsx
+++ b/components/TransitionLink.tsx
@@ -45,7 +45,12 @@ export function TransitionLink({
const nextURL = new URL(href, location.href),
currentURL = new URL(router.asPath, location.href);
- if (nextURL.pathname === currentURL.pathname && nextURL.search === currentURL.search)
+ const isHashOnlyNavigation =
+ nextURL.pathname === currentURL.pathname &&
+ nextURL.search === currentURL.search &&
+ nextURL.hash !== currentURL.hash;
+
+ if (isHashOnlyNavigation || nextURL.href === currentURL.href)
return;
event.preventDefault();
diff --git a/pages/_document.tsx b/pages/_document.tsx
index e9d66b9..646a401 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -90,7 +90,10 @@ export default class CustomDocument extends Document {
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=attach_file_add,chat,code,dark_mode,diversity_3,fingerprint,key,keyboard_arrow_down,language,light_mode,menu,translate,trending_up,visibility&display=swap"
/>
-
+