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(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" /> - +