@@ -48,6 +48,10 @@ import bg from 'data-url:./bg.svg';
4848import { keyframes } from "../../../../../@react-spectrum/s2/style/style-macro" with { type : 'macro' } ;
4949// @ts -ignore
5050import { getBaseUrl } from "../../../src/pageUtils" ;
51+ // @ts -ignore
52+ import { fontSizeToken } from "../../../../../@react-spectrum/s2/style/tokens" with { type : 'macro' } ;
53+ // @ts -ignore
54+ import { letters } from "../../../src/textWidth" ;
5155
5256const container = style ( {
5357 backgroundColor : 'layer-2/80' ,
@@ -67,78 +71,90 @@ const container = style({
6771const swapWrapper = style ( {
6872 display : 'inline-block' ,
6973 position : 'relative' ,
70- height : '[1em ]' ,
74+ height : '[1.2em ]' ,
7175 overflow : 'hidden' ,
7276 verticalAlign : 'baseline' ,
7377 whiteSpace : 'nowrap' ,
74- lineHeight : '[1em]' ,
75- marginEnd : 12
78+ lineHeight : '[1.2]'
7679} ) ;
7780
7881// Track that scrolls vertically through all the items.
79- const slideTrack = keyframes ( `
82+ // Use 3D to ensure crisp text rendering.
83+ // With 10x hold time vs transition time:
84+ // Total units: (6 holds × 10) + (6 transitions × 1) = 60 + 6 = 66 units
85+ // Each transition = 100/66 = 1.515%
86+ // Each hold = 10 × 1.515% = 15.152%
87+ const slideTrack : string = keyframes ( `
8088 0% {
81- transform: translateY(0% );
89+ transform: translate3d(0, 0, 0 );
8290 }
83- 15% {
84- transform: translateY(0% );
91+ 15.15 % {
92+ transform: translate3d(0, 0, 0 );
8593 }
86- 16% {
87- transform: translateY(-16.666% );
94+ 16.67 % {
95+ transform: translate3d(0, -1.2em, 0 );
8896 }
89- 31% {
90- transform: translateY(-16.666% );
97+ 31.82 % {
98+ transform: translate3d(0, -1.2em, 0 );
9199 }
92- 32 % {
93- transform: translateY(-33.333% );
100+ 33.33 % {
101+ transform: translate3d(0, -2.4em, 0 );
94102 }
95- 47 % {
96- transform: translateY(-33.333% );
103+ 48.48 % {
104+ transform: translate3d(0, -2.4em, 0 );
97105 }
98- 48 % {
99- transform: translateY(-50% );
106+ 50 % {
107+ transform: translate3d(0, -3.6em, 0 );
100108 }
101- 63 % {
102- transform: translateY(-50% );
109+ 65.15 % {
110+ transform: translate3d(0, -3.6em, 0 );
103111 }
104- 64 % {
105- transform: translateY(-66.666% );
112+ 66.67 % {
113+ transform: translate3d(0, -4.8em, 0 );
106114 }
107- 79 % {
108- transform: translateY(-66.666% );
115+ 81.82 % {
116+ transform: translate3d(0, -4.8em, 0 );
109117 }
110- 80 % {
111- transform: translateY(-83.333% );
118+ 83.33 % {
119+ transform: translate3d(0, -6em, 0 );
112120 }
113- 95 % {
114- transform: translateY(-83.333% );
121+ 98.48 % {
122+ transform: translate3d(0, -6em, 0 );
115123 }
116124 100% {
117- transform: translateY(0% );
125+ transform: translate3d(0, -7.2em, 0 );
118126 }
119127` ) ;
120128
121129const swapTrack = style ( {
122- animation : slideTrack ,
123- animationDuration : 15000 ,
124- animationTimingFunction : 'linear ' ,
125- animationIterationCount : 'infinite' ,
126- display : 'flex' ,
130+ position : 'relative' ,
131+ display : {
132+ default : 'flex ' ,
133+ '@media (prefers-reduced-motion: reduce)' : 'none'
134+ } ,
127135 flexDirection : 'column' ,
128- whiteSpace : 'nowrap'
136+ whiteSpace : 'nowrap' ,
137+ height : '[1.2em]' ,
138+ overflow : 'hidden' ,
139+ fontSize : '[1em]' ,
140+ willChange : 'transform'
129141} ) ;
130142
131- // for measuring longest word
132143const swapSizer = style ( {
133- opacity : 0 ,
144+ display : {
145+ default : 'none' ,
146+ '@media (prefers-reduced-motion: reduce)' : 'block'
147+ } ,
134148 whiteSpace : 'nowrap'
135149} ) ;
136150
137-
138151const swapRow = style ( {
139- display : 'block' ,
140- height : '[1em]' ,
141- lineHeight : '[1em]'
152+ animation : slideTrack ,
153+ animationDuration : 10000 ,
154+ animationTimingFunction : 'linear' ,
155+ animationIterationCount : 'infinite' ,
156+ lineHeight : '[1.2]' ,
157+ height : '[1.2em]'
142158} ) ;
143159
144160export function Home ( ) {
@@ -197,21 +213,21 @@ export function Home() {
197213 </ div >
198214 </ nav >
199215 < header aria-labelledby = { headingId } className = { style ( { marginX : 'auto' , paddingX : { default : 16 , sm : 40 } , paddingY : 96 , maxWidth : 1024 } ) } >
200- < h1 id = { headingId } className = { style ( { font : 'heading-3xl' , marginY : 0 , color : 'white' } ) } >
201- < span className = { swapWrapper } > Build apps </ span >
216+ < HomeH1 id = { headingId } >
217+ < span className = { swapWrapper } > Build apps with </ span >
202218 < span className = { swapWrapper } >
203- { /* @ts -ignore */ }
204- < span className = { swapTrack } >
205- < span className = { swapRow } > with polish </ span >
206- < span className = { swapRow } > with speed </ span >
207- < span className = { swapRow } > with ease </ span >
208- < span className = { swapRow } > with accessibility </ span >
209- < span className = { swapRow } > with consistency </ span >
210- < span className = { swapRow } > with React Spectrum </ span >
211- </ span >
212- < span className = { swapSizer } aria-hidden > with React Spectrum</ span >
219+ < div className = { swapTrack } >
220+ < span className = { swapRow } > polish </ span >
221+ < span className = { swapRow } > speed </ span >
222+ < span className = { swapRow } > ease </ span >
223+ < span className = { swapRow } > accessibility </ span >
224+ < span className = { swapRow } > consistency </ span >
225+ < span className = { swapRow } > React Spectrum </ span >
226+ < span className = { swapRow } aria-hidden > polish </ span >
227+ </ div >
228+ < span className = { swapSizer } aria-hidden > React Spectrum</ span >
213229 </ span >
214- </ h1 >
230+ </ HomeH1 >
215231 < p className = { style ( { font : 'body-3xl' , marginY : 0 , color : 'white' } ) } > React Spectrum gives you the power to build high quality, accessible UI with the cohesive look and feel of Adobe. </ p >
216232 < div className = { style ( { display : 'flex' , gap : 16 , flexDirection : { default : 'column' , sm : 'row' } , marginTop : 32 , marginBottom : 56 } ) } >
217233 < LinkButton size = "XL" staticColor = "white" href = "getting-started" > Get started</ LinkButton >
@@ -386,7 +402,7 @@ export function Home() {
386402 < h4 className = { style ( { font : 'title' , marginTop : 0 } ) } > Button.tsx</ h4 >
387403 < Pre > < Code lang = "tsx" > { `import {style, focusRing} from '@react-spectrum/s2/style' with {type: 'macro'};
388404import {hstack} from './style-utils' with {type: 'macro'};
389-
405+
390406const buttonStyle = style({
391407 ...focusRing(),
392408 ...hstack(4)
@@ -439,21 +455,21 @@ const buttonStyle = style({
439455 description = "Comprehensive markdown docs, llms.txt, and an agent-friendly MCP server."
440456 illustration = { < Sparkles /> }
441457 styles = { style ( { gridColumnStart : { default : 'span 6' , lg : 'span 2' } } ) } >
442-
458+
443459 </ Feature >
444460 < Feature
445461 title = "SSR"
446462 description = "Server-side rendering and React Server Components support, maximizing Core Web Vitals with zero layout thrashing."
447463 illustration = { < Server /> }
448464 styles = { style ( { gridColumnStart : { default : 'span 6' , lg : 'span 2' } } ) } >
449-
465+
450466 </ Feature >
451467 < Feature
452468 title = "Small bundle"
453469 description = "Aggressive tree-shaking and atomic CSS resulting in reduced bundle sizes and faster runtime performance."
454470 illustration = { < SpeedFast /> }
455471 styles = { style ( { gridColumnStart : { default : 'span 6' , lg : 'span 2' } } ) } >
456-
472+
457473 </ Feature >
458474 </ Section >
459475 </ main >
@@ -488,6 +504,46 @@ const buttonStyle = style({
488504 ) ;
489505}
490506
507+ function getTitleTextWidth ( text : string ) {
508+ let width = 0 ;
509+ for ( let c of text ) {
510+ let w = letters [ c ] ;
511+ if ( w != null ) {
512+ width += w ;
513+ }
514+ }
515+
516+ return width ;
517+ }
518+
519+ function HomeH1 ( props ) {
520+ let { children, ...otherProps } = props ;
521+ return (
522+ < h1
523+ { ...otherProps }
524+ style = { { '--width-per-em' : getTitleTextWidth ( 'React Spectrum' ) } as any }
525+ className = { style ( {
526+ font : 'heading-3xl' ,
527+ // This variable is used to calculate the line height.
528+ // Normally it is set by the fontSize, but the custom clamp prevents this.
529+ '--fs' : {
530+ type : 'opacity' ,
531+ value : 'pow(1.125, 10)' // heading-2xl
532+ } ,
533+ '--headingFontSize' : {
534+ type : 'width' ,
535+ value : `[round(pow(1.125, ${ fontSizeToken ( 'heading-size-xxxl' ) } ) * var(--s2-font-size-base, 14) / 16 * 1rem, 1px)]`
536+ } ,
537+ // On mobile, adjust heading to fit in the viewport, and clamp between a min and max font size.
538+ fontSize : `clamp(${ 35 / 16 } rem, (100vw - 40px) / var(--width-per-em), var(--headingFontSize))` ,
539+ marginY : 0 ,
540+ color : 'white'
541+ } ) } >
542+ { children }
543+ </ h1 >
544+ )
545+ }
546+
491547function Section ( { title, description, children} : any ) {
492548 let headingId = useId ( ) ;
493549 return (
0 commit comments