1
1
"use client"
2
2
3
3
import clsx from "clsx"
4
- import { useState } from "react"
4
+ import { useState , Fragment } from "react"
5
5
6
6
import { Button } from "@/app/conf/_design-system/button"
7
- import ArrowRightIcon from "./arrow-right.svg?svgr"
8
7
import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration"
8
+ import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr"
9
9
10
10
import blurBean from "./blur-bean.webp"
11
11
@@ -29,28 +29,35 @@ const USE_CASES: UseCase[] = [
29
29
description :
30
30
"Fetch only the data you need to minimize payload size and round-trips. Build resilient UIs that work well on variable networks." ,
31
31
cta : "Mobile Patterns" ,
32
- href : "/learn/best-practices#mobile " ,
32
+ href : "TODO " ,
33
33
} ,
34
34
{
35
35
label : "A frontend-heavy app with advanced UI needs" ,
36
36
description :
37
37
"Co-locate queries with components and keep state consistent. Compose data from many backends without bespoke endpoints." ,
38
38
cta : "Frontend Integration Guide" ,
39
- href : "/learn/queries#components " ,
39
+ href : "TODO " ,
40
40
} ,
41
41
{
42
42
label : "An app with real-time updates" ,
43
43
description :
44
44
"Use subscriptions for low-latency updates while keeping the schema as the single contract for clients and servers." ,
45
45
cta : "Real-time with Subscriptions" ,
46
- href : "/learn/subscriptions " ,
46
+ href : "TODO " ,
47
47
} ,
48
48
{
49
49
label : "A simple full stack TypeScript app" ,
50
50
description :
51
51
"Strong types end-to-end with code generation and great DX. Ship faster without compromising correctness." ,
52
52
cta : "Full Stack TS Starter" ,
53
- href : "/learn#typescript" ,
53
+ href : "TODO" ,
54
+ } ,
55
+ {
56
+ label : "An AI-powered app" ,
57
+ description :
58
+ "Build apps with soft core with GraphQL MCP, using GraphQL schema introspection to give access and teach an LLM about your data." ,
59
+ cta : "MCP GraphQL" ,
60
+ href : "https://github.com/graphql/graphql-mcp" ,
54
61
} ,
55
62
]
56
63
@@ -75,39 +82,71 @@ export function UseCases({
75
82
</ p >
76
83
77
84
< div className = "3xl:flex-1" />
78
- < ul className = "mt-8 divide-y divide-sec-dark border border-sec-dark lg:mt-16" >
85
+ < div
86
+ role = "tablist"
87
+ className = "mt-8 divide-y divide-sec-dark border border-sec-dark max-lg:sr-only lg:mt-16"
88
+ >
79
89
{ USE_CASES . map ( ( useCase , i ) => (
80
- < li key = { useCase . label } >
90
+ < button
91
+ role = "tab"
92
+ type = "button"
93
+ key = { useCase . label }
94
+ tabIndex = { i === 0 ? 0 : - 1 }
95
+ onKeyDown = { arrowsMoveSideways }
96
+ onPointerDown = { ( ) => setSelectedIndex ( i ) }
97
+ onFocus = { ( ) => setSelectedIndex ( i ) }
98
+ aria-selected = { i === selectedIndex ? "true" : undefined }
99
+ className = "gql-focus-visible group flex w-full items-center justify-between gap-6 border-b border-sec-dark px-3 py-4 text-left transition-colors hover:bg-sec-lighter aria-selected:bg-sec-base aria-selected:hover:bg-sec-lighter hover:dark:bg-neu-100/25 dark:aria-selected:bg-sec-darker"
100
+ >
101
+ < span className = "typography-body-lg" > { useCase . label } </ span >
102
+ < ArrowDownIcon className = "size-10 shrink-0 -rotate-90 text-sec-dark opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100 group-aria-selected:opacity-100 dark:text-neu-900" />
103
+ </ button >
104
+ ) ) }
105
+ </ div >
106
+ < div className = "3xl:flex-1" />
107
+ </ div >
108
+
109
+ < article className = "relative flex h-auto flex-col bg-sec-base dark:bg-sec-darker" >
110
+ < Stripes />
111
+ < div className = "flex flex-1 justify-center overflow-hidden max-lg:flex-col lg:items-center" >
112
+ { USE_CASES . map ( ( useCase , i ) => (
113
+ < Fragment key = { useCase . label } >
81
114
< button
82
115
type = "button"
83
- onClick = { ( ) => setSelectedIndex ( i ) }
116
+ onPointerDown = { ( ) => setSelectedIndex ( i ) }
117
+ onFocus = { ( ) => setSelectedIndex ( i ) }
84
118
aria-selected = { i === selectedIndex ? "true" : undefined }
85
- className = "group flex w-full items-center justify-between gap-6 px-3 py-4 text-left transition-colors hover:bg-sec-lighter aria-selected:bg-sec-base aria-selected:hover: bg-sec-lighter hover: dark:bg-neu-100/25 dark:aria-selected:bg-sec-darker"
119
+ className = "z-[1] flex w-full items-center justify-between gap-6 border-b border-sec-dark px-3 py-4 text-left first:border-t hover:bg-sec-light aria-selected:bg-sec-light dark: bg-sec-darker dark:hover: bg-neu-100/25 dark:aria-selected:bg-sec-darker max-lg:dark:bg-neu-50 lg:hidden "
86
120
>
87
- < span className = "typography-body-lg" > { useCase . label } </ span >
88
- < ArrowRightIcon className = "size-10 shrink-0 text-sec-dark opacity-0 transition-opacity group-hover:opacity-100 group-aria-selected:opacity-100 dark:text-neu-900" />
121
+ < span className = "typography-body-lg text-neu-900" >
122
+ { useCase . label }
123
+ </ span >
124
+ < ArrowDownIcon className = "size-6 shrink-0 text-sec-dark transition-transform [[aria-selected=false]>&]:rotate-180" />
89
125
</ button >
90
- </ li >
126
+ < div
127
+ role = "tabpanel"
128
+ className = { clsx (
129
+ "relative h-full flex-1 p-8 lg:p-12 xl:p-16" ,
130
+ selectedIndex === i ? "border-b border-sec-dark" : "hidden" ,
131
+ ) }
132
+ >
133
+ < div className = "relative z-10 my-auto max-h-[528px] max-w-2xl" >
134
+ < h3 className = "typography-body-lg text-sec-darker dark:text-sec-lighter max-lg:hidden" >
135
+ { useCase . label }
136
+ </ h3 >
137
+ < p className = "typography-h3 text-neu-800 lg:mt-10 lg:max-xl:text-xl" >
138
+ { useCase . description }
139
+ </ p >
140
+ < div className = "mt-8 flex xl:mt-[120px]" >
141
+ < Button href = { useCase . href } variant = "primary" >
142
+ { useCase . cta }
143
+ < ArrowDownIcon className = "size-6 shrink-0 -rotate-90 text-neu-0" />
144
+ </ Button >
145
+ </ div >
146
+ </ div >
147
+ </ div >
148
+ </ Fragment >
91
149
) ) }
92
- </ ul >
93
- < div className = "3xl:flex-1" />
94
- </ div >
95
-
96
- < article className = "relative flex h-auto flex-col bg-sec-base p-8 dark:bg-sec-darker md:p-12 lg:p-16" >
97
- < Stripes />
98
- < div className = "z-10 my-auto max-h-[528px] max-w-2xl" >
99
- < h3 className = "typography-body-lg text-sec-darker dark:text-sec-lighter" >
100
- { selected . label }
101
- </ h3 >
102
- < p className = "typography-h3 mt-10 text-neu-800" >
103
- { selected . description }
104
- </ p >
105
- < div className = "mt-8 flex xl:mt-[120px]" >
106
- < Button href = { selected . href } variant = "primary" >
107
- { selected . cta }
108
- < ArrowRightIcon className = "size-6 shrink-0 text-neu-0" />
109
- </ Button >
110
- </ div >
111
150
</ div >
112
151
</ article >
113
152
</ div >
@@ -137,3 +176,19 @@ function Stripes() {
137
176
</ div >
138
177
)
139
178
}
179
+
180
+ function arrowsMoveSideways ( event : React . KeyboardEvent < HTMLButtonElement > ) {
181
+ if ( event . key === "ArrowLeft" || event . key === "ArrowUp" ) {
182
+ const previousElement = event . currentTarget . previousElementSibling
183
+ if ( previousElement ) {
184
+ event . preventDefault ( )
185
+ ; ( previousElement as HTMLElement ) . focus ( )
186
+ }
187
+ } else if ( event . key === "ArrowRight" || event . key === "ArrowDown" ) {
188
+ const nextElement = event . currentTarget . nextElementSibling
189
+ if ( nextElement ) {
190
+ event . preventDefault ( )
191
+ ; ( nextElement as HTMLElement ) . focus ( )
192
+ }
193
+ }
194
+ }
0 commit comments