diff --git a/sites/www/content/why-flutter/flutter-whitepaper-2026.pdf b/sites/www/content/why-flutter/flutter-whitepaper-2026.pdf new file mode 100644 index 00000000000..58feaf9c285 Binary files /dev/null and b/sites/www/content/why-flutter/flutter-whitepaper-2026.pdf differ diff --git a/sites/www/content/why-flutter/images/dash-gemini-sprite.webp b/sites/www/content/why-flutter/images/dash-gemini-sprite.webp new file mode 100644 index 00000000000..50126fa64fc Binary files /dev/null and b/sites/www/content/why-flutter/images/dash-gemini-sprite.webp differ diff --git a/sites/www/content/why-flutter/images/dash-laptop-sprite-v2.webp b/sites/www/content/why-flutter/images/dash-laptop-sprite-v2.webp new file mode 100644 index 00000000000..2ac337ec625 Binary files /dev/null and b/sites/www/content/why-flutter/images/dash-laptop-sprite-v2.webp differ diff --git a/sites/www/content/why-flutter/images/dash-typing-sprite.webp b/sites/www/content/why-flutter/images/dash-typing-sprite.webp new file mode 100644 index 00000000000..acde286d3d2 Binary files /dev/null and b/sites/www/content/why-flutter/images/dash-typing-sprite.webp differ diff --git a/sites/www/content/why-flutter/images/flutter-logo-mark.svg b/sites/www/content/why-flutter/images/flutter-logo-mark.svg new file mode 100644 index 00000000000..eb13ef4a2a1 --- /dev/null +++ b/sites/www/content/why-flutter/images/flutter-logo-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/www/content/why-flutter/images/google-earth-desktop.png b/sites/www/content/why-flutter/images/google-earth-desktop.png new file mode 100644 index 00000000000..f9fe1bc76d5 Binary files /dev/null and b/sites/www/content/why-flutter/images/google-earth-desktop.png differ diff --git a/sites/www/content/why-flutter/images/headspace-app.png b/sites/www/content/why-flutter/images/headspace-app.png new file mode 100644 index 00000000000..85c7e372f22 Binary files /dev/null and b/sites/www/content/why-flutter/images/headspace-app.png differ diff --git a/sites/www/content/why-flutter/images/hero-screens/s01.webp b/sites/www/content/why-flutter/images/hero-screens/s01.webp new file mode 100644 index 00000000000..388835bd7f7 Binary files /dev/null and b/sites/www/content/why-flutter/images/hero-screens/s01.webp differ diff --git a/sites/www/content/why-flutter/images/hero-screens/s02.webp b/sites/www/content/why-flutter/images/hero-screens/s02.webp new file mode 100644 index 00000000000..94300d6c42c Binary files /dev/null and b/sites/www/content/why-flutter/images/hero-screens/s02.webp differ diff --git a/sites/www/content/why-flutter/images/hero-screens/s03.webp b/sites/www/content/why-flutter/images/hero-screens/s03.webp new file mode 100644 index 00000000000..59693a0852d Binary files /dev/null and b/sites/www/content/why-flutter/images/hero-screens/s03.webp differ diff --git a/sites/www/content/why-flutter/images/hero-screens/s04.webp b/sites/www/content/why-flutter/images/hero-screens/s04.webp new file mode 100644 index 00000000000..e67f0fee7af Binary files /dev/null and b/sites/www/content/why-flutter/images/hero-screens/s04.webp differ diff --git a/sites/www/content/why-flutter/images/hero-screens/s05.webp b/sites/www/content/why-flutter/images/hero-screens/s05.webp new file mode 100644 index 00000000000..41af104b0c9 Binary files /dev/null and b/sites/www/content/why-flutter/images/hero-screens/s05.webp differ diff --git a/sites/www/content/why-flutter/images/hero-screens/s06.webp b/sites/www/content/why-flutter/images/hero-screens/s06.webp new file mode 100644 index 00000000000..cda39d85219 Binary files /dev/null and b/sites/www/content/why-flutter/images/hero-screens/s06.webp differ diff --git a/sites/www/content/why-flutter/images/hero-screens/s07.webp b/sites/www/content/why-flutter/images/hero-screens/s07.webp new file mode 100644 index 00000000000..17d0f4e99e9 Binary files /dev/null and b/sites/www/content/why-flutter/images/hero-screens/s07.webp differ diff --git a/sites/www/content/why-flutter/images/hero-screens/s08.webp b/sites/www/content/why-flutter/images/hero-screens/s08.webp new file mode 100644 index 00000000000..e359ef5cbc9 Binary files /dev/null and b/sites/www/content/why-flutter/images/hero-screens/s08.webp differ diff --git a/sites/www/content/why-flutter/images/hero-screens/s09.webp b/sites/www/content/why-flutter/images/hero-screens/s09.webp new file mode 100644 index 00000000000..3eb80f097fb Binary files /dev/null and b/sites/www/content/why-flutter/images/hero-screens/s09.webp differ diff --git a/sites/www/content/why-flutter/images/hero-screens/s10.webp b/sites/www/content/why-flutter/images/hero-screens/s10.webp new file mode 100644 index 00000000000..cadc380ca87 Binary files /dev/null and b/sites/www/content/why-flutter/images/hero-screens/s10.webp differ diff --git a/sites/www/content/why-flutter/images/hero-screens/s11.webp b/sites/www/content/why-flutter/images/hero-screens/s11.webp new file mode 100644 index 00000000000..2027e02af37 Binary files /dev/null and b/sites/www/content/why-flutter/images/hero-screens/s11.webp differ diff --git a/sites/www/content/why-flutter/images/hero-screens/s12.webp b/sites/www/content/why-flutter/images/hero-screens/s12.webp new file mode 100644 index 00000000000..229d6bb1c37 Binary files /dev/null and b/sites/www/content/why-flutter/images/hero-screens/s12.webp differ diff --git a/sites/www/content/why-flutter/images/leancode.svg b/sites/www/content/why-flutter/images/leancode.svg new file mode 100644 index 00000000000..e1fb3355132 --- /dev/null +++ b/sites/www/content/why-flutter/images/leancode.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/sites/www/content/why-flutter/images/lg-tv.png b/sites/www/content/why-flutter/images/lg-tv.png new file mode 100644 index 00000000000..8d9b302d411 Binary files /dev/null and b/sites/www/content/why-flutter/images/lg-tv.png differ diff --git a/sites/www/content/why-flutter/images/phone-frame.png b/sites/www/content/why-flutter/images/phone-frame.png new file mode 100644 index 00000000000..0e0395a31a6 Binary files /dev/null and b/sites/www/content/why-flutter/images/phone-frame.png differ diff --git a/sites/www/content/why-flutter/images/resource-arrow.svg b/sites/www/content/why-flutter/images/resource-arrow.svg new file mode 100644 index 00000000000..b231efb2a31 --- /dev/null +++ b/sites/www/content/why-flutter/images/resource-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/www/content/why-flutter/images/universal-app.png b/sites/www/content/why-flutter/images/universal-app.png new file mode 100644 index 00000000000..c5e12e63cf3 Binary files /dev/null and b/sites/www/content/why-flutter/images/universal-app.png differ diff --git a/sites/www/content/why-flutter/images/vgvlogo.svg b/sites/www/content/why-flutter/images/vgvlogo.svg new file mode 100644 index 00000000000..baa55843b0f --- /dev/null +++ b/sites/www/content/why-flutter/images/vgvlogo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sites/www/content/why-flutter/index.md b/sites/www/content/why-flutter/index.md new file mode 100644 index 00000000000..a4bda4da201 --- /dev/null +++ b/sites/www/content/why-flutter/index.md @@ -0,0 +1,10 @@ +--- +title: Why Flutter +bodyTags: interior whyFlutter +description: >- + Discover how unifying your codebase empowers you to ship to iOS, Android, + Web, and Desktop faster and more efficiently than ever before. +publishDate: "2026-08-03" +--- + + diff --git a/sites/www/lib/main.client.options.dart b/sites/www/lib/main.client.options.dart index a2cac8325f3..419bc3b6536 100644 --- a/sites/www/lib/main.client.options.dart +++ b/sites/www/lib/main.client.options.dart @@ -24,6 +24,14 @@ import 'package:flutter_website/src/components/pages/games_adaptive_media.dart' deferred as _games_adaptive_media; import 'package:flutter_website/src/components/pages/showcase_grid.dart' deferred as _showcase_grid; +import 'package:flutter_website/src/components/sections/why_flutter_ai_era_section.dart' + deferred as _why_flutter_ai_era_section; +import 'package:flutter_website/src/components/sections/why_flutter_cases_section.dart' + deferred as _why_flutter_cases_section; +import 'package:flutter_website/src/components/sections/why_flutter_code_shared_section.dart' + deferred as _why_flutter_code_shared_section; +import 'package:flutter_website/src/components/sections/why_flutter_hero_section.dart' + deferred as _why_flutter_hero_section; import 'package:flutter_website/src/models/content/banner_content.dart' as _banner_content; import 'package:site_shared/components/blog/client/blog_categories.dart' @@ -145,6 +153,33 @@ ClientOptions get defaultClientOptions => ClientOptions( ), loader: _showcase_grid.loadLibrary, ), + 'why_flutter_ai_era_section': ClientLoader( + (p) => _why_flutter_ai_era_section.WhyFlutterAiEraSection( + geminiSprite: p['geminiSprite'] as String, + ), + loader: _why_flutter_ai_era_section.loadLibrary, + ), + 'why_flutter_cases_section': ClientLoader( + (p) => _why_flutter_cases_section.WhyFlutterCasesSection( + media: (p['media'] as Map).cast(), + ), + loader: _why_flutter_cases_section.loadLibrary, + ), + 'why_flutter_code_shared_section': ClientLoader( + (p) => _why_flutter_code_shared_section.WhyFlutterCodeSharedSection( + laptopSprite: p['laptopSprite'] as String, + ), + loader: _why_flutter_code_shared_section.loadLibrary, + ), + 'why_flutter_hero_section': ClientLoader( + (p) => _why_flutter_hero_section.WhyFlutterHeroSection( + typingSprite: p['typingSprite'] as String, + screens: (p['screens'] as List).cast(), + whitepaperUrl: p['whitepaperUrl'] as String, + flutterLogo: p['flutterLogo'] as String, + ), + loader: _why_flutter_hero_section.loadLibrary, + ), 'site_shared:blog_categories': ClientLoader( (p) => _blog_categories.BlogCategories( categories: (p['categories'] as List) diff --git a/sites/www/lib/main.server.dart b/sites/www/lib/main.server.dart index cd3cecbd3f6..353256d41bd 100644 --- a/sites/www/lib/main.server.dart +++ b/sites/www/lib/main.server.dart @@ -41,6 +41,7 @@ import 'src/pages/news_page.dart'; import 'src/pages/not_found_page.dart'; import 'src/pages/showcase_page.dart'; import 'src/pages/web_page.dart'; +import 'src/pages/why_flutter_page.dart'; import 'src/utils/asset_utils.dart'; void main() async { @@ -118,6 +119,7 @@ void main() async { defineComponent('BrandPage', const BrandPage()), defineComponent('FlipPage', const FlipPage()), defineComponent('NewsPage', const NewsPage()), + defineComponent('WhyFlutterPage', const WhyFlutterPage()), defineComponentWithAttrs('Image', Image.fromAttrs), CustomComponent( diff --git a/sites/www/lib/main.server.options.dart b/sites/www/lib/main.server.options.dart index 3a88eb316a0..976a1dce9f1 100644 --- a/sites/www/lib/main.server.options.dart +++ b/sites/www/lib/main.server.options.dart @@ -21,6 +21,14 @@ import 'package:flutter_website/src/components/pages/games_adaptive_media.dart' as _games_adaptive_media; import 'package:flutter_website/src/components/pages/showcase_grid.dart' as _showcase_grid; +import 'package:flutter_website/src/components/sections/why_flutter_ai_era_section.dart' + as _why_flutter_ai_era_section; +import 'package:flutter_website/src/components/sections/why_flutter_cases_section.dart' + as _why_flutter_cases_section; +import 'package:flutter_website/src/components/sections/why_flutter_code_shared_section.dart' + as _why_flutter_code_shared_section; +import 'package:flutter_website/src/components/sections/why_flutter_hero_section.dart' + as _why_flutter_hero_section; import 'package:jaspr_content/components/file_tree.dart' as _file_tree; import 'package:site_shared/components/blog/client/blog_categories.dart' as _blog_categories; @@ -85,6 +93,28 @@ ServerOptions get defaultServerOptions => ServerOptions( 'showcase_grid', params: __showcase_gridShowcaseGrid, ), + _why_flutter_ai_era_section.WhyFlutterAiEraSection: + ClientTarget<_why_flutter_ai_era_section.WhyFlutterAiEraSection>( + 'why_flutter_ai_era_section', + params: __why_flutter_ai_era_sectionWhyFlutterAiEraSection, + ), + _why_flutter_cases_section.WhyFlutterCasesSection: + ClientTarget<_why_flutter_cases_section.WhyFlutterCasesSection>( + 'why_flutter_cases_section', + params: __why_flutter_cases_sectionWhyFlutterCasesSection, + ), + _why_flutter_code_shared_section.WhyFlutterCodeSharedSection: + ClientTarget< + _why_flutter_code_shared_section.WhyFlutterCodeSharedSection + >( + 'why_flutter_code_shared_section', + params: __why_flutter_code_shared_sectionWhyFlutterCodeSharedSection, + ), + _why_flutter_hero_section.WhyFlutterHeroSection: + ClientTarget<_why_flutter_hero_section.WhyFlutterHeroSection>( + 'why_flutter_hero_section', + params: __why_flutter_hero_sectionWhyFlutterHeroSection, + ), _blog_categories.BlogCategories: ClientTarget<_blog_categories.BlogCategories>( 'site_shared:blog_categories', @@ -142,6 +172,24 @@ Map __games_adaptive_mediaGamesAdaptiveMedia( Map __showcase_gridShowcaseGrid( _showcase_grid.ShowcaseGrid c, ) => {'data': c.data, 'items': c.items.map((i) => i.toId()).toList()}; +Map __why_flutter_ai_era_sectionWhyFlutterAiEraSection( + _why_flutter_ai_era_section.WhyFlutterAiEraSection c, +) => {'geminiSprite': c.geminiSprite}; +Map __why_flutter_cases_sectionWhyFlutterCasesSection( + _why_flutter_cases_section.WhyFlutterCasesSection c, +) => {'media': c.media}; +Map +__why_flutter_code_shared_sectionWhyFlutterCodeSharedSection( + _why_flutter_code_shared_section.WhyFlutterCodeSharedSection c, +) => {'laptopSprite': c.laptopSprite}; +Map __why_flutter_hero_sectionWhyFlutterHeroSection( + _why_flutter_hero_section.WhyFlutterHeroSection c, +) => { + 'typingSprite': c.typingSprite, + 'screens': c.screens, + 'whitepaperUrl': c.whitepaperUrl, + 'flutterLogo': c.flutterLogo, +}; Map __blog_categoriesBlogCategories( _blog_categories.BlogCategories c, ) => {'categories': c.categories.map((i) => i.toMap()).toList()}; diff --git a/sites/www/lib/src/components/sections/why_flutter_ai_era_section.dart b/sites/www/lib/src/components/sections/why_flutter_ai_era_section.dart new file mode 100644 index 00000000000..45fbaf9c4f1 --- /dev/null +++ b/sites/www/lib/src/components/sections/why_flutter_ai_era_section.dart @@ -0,0 +1,95 @@ +// Copyright 2026, the Flutter authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. + +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; + +import '../../utils/visibility_observer.dart'; +import '../common/icon.dart'; + +/// The "Built for the AI era" section. +/// +/// The Dash and Gemini sprite starts looping once it scrolls into view, so the +/// section hydrates on the client. +@client +class WhyFlutterAiEraSection extends StatefulComponent { + const WhyFlutterAiEraSection({required this.geminiSprite, super.key}); + + /// Resolved URL of the Dash and Gemini sprite sheet. + /// + /// Resolved by the page rather than here, so that this component — which + /// hydrates on the client — never imports the server-only asset helpers. + final String geminiSprite; + + @override + State createState() => _WhyFlutterAiEraSectionState(); +} + +class _WhyFlutterAiEraSectionState extends State { + static const _capabilities = ['MCP Server', 'GenUI', 'LLM-fluent']; + + bool _isSpriteRunning = false; + + @override + void initState() { + super.initState(); + if (!kIsWeb) return; + + observeOnce('#why-flutter-gemini', () { + setState(() => _isSpriteRunning = true); + }); + } + + @override + Component build(BuildContext context) { + return section(id: 'ai-era', classes: 'module why-flutter-ai', [ + div(classes: 'why-flutter-row container', [ + div(classes: 'why-flutter-ai-copy', [ + const h2([.text('Built for the AI era')]), + const p(classes: 'why-flutter-body', [ + .text( + 'Flutter provides a scalable foundation for AI-powered ' + 'experiences, making it easy to integrate agentic features and ' + 'evolve your product.', + ), + ]), + div(classes: 'why-flutter-chips', [ + for (final capability in _capabilities) + _CapabilityChip(label: capability), + ]), + ]), + div(classes: 'why-flutter-ai-media', [ + div( + id: 'why-flutter-gemini', + classes: [ + 'why-flutter-gemini-sprite', + if (_isSpriteRunning) 'is-running', + ].join(' '), + styles: Styles( + raw: { + '--why-flutter-sprite': 'url(${component.geminiSprite})', + }, + ), + const [], + ), + ]), + ]), + ]); + } +} + +/// A single capability chip with a teal check mark. +class _CapabilityChip extends StatelessComponent { + const _CapabilityChip({required this.label}); + + final String label; + + @override + Component build(BuildContext context) { + return div(classes: 'why-flutter-chip', [ + const div(classes: 'why-flutter-chip-check', [Icon(symbol: 'check')]), + span([.text(label)]), + ]); + } +} diff --git a/sites/www/lib/src/components/sections/why_flutter_cases_section.dart b/sites/www/lib/src/components/sections/why_flutter_cases_section.dart new file mode 100644 index 00000000000..f172436df0a --- /dev/null +++ b/sites/www/lib/src/components/sections/why_flutter_cases_section.dart @@ -0,0 +1,233 @@ +// Copyright 2026, the Flutter authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. + +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:universal_web/web.dart' as web; + +import '../common/icon.dart'; + +/// Describes a single success-case card. +typedef _Case = ({ + String company, + String quote, + String? storyUrl, + String theme, + bool isDark, +}); + +/// The "What switching actually returned" success-case carousel. +/// +/// On wide viewports the track translates one card at a time; below the medium +/// breakpoint it becomes a native snap-scroll strip driven by the same arrows. +@client +class WhyFlutterCasesSection extends StatefulComponent { + const WhyFlutterCasesSection({required this.media, super.key}); + + /// Resolved URLs of the device imagery, keyed by card theme. + /// + /// Resolved by the page because the asset helpers are server-only, and this + /// component hydrates on the client. + final Map media; + + @override + State createState() => _WhyFlutterCasesSectionState(); +} + +class _WhyFlutterCasesSectionState extends State { + /// Width of one card plus the gap between cards. + static const _cardStride = 1220; + + static const List<_Case> _cases = [ + ( + company: 'Headspace', + quote: '30-40% increase in developer productivity after adopting Flutter', + storyUrl: '/showcase/headspace', + theme: 'headspace', + isDark: false, + ), + ( + company: 'Universal', + quote: + '45% reduction in codebase size and near-zero crashes with Flutter', + storyUrl: '/showcase/universal-studios', + theme: 'universal', + isDark: false, + ), + ( + company: 'LG WebOS', + quote: '2× faster launch times with lower runtime memory usage.', + storyUrl: '/showcase/lg-electronics', + theme: 'lg', + isDark: false, + ), + ( + company: 'Google Earth', + quote: + '3x faster. Flutter has vastly increased our ability to deliver ' + 'multi-platform', + storyUrl: null, + theme: 'earth', + isDark: true, + ), + ]; + + int _index = 0; + + void _step(int delta) { + final strip = web.document.querySelector('#why-flutter-case-strip'); + if (strip != null && web.window.innerWidth < 768) { + strip.scrollBy( + web.ScrollToOptions( + left: (delta * (web.window.innerWidth - 32)).toDouble(), + behavior: 'smooth', + ), + ); + return; + } + + setState(() { + _index = (_index + delta).clamp(0, _cases.length - 1); + }); + } + + @override + Component build(BuildContext context) { + return section(id: 'success-cases', classes: 'module why-flutter-cases', [ + const div(classes: 'stacked-header container', [ + p(classes: 'why-flutter-kicker', [.text('Success cases')]), + h2([.text('How Flutter helps your business scale')]), + p(classes: 'why-flutter-sub', [ + .text('The measurable ROI teams achieved after selecting Flutter.'), + ]), + ]), + div(classes: 'why-flutter-case-viewport', [ + div( + classes: 'why-flutter-case-track', + styles: Styles( + raw: {'transform': 'translateX(${-_index * _cardStride}px)'}, + ), + [ + for (final study in _cases) + _CaseCard(study: study, media: component.media), + ], + ), + ]), + div(id: 'why-flutter-case-strip', classes: 'why-flutter-case-strip', [ + for (final study in _cases) + _CaseCard(study: study, media: component.media, isCompact: true), + ]), + div(classes: 'why-flutter-case-arrows', [ + _ArrowButton( + symbol: 'arrow_back', + label: 'Previous success case', + onPressed: () => _step(-1), + ), + _ArrowButton( + symbol: 'arrow_forward', + label: 'Next success case', + onPressed: () => _step(1), + ), + ]), + ]); + } +} + +/// A single success-case card. +/// +/// The [isCompact] variant is the vertical card used in the small-viewport +/// snap-scroll strip. +class _CaseCard extends StatelessComponent { + const _CaseCard({ + required this.study, + required this.media, + this.isCompact = false, + }); + + final _Case study; + final Map media; + final bool isCompact; + + @override + Component build(BuildContext context) { + return div( + classes: [ + if (isCompact) + 'why-flutter-case-card-compact' + else + 'why-flutter-case-card', + 'theme-${study.theme}', + if (study.isDark) 'is-dark', + ].join(' '), + [ + div(classes: 'why-flutter-case-copy', [ + p(classes: 'why-flutter-case-company', [.text(study.company)]), + p(classes: 'why-flutter-case-quote', [.text(study.quote)]), + if (study.storyUrl case final url?) + a(href: url, classes: 'why-flutter-case-link', const [ + .text('Read story'), + Icon.linkArrow(), + ]), + ]), + div(classes: 'why-flutter-case-media', [ + _CaseMedia(study: study, media: media), + ]), + ], + ); + } +} + +/// The device imagery that overflows the top edge of each success-case card. +class _CaseMedia extends StatelessComponent { + const _CaseMedia({required this.study, required this.media}); + + final _Case study; + final Map media; + + @override + Component build(BuildContext context) { + return switch (study.theme) { + 'earth' => img( + src: media['earth']!, + alt: 'Google Earth running on desktop', + ), + 'lg' => img(src: media['lg']!, alt: 'LG webOS television'), + final theme => .fragment([ + img( + src: media[theme]!, + alt: '${study.company} app', + classes: 'why-flutter-case-screenshot', + ), + img( + src: media['phoneFrame']!, + alt: '', + classes: 'why-flutter-case-frame', + ), + ]), + }; + } +} + +/// A circular carousel arrow button. +class _ArrowButton extends StatelessComponent { + const _ArrowButton({ + required this.symbol, + required this.label, + required this.onPressed, + }); + + final String symbol; + final String label; + final void Function() onPressed; + + @override + Component build(BuildContext context) { + return button( + classes: 'why-flutter-arrow', + attributes: {'aria-label': label}, + onClick: onPressed, + [Icon(symbol: symbol)], + ); + } +} diff --git a/sites/www/lib/src/components/sections/why_flutter_code_shared_section.dart b/sites/www/lib/src/components/sections/why_flutter_code_shared_section.dart new file mode 100644 index 00000000000..f82cc8e1973 --- /dev/null +++ b/sites/www/lib/src/components/sections/why_flutter_code_shared_section.dart @@ -0,0 +1,97 @@ +// Copyright 2026, the Flutter authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import '../../utils/visibility_observer.dart'; + +/// On scrolling into view the Dash-at-laptop sprite plays through once, and +/// the percentage begins counting up 1.5 seconds later. +@client +class WhyFlutterCodeSharedSection extends StatefulComponent { + const WhyFlutterCodeSharedSection({required this.laptopSprite, super.key}); + + /// Resolved URL of the Dash-at-laptop sprite sheet. + final String laptopSprite; + + @override + State createState() => + _WhyFlutterCodeSharedSectionState(); +} + +class _WhyFlutterCodeSharedSectionState + extends State { + static const _sharedPercentage = 97; + + bool _isSpriteRunning = false; + int _percentage = 0; + + @override + void initState() { + super.initState(); + if (!kIsWeb) return; + + observeOnce('#why-flutter-code-shared', () { + setState(() => _isSpriteRunning = true); + + Timer(const Duration(milliseconds: 1500), () { + animateValue( + duration: const Duration(milliseconds: 1600), + onTick: (fraction) { + setState(() { + _percentage = (_sharedPercentage * fraction).round(); + }); + }, + ); + }); + }); + } + + @override + Component build(BuildContext context) { + return section(id: 'code-shared', classes: 'module why-flutter-shared', [ + div(classes: 'why-flutter-row why-flutter-row-reverse container', [ + div(classes: 'why-flutter-shared-media', [ + div( + id: 'why-flutter-code-shared', + classes: 'why-flutter-shared-stat', + [ + const p(classes: 'why-flutter-kicker', [.text('Code shared')]), + span(classes: 'why-flutter-stat-number', [ + .text('$_percentage%'), + ]), + ], + ), + div( + classes: [ + 'why-flutter-laptop-sprite', + if (_isSpriteRunning) 'is-running', + ].join(' '), + styles: Styles( + raw: { + '--why-flutter-sprite': 'url(${component.laptopSprite})', + }, + ), + const [], + ), + ]), + const div(classes: 'why-flutter-shared-copy', [ + h2([.text('One codebase, one team')]), + p(classes: 'why-flutter-body', [ + .text( + 'Eliminate the cost and overhead of maintaining seperate apps, codebases, and development teams.', + ), + ]), + a( + href: '/showcase', + classes: 'btn why-flutter-outline-btn', + [.text('Read our showcases')], + ), + ]), + ]), + ]); + } +} diff --git a/sites/www/lib/src/components/sections/why_flutter_features_section.dart b/sites/www/lib/src/components/sections/why_flutter_features_section.dart new file mode 100644 index 00000000000..e75e8b6e104 --- /dev/null +++ b/sites/www/lib/src/components/sections/why_flutter_features_section.dart @@ -0,0 +1,99 @@ +// Copyright 2026, the Flutter authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. + +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; + +import '../common/icon.dart'; + +/// Describes a single "Every pixel, on brand" feature card. +typedef _Feature = ({String symbol, String tint, String title, String body}); + +/// The "Every pixel, on brand" section. +/// +/// A 2x2 grid on wide viewports that becomes a horizontal snap-scroll carousel +/// on small ones. The decorative blobs animate purely in CSS on hover, so this +/// section needs no client-side code. +class WhyFlutterFeaturesSection extends StatelessComponent { + const WhyFlutterFeaturesSection({super.key}); + + static const List<_Feature> _features = [ + ( + symbol: 'design_services', + tint: 'pixel', + title: 'Pixel-Perfect Design', + body: + 'Flutter renders every pixel itself, giving you complete creative ' + 'freedom without being limited by native UI components.', + ), + ( + symbol: 'speed', + tint: 'performance', + title: 'Consistent Performance', + body: + "Flutter's rendering engine delivers the same polished UI at 60 or " + '120 FPS across phones, tablets, desktops, and embedded devices.', + ), + ( + symbol: 'insights', + tint: 'smooth', + title: 'Smooth experiences', + body: + "Flutter's Impeller renderer eliminates jank, delivering fluid " + 'animations and interactions on every platform.', + ), + ( + symbol: 'hub', + tint: 'system', + title: 'Tools included', + body: + 'Integrated IDE, debugging, DevTools, and platform-specific tools, ' + 'with an AI-powered development workflow.', + ), + ]; + + @override + Component build(BuildContext context) { + return section( + id: 'design-fidelity', + classes: 'module why-flutter-features', + [ + const div(classes: 'stacked-header container', [ + p(classes: 'why-flutter-kicker', [.text('Design fidelity')]), + h2([.text('Every pixel, on brand')]), + p(classes: 'why-flutter-sub', [ + .text( + "Don't compromise your brand. Control every pixel with a single " + 'rendering engine.', + ), + ]), + ]), + div(classes: 'why-flutter-feature-grid', [ + for (final feature in _features) _FeatureCard(feature: feature), + ]), + ], + ); + } +} + +/// A single feature card, including the two blurred blobs that orbit its +/// corners while hovered. +class _FeatureCard extends StatelessComponent { + const _FeatureCard({required this.feature}); + + final _Feature feature; + + @override + Component build(BuildContext context) { + return div(classes: 'why-flutter-feature-card tint-${feature.tint}', [ + const div(classes: 'why-flutter-blob why-flutter-blob-top', []), + div(classes: 'why-flutter-feature-icon', [Icon(symbol: feature.symbol)]), + div(classes: 'why-flutter-feature-body', [ + h3([.text(feature.title)]), + p([.text(feature.body)]), + ]), + const div(classes: 'why-flutter-blob why-flutter-blob-bottom', []), + ]); + } +} diff --git a/sites/www/lib/src/components/sections/why_flutter_hero_section.dart b/sites/www/lib/src/components/sections/why_flutter_hero_section.dart new file mode 100644 index 00000000000..1aa3e38134c --- /dev/null +++ b/sites/www/lib/src/components/sections/why_flutter_hero_section.dart @@ -0,0 +1,230 @@ +// Copyright 2026, the Flutter authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. + +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/analytics.dart'; + +import '../../utils/visibility_observer.dart'; +import '../common/icon.dart'; + +/// The "Why Flutter" hero, combined with the "Faster time-to-market" productivity +/// chart that shares its gradient background. +/// +/// The chart's bar and its 3X counter animate once the chart scrolls into +/// view, so the section hydrates on the client. Asset URLs are resolved by the +/// page and passed in, because the asset helpers are server-only. +@client +class WhyFlutterHeroSection extends StatefulComponent { + const WhyFlutterHeroSection({ + required this.typingSprite, + required this.screens, + required this.whitepaperUrl, + required this.flutterLogo, + super.key, + }); + + /// Resolved URL of the typing Dash sprite sheet. + final String typingSprite; + + /// Resolved URLs of the twelve app screenshots, in column-major order. + final List screens; + + /// Resolved URL of the downloadable business whitepaper. + final String whitepaperUrl; + + /// Resolved URL of the Flutter logo mark for the chart bar. + final String flutterLogo; + + @override + State createState() => _WhyFlutterHeroSectionState(); +} + +class _WhyFlutterHeroSectionState extends State { + static const _productivityMultiple = 3; + + bool _isBarGrown = false; + int _multiple = 0; + + @override + void initState() { + super.initState(); + if (!kIsWeb) return; + + observeOnce('#why-flutter-chart', threshold: 0.35, () { + setState(() => _isBarGrown = true); + + animateValue( + duration: const Duration(milliseconds: 1600), + onTick: (fraction) { + setState(() { + _multiple = (_productivityMultiple * fraction).round(); + }); + }, + ); + }); + } + + @override + Component build(BuildContext context) { + return section(id: 'why-flutter-hero', classes: 'why-flutter-hero', [ + const div(classes: 'why-flutter-hero-glow', []), + div(classes: 'why-flutter-hero-stage', [ + _HeroCopy(whitepaperUrl: component.whitepaperUrl), + _ScreenCarousel(screens: component.screens), + div( + classes: 'why-flutter-hero-dash', + styles: Styles( + raw: {'--why-flutter-sprite': 'url(${component.typingSprite})'}, + ), + const [], + ), + _ProductivityChart( + isBarGrown: _isBarGrown, + multiple: _multiple, + flutterLogo: component.flutterLogo, + ), + ]), + ]); + } +} + +/// The hero headline, supporting copy, and whitepaper download button. +class _HeroCopy extends StatelessComponent { + const _HeroCopy({required this.whitepaperUrl}); + + final String whitepaperUrl; + + @override + Component build(BuildContext context) { + return div(classes: 'why-flutter-hero-copy', [ + const h1([.text('The business value of Flutter')]), + const p([ + .text( + 'Discover how unifying your codebase empowers you to ship to iOS, ' + 'Android, Web, and Desktop faster and more efficiently than ever ' + 'before.', + ), + ]), + a( + href: whitepaperUrl, + classes: 'btn why-flutter-hero-btn', + attributes: const {'download': ''}, + events: events( + onClick: () { + analytics.sendEvent('whitepaper_download', { + 'file': 'flutter-whitepaper-2026.pdf', + }); + }, + ), + const [Icon(symbol: 'download'), .text('Download 2026 Whitepaper')], + ), + ]); + } +} + +/// Three columns of app screenshots that loop vertically, forever. +/// +/// Each column repeats its screens twice so the CSS translation restarts +/// seamlessly. +class _ScreenCarousel extends StatelessComponent { + const _ScreenCarousel({required this.screens}); + + static const _columnCount = 3; + + final List screens; + + @override + Component build(BuildContext context) { + return div(classes: 'why-flutter-screens', [ + div(classes: 'why-flutter-screens-track', [ + for (var column = 0; column < _columnCount; column++) + div(classes: 'why-flutter-screen-column column-${column + 1}', [ + for (var pass = 0; pass < 2; pass++) + for ( + var index = column; + index < screens.length; + index += _columnCount + ) + img( + src: screens[index], + alt: '', + attributes: const {'aria-hidden': 'true'}, + ), + ]), + ]), + ]); + } +} + +/// The "Faster time-to-market" block and its productivity bar chart. +class _ProductivityChart extends StatelessComponent { + const _ProductivityChart({ + required this.isBarGrown, + required this.multiple, + required this.flutterLogo, + }); + + final bool isBarGrown; + final int multiple; + final String flutterLogo; + + @override + Component build(BuildContext context) { + return div(classes: 'why-flutter-productivity', [ + const h2([.text('Faster time-to-market')]), + const p(classes: 'why-flutter-sub', [ + .text( + 'Consolidation is the answer. Build once and launch seamlessly ' + 'across ', + ), + span(classes: 'accent', [.text('iOS')]), + .text(', '), + span(classes: 'accent', [.text('Android')]), + .text(', '), + span(classes: 'accent', [.text('web')]), + .text(', and '), + span(classes: 'accent', [.text('desktop')]), + .text('.'), + ]), + const p(classes: 'why-flutter-chart-label', [.text('Productivity')]), + div(id: 'why-flutter-chart', classes: 'why-flutter-chart', [ + div(classes: 'why-flutter-chart-plot', [ + div(classes: 'why-flutter-chart-grid', [ + for (var line = 0; line < 8; line++) const div([]), + ]), + const div(classes: 'why-flutter-bar why-flutter-bar-baseline', []), + div( + classes: [ + 'why-flutter-bar', + 'why-flutter-bar-flutter', + if (isBarGrown) 'is-grown', + ].join(' '), + [ + img( + src: flutterLogo, + alt: 'Flutter', + classes: 'why-flutter-bar-logo', + ), + ], + ), + const div(classes: 'why-flutter-chart-axis', []), + span(classes: 'why-flutter-chart-multiple', [.text('${multiple}X')]), + const p(classes: 'why-flutter-bar-label label-baseline', [ + .text('Traditional baseline'), + ]), + const p(classes: 'why-flutter-bar-label label-flutter', [ + .text('Teams with Flutter'), + ]), + const p(classes: 'why-flutter-chart-axis-label', [ + .text('Productivity'), + ]), + const div(classes: 'why-flutter-chart-badge', [ + Icon(symbol: 'arrow_upward'), + ]), + ]), + ]), + ]); + } +} diff --git a/sites/www/lib/src/components/sections/why_flutter_resources_section.dart b/sites/www/lib/src/components/sections/why_flutter_resources_section.dart new file mode 100644 index 00000000000..41559b676ad --- /dev/null +++ b/sites/www/lib/src/components/sections/why_flutter_resources_section.dart @@ -0,0 +1,97 @@ +// Copyright 2026, the Flutter authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. + +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; + +import '../../utils/asset_utils.dart'; +import '../common/icon.dart'; + +/// Describes a single third-party research report. +typedef _Resource = ({ + String href, + String logo, + String logoAlt, + String title, + String summary, +}); + +/// The "Don't take our word for it" section, linking out to third-party +/// research on Flutter's business value. +/// +/// All hover behaviour is CSS, so this section stays server-rendered. +class WhyFlutterResourcesSection extends StatelessComponent { + const WhyFlutterResourcesSection({super.key}); + + static const List<_Resource> _resources = [ + ( + href: 'https://leancode.co/insights/flutter-cto-report', + logo: 'leancode.svg', + logoAlt: 'LeanCode', + title: 'Flutter CTO Report by LeanCode', + summary: 'What 300+ CTOs say about Flutter in production.', + ), + ( + href: 'https://verygood.ventures/whitepapers/business-value-of-flutter/', + logo: 'vgvlogo.svg', + logoAlt: 'Very Good Ventures', + title: "VGV's Flutter Whitepaper by Very Good Ventures", + summary: 'The ROI case, from the team behind top Flutter apps.', + ), + ]; + + @override + Component build(BuildContext context) { + return section(id: 'resources', classes: 'module why-flutter-resources', [ + const div(classes: 'stacked-header container', [ + p(classes: 'why-flutter-kicker', [.text('Resources')]), + h2([.text("Don't take our word for it.")]), + p(classes: 'why-flutter-sub', [ + .text( + 'Discover reports, case studies, and research on building ' + 'with Flutter at scale.', + ), + ]), + ]), + div(classes: 'why-flutter-resource-list container', [ + for (final resource in _resources) _ResourceRow(resource: resource), + ]), + ]); + } +} + +/// A single resource row linking out to an external report. +class _ResourceRow extends StatelessComponent { + const _ResourceRow({required this.resource}); + + final _Resource resource; + + @override + Component build(BuildContext context) { + return a( + href: resource.href, + target: Target.blank, + attributes: const {'rel': 'noopener'}, + classes: 'why-flutter-resource', + [ + img( + src: context.asset('/why-flutter/images/${resource.logo}'), + alt: resource.logoAlt, + classes: 'why-flutter-resource-logo', + ), + div(classes: 'why-flutter-resource-copy', [ + h3([.text(resource.title)]), + p(classes: 'why-flutter-body', [.text(resource.summary)]), + ]), + const div(classes: 'why-flutter-resource-tile', [ + Icon(symbol: 'arrow_outward'), + ]), + const div(classes: 'why-flutter-resource-button', [ + span([.text('Read more')]), + Icon(symbol: 'north_east'), + ]), + ], + ); + } +} diff --git a/sites/www/lib/src/data/nav_items.dart b/sites/www/lib/src/data/nav_items.dart index 82cb93df7d7..2248e9c3ac7 100644 --- a/sites/www/lib/src/data/nav_items.dart +++ b/sites/www/lib/src/data/nav_items.dart @@ -23,6 +23,10 @@ final List headerNavItems = [ NavLink(label: 'Monetization', href: '/monetization'), ], ), + NavItem( + label: 'Why Flutter', + href: '/why-flutter', + ), NavItem( label: 'Ecosystem', href: '/ecosystem', diff --git a/sites/www/lib/src/pages/why_flutter_page.dart b/sites/www/lib/src/pages/why_flutter_page.dart new file mode 100644 index 00000000000..5701284a2c5 --- /dev/null +++ b/sites/www/lib/src/pages/why_flutter_page.dart @@ -0,0 +1,66 @@ +// Copyright 2026, the Flutter authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. + +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; + +import '../components/sections/cta_section.dart'; +import '../components/sections/why_flutter_ai_era_section.dart'; +import '../components/sections/why_flutter_cases_section.dart'; +import '../components/sections/why_flutter_code_shared_section.dart'; +import '../components/sections/why_flutter_features_section.dart'; +import '../components/sections/why_flutter_hero_section.dart'; +import '../components/sections/why_flutter_resources_section.dart'; +import '../utils/asset_utils.dart'; + +/// The "Why Flutter" page, making the business case for adopting Flutter. +/// +/// The site's `DefaultLayout` supplies the header and footer, and the closing +/// call to action reuses the shared [CTASection]. +/// +/// Asset URLs are resolved here and handed to the sections that hydrate on the +/// client. Those sections must not resolve assets themselves: the asset +/// helpers import `dart:io`, which would pull the server-only SDK into the +/// client bundle and stop it compiling. +class WhyFlutterPage extends StatelessComponent { + const WhyFlutterPage({super.key}); + + /// Number of app screenshots cycling in the hero. + static const _heroScreenCount = 12; + + @override + Component build(BuildContext context) { + String image(String name) => context.asset('/why-flutter/images/$name'); + + return main_(classes: 'why-flutter', [ + WhyFlutterHeroSection( + typingSprite: image('dash-typing-sprite.webp'), + screens: [ + for (var index = 1; index <= _heroScreenCount; index++) + image('hero-screens/s${index.toString().padLeft(2, '0')}.webp'), + ], + whitepaperUrl: context.asset( + '/why-flutter/flutter-whitepaper-2026.pdf', + ), + flutterLogo: image('flutter-logo-mark.svg'), + ), + WhyFlutterCodeSharedSection( + laptopSprite: image('dash-laptop-sprite-v2.webp'), + ), + const WhyFlutterFeaturesSection(), + WhyFlutterAiEraSection(geminiSprite: image('dash-gemini-sprite.webp')), + WhyFlutterCasesSection( + media: { + 'headspace': image('headspace-app.png'), + 'universal': image('universal-app.png'), + 'phoneFrame': image('phone-frame.png'), + 'earth': image('google-earth-desktop.png'), + 'lg': image('lg-tv.png'), + }, + ), + const WhyFlutterResourcesSection(), + const CTASection(), + ]); + } +} diff --git a/sites/www/lib/src/utils/visibility_observer.dart b/sites/www/lib/src/utils/visibility_observer.dart new file mode 100644 index 00000000000..023a941f34a --- /dev/null +++ b/sites/www/lib/src/utils/visibility_observer.dart @@ -0,0 +1,77 @@ +// Copyright 2026, the Flutter authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:universal_web/web.dart' as web; + +/// Calls [onVisible] once, the first time the element matching [selector] is +/// at least [threshold] visible in the viewport. +/// +/// Used to start the scroll-triggered animations on the "Why us" page. Listens +/// through [web.EventStreamProviders] rather than `addEventListener` so the +/// file stays compilable for the server during pre-rendering. +void observeOnce( + String selector, + void Function() onVisible, { + double threshold = 0.5, +}) { + StreamSubscription? scrollSubscription; + StreamSubscription? resizeSubscription; + var hasFired = false; + + void check() { + if (hasFired) return; + + final element = web.document.querySelector(selector); + if (element == null) return; + + final rect = element.getBoundingClientRect(); + final height = rect.height; + if (height <= 0) return; + + final viewportHeight = web.window.innerHeight; + final visibleTop = rect.top.clamp(0, viewportHeight); + final visibleBottom = rect.bottom.clamp(0, viewportHeight); + + if ((visibleBottom - visibleTop) / height >= threshold) { + hasFired = true; + unawaited(scrollSubscription?.cancel() ?? Future.value()); + unawaited(resizeSubscription?.cancel() ?? Future.value()); + onVisible(); + } + } + + scrollSubscription = web.EventStreamProviders.scrollEvent + .forTarget(web.window) + .listen((_) => check()); + resizeSubscription = web.EventStreamProviders.resizeEvent + .forTarget(web.window) + .listen((_) => check()); + + // Catch elements that are already on screen when the page loads. + Timer(Duration.zero, check); +} + +/// Drives an ease-out-cubic tween over [duration], invoking [onTick] with the +/// eased fraction between 0 and 1 until it completes. +void animateValue({ + required Duration duration, + required void Function(double easedFraction) onTick, +}) { + const frameInterval = Duration(milliseconds: 16); + final elapsed = Stopwatch()..start(); + + Timer.periodic(frameInterval, (timer) { + final fraction = (elapsed.elapsedMilliseconds / duration.inMilliseconds) + .clamp(0.0, 1.0); + final remaining = 1 - fraction; + onTick(1 - (remaining * remaining * remaining)); + + if (fraction >= 1) { + timer.cancel(); + elapsed.stop(); + } + }); +} diff --git a/sites/www/lib/styles/pages/_why_flutter.scss b/sites/www/lib/styles/pages/_why_flutter.scss new file mode 100644 index 00000000000..10861c3a419 --- /dev/null +++ b/sites/www/lib/styles/pages/_why_flutter.scss @@ -0,0 +1,992 @@ +@use '../core/breakpoints'; + +// Styles for the "Why Flutter" page (`/why-flutter`). +// +// Written mobile-first to match the rest of the site, so the small-viewport +// layout is the base and each `breakpoints.screen()` block layers the wider +// treatments on top. Colour, type, radius and spacing all resolve to the +// tokens in `core/_vars.scss`; only values with no token equivalent — the +// stat numerals and the hero stage geometry — are literals. + +.why-flutter { + --why-flutter-hero-stage-height: 1616px; + --why-flutter-screen-width: 200px; + --why-flutter-screen-height: 436.667px; + --why-flutter-screen-gap: 29px; + --why-flutter-case-stride: 1220px; + + // Shared section furniture. + .stacked-header { + margin-bottom: var(--spacer); + + .why-flutter-kicker { + margin-bottom: var(--spacer-sm); + } + + h2 { + margin-bottom: 16px; + } + + .why-flutter-sub { + margin-bottom: 0; + max-width: 840px; + } + } + + .why-flutter-kicker { + color: var(--primary); + font-size: var(--font-size-heading-4); + font-weight: var(--font-weight-bold); + line-height: 1.2; + margin: 0; + } + + .why-flutter-sub { + color: var(--grey-6); + font-size: var(--font-size-heading-2); + line-height: 1.2; + margin: 0; + } + + .why-flutter-body { + color: var(--site-base-fgColor); + font-size: var(--font-size-heading-4); + line-height: 1.4; + } + + h2 { + font-size: var(--font-size-heading-1); + font-weight: var(--font-weight-bold); + letter-spacing: -0.3px; + line-height: 1.1; + margin: 0; + } +} + +// ── Hero + productivity chart ────────────────────────────────────────────── + +.why-flutter-hero { + background: linear-gradient(180deg, var(--blue-4) 0%, var(--blue-8) 100%); + color: white; + overflow: hidden; + position: relative; + + // `core/_base.scss` sets `color: black` directly on h1..h6 and a grey on + // body copy. Inheriting white from the section can't override a direct + // declaration, so the dark-background text restates it. + h1, + h2, + p { + color: white; + } + + .why-flutter-hero-glow { + background: radial-gradient( + ellipse 80% 60% at 50% 45%, + var(--blue-8) 40%, + transparent 100% + ); + filter: blur(70px); + inset: -10%; + pointer-events: none; + position: absolute; + } + + .why-flutter-hero-stage { + display: flex; + flex-direction: column; + position: relative; + } + + .why-flutter-hero-copy { + box-sizing: border-box; + padding: var(--gutter-md) var(--gutter-sm) var(--gutter-sm); + + h1 { + font-size: var(--font-size-heading-1); + font-weight: var(--font-weight-bold); + letter-spacing: -0.35px; + line-height: 1.15; + margin: 0 0 var(--spacer-md); + } + + p { + font-size: var(--font-size-heading-4); + line-height: 1.4; + margin: 0 0 var(--spacer); + max-width: 446px; + } + } + + .why-flutter-hero-btn { + background-color: var(--blue-3); + color: var(--blue-9); + gap: var(--spacer-sm); + + &:hover { + background-color: var(--blue-2); + color: var(--blue-9); + } + } +} + +// Three columns of app screenshots looping vertically, forever. +.why-flutter-screens { + height: 604px; + margin-inline: auto; + overflow: hidden; + width: 672px; + + .why-flutter-screens-track { + display: flex; + gap: 36px; + } + + .why-flutter-screen-column { + display: flex; + flex-direction: column; + gap: var(--why-flutter-screen-gap); + will-change: transform; + + img { + border-radius: 22px; + flex-shrink: 0; + height: var(--why-flutter-screen-height); + object-fit: cover; + width: var(--why-flutter-screen-width); + } + + &.column-1 { animation: why-flutter-scroll-up 46s linear infinite; } + &.column-2 { animation: why-flutter-scroll-down 58s linear infinite; } + &.column-3 { animation: why-flutter-scroll-up 52s linear infinite; } + } +} + +@keyframes why-flutter-scroll-up { + from { transform: translateY(0); } + to { transform: translateY(-1862.667px); } +} + +@keyframes why-flutter-scroll-down { + from { transform: translateY(-1862.667px); } + to { transform: translateY(0); } +} + +// Sprite sheets. Each is a two-row grid stepped through with +// `background-position`; the column count differs per sheet. +.why-flutter-hero-dash, +.why-flutter-laptop-sprite, +.why-flutter-gemini-sprite { + background-image: var(--why-flutter-sprite); + background-repeat: no-repeat; +} + +.why-flutter-hero-dash { + animation: why-flutter-dash-typing 2.125s linear infinite; + background-size: 2600% 200%; + height: 355px; + margin-inline: auto; + width: 437px; +} + +@keyframes why-flutter-dash-typing { + 0% { background-position: 0 0; animation-timing-function: steps(26); } + 50.98% { background-position: -11362px 0; } + 50.981% { background-position: 0 -355px; animation-timing-function: steps(25); } + 100% { background-position: -10925px -355px; } +} + +.why-flutter-productivity { + padding: var(--gutter-md) var(--gutter-sm); + text-align: center; + + .why-flutter-sub { + color: white; + margin-inline: auto; + max-width: 830px; + + .accent { color: var(--green-2); } + } + + .why-flutter-chart-label { + color: var(--green-2); + font-weight: var(--font-weight-bold); + margin-top: var(--spacer-md); + } +} + +.why-flutter-chart { + height: 419px; + margin-inline: auto; + position: relative; + transform: scale(0.49); + transform-origin: top center; + width: 799px; + + .why-flutter-chart-plot { + height: 342px; + left: 0; + position: absolute; + top: 77px; + width: 766px; + } + + .why-flutter-chart-grid { + display: flex; + flex-direction: column; + gap: 36.6px; + left: 60px; + position: absolute; + top: 0; + width: 706px; + + div { + background: rgba(4, 43, 89, 0.35); + height: 1px; + } + } + + .why-flutter-bar { + bottom: 53.7px; + position: absolute; + width: 101.4px; + } + + .why-flutter-bar-baseline { + background: linear-gradient(270deg, #212121 0%, var(--blue-9) 80%); + height: 92.8px; + left: 118.5px; + } + + .why-flutter-bar-flutter { + align-items: center; + background: linear-gradient(180deg, var(--blue-3) 0%, var(--green-2) 84%); + box-sizing: border-box; + display: flex; + flex-direction: column; + height: 0; + justify-content: center; + left: 279.7px; + overflow: hidden; + transition: height 1.6s cubic-bezier(0.22, 1, 0.36, 1); + + &.is-grown { + height: 251.7px; + + .why-flutter-bar-logo { + opacity: 1; + transform: scale(1); + } + } + + .why-flutter-bar-logo { + height: auto; + object-fit: contain; + opacity: 0; + transform: scale(0.8); + transition: opacity 0.4s ease 0.8s, transform 0.4s ease 0.8s; + width: 40px; + } + } + + .why-flutter-chart-axis { + background: var(--blue-8); + height: 2.4px; + left: 60px; + position: absolute; + top: 288.3px; + width: 706px; + } + + .why-flutter-chart-multiple { + color: var(--green-2); + font-size: 220px; + font-weight: 500; + left: 405px; + line-height: 264px; + position: absolute; + top: 8px; + } + + .why-flutter-bar-label { + color: white; + font-size: var(--font-size-default); + line-height: 1.3; + position: absolute; + text-align: center; + top: 302px; + width: 102px; + + &.label-baseline { left: 118px; } + &.label-flutter { left: 280px; } + } + + .why-flutter-chart-axis-label { + color: white; + display: none; + font-size: var(--font-size-default); + left: -38px; + position: absolute; + top: 140px; + transform: rotate(-90deg); + } + + .why-flutter-chart-badge { + align-items: center; + background: #fafad1; + border-radius: 50%; + display: flex; + height: 112.2px; + justify-content: center; + left: 653.5px; + position: absolute; + top: 0; + width: 112.2px; + + .icon { + color: var(--blue-8); + font-size: 55px; + } + } +} + +// ── One team, not three ──────────────────────────────────────────────────── + +.why-flutter-row { + align-items: center; + display: flex; + flex-direction: column; + gap: var(--spacer); +} + +// The media column is first in the DOM; stacked, the copy should lead. +.why-flutter-row-reverse { flex-direction: column-reverse; } + +.why-flutter-shared-media { + flex-shrink: 0; + height: 458px; + position: relative; + transform: scale(0.78); + width: 471px; + + .why-flutter-shared-stat { + align-items: center; + display: flex; + flex-direction: column; + gap: var(--spacer-sm); + left: 0; + position: absolute; + top: 70px; + width: 100%; + } + + .why-flutter-stat-number { + background: linear-gradient(to top, #f4fcfe 0%, #88dff9 100%); + background-clip: text; + color: transparent; + font-size: 232px; + font-weight: 500; + letter-spacing: -0.02em; + line-height: 0.8; + } +} + +.why-flutter-laptop-sprite { + background-size: 3600% 200%; + height: 247px; + left: 64.5px; + mask-image: radial-gradient( + ellipse 72% 72% at 50% 52%, + #000 62%, + transparent 98% + ); + position: absolute; + top: 212px; + width: 342px; + + &.is-running { animation: why-flutter-dash-laptop 3s linear forwards; } +} + +@keyframes why-flutter-dash-laptop { + 0% { background-position: 0 0; animation-timing-function: steps(36); } + 50.704% { background-position: -12312px 0; } + 50.705% { background-position: 0 -247px; animation-timing-function: steps(35); } + 100% { background-position: -11970px -247px; } +} + +.why-flutter-shared-copy, +.why-flutter-ai-copy { + display: flex; + flex: 1; + flex-direction: column; + gap: var(--spacer-md); + min-width: 0; +} + +.why-flutter-outline-btn { + align-self: flex-start; + box-shadow: inset 0 0 0 1px var(--primary); + color: var(--primary); + + &:hover { + background-color: var(--primary); + color: white; + } +} + +// ── Every pixel, on brand ────────────────────────────────────────────────── + +.why-flutter-feature-grid { + box-sizing: border-box; + display: flex; + gap: 16px; + overflow-x: auto; + padding-inline: var(--gutter-sm); + scroll-snap-type: x mandatory; + scrollbar-width: none; + + &::-webkit-scrollbar { display: none; } +} + +.why-flutter-feature-card { + background: white; + border-radius: var(--ui-border-radius-sm); + box-shadow: inset 0 0 0 1px #e5f0fc; + box-sizing: border-box; + display: flex; + flex-direction: column; + flex-shrink: 0; + gap: var(--spacer); + height: 480px; + overflow: hidden; + padding: var(--spacer-md); + position: relative; + scroll-snap-align: center; + width: 320px; + + h3 { + color: var(--blue-9); + font-size: var(--font-size-heading-3); + font-weight: var(--font-weight-bold); + line-height: 1.2; + margin: 0; + } + + p { + color: var(--site-base-fgColor); + font-size: var(--font-size-heading-4); + line-height: 1.4; + margin: 0; + } + + // Tints and hover blob colours, one pair per card. + &.tint-pixel { --why-flutter-tint: #ebfafd; --why-flutter-blob: var(--blue-3); } + &.tint-performance { --why-flutter-tint: #fcfcf2; --why-flutter-blob: #fafad1; } + &.tint-system { --why-flutter-tint: #eeeafe; --why-flutter-blob: var(--violet-1); } + &.tint-smooth { --why-flutter-tint: #edfffb; --why-flutter-blob: var(--green-2); } + + &:hover .why-flutter-blob { + animation-play-state: running; + opacity: 0.65; + } +} + +.why-flutter-feature-icon { + align-items: center; + background: var(--why-flutter-tint); + border-radius: var(--ui-border-radius-sm); + display: flex; + height: 72px; + justify-content: center; + position: relative; + width: 72px; + + .icon { + color: var(--blue-9); + font-size: 32px; + } +} + +.why-flutter-feature-body { + display: flex; + flex-direction: column; + gap: var(--spacer-md); + position: relative; +} + +.why-flutter-blob { + animation-play-state: paused; + background: var(--why-flutter-blob); + border-radius: 50%; + filter: blur(55px); + opacity: 0; + pointer-events: none; + position: absolute; + transition: opacity 0.5s; +} + +.why-flutter-blob-top { + animation: why-flutter-orbit-a 8.4s linear infinite; + height: 78px; + right: -25px; + top: -30px; + width: 190px; +} + +.why-flutter-blob-bottom { + animation: why-flutter-orbit-b 8.4s linear infinite; + bottom: -35px; + height: 112px; + left: -21px; + width: 154px; +} + +@keyframes why-flutter-orbit-a { + 0% { transform: translate(0, 0); } + 25% { transform: translate(-330px, 90px); } + 50% { transform: translate(-330px, 430px); } + 75% { transform: translate(0, 430px); } + 100% { transform: translate(0, 0); } +} + +@keyframes why-flutter-orbit-b { + 0% { transform: translate(0, 0); } + 25% { transform: translate(330px, 0); } + 50% { transform: translate(330px, -430px); } + 75% { transform: translate(0, -430px); } + 100% { transform: translate(0, 0); } +} + +// ── Built for the AI era ─────────────────────────────────────────────────── + +.why-flutter-chips { + display: flex; + flex-wrap: wrap; + gap: var(--spacer-sm); + justify-content: center; +} + +.why-flutter-chip { + align-items: center; + background: #f2f9f5; + border-radius: 49px; + display: flex; + gap: var(--spacer-sm); + padding: 4px var(--gutter-sm) 4px 4px; + + // Child combinator: a descendant selector would also match the check glyph + // and override the icon colour set below. + > span { + font-size: var(--font-size-heading-4); + } +} + +.why-flutter-chip-check { + align-items: center; + background: white; + border-radius: 50%; + display: flex; + height: 32px; + justify-content: center; + width: 32px; + + .icon { + color: var(--green-1); + font-size: 16px; + } +} + +.why-flutter-gemini-sprite { + background-size: 5100% 200%; + height: 366px; + margin-inline: auto; + width: 318px; + + &.is-running { animation: why-flutter-dash-gemini 4.208s linear infinite; } +} + +@keyframes why-flutter-dash-gemini { + 0% { background-position: 0 0; animation-timing-function: steps(51); } + 50.495% { background-position: -16218px 0; } + 50.496% { background-position: 0 -366px; animation-timing-function: steps(50); } + 100% { background-position: -15900px -366px; } +} + +// ── Success cases ────────────────────────────────────────────────────────── + +.why-flutter-case-viewport { display: none; } + +.why-flutter-case-strip { + display: flex; + gap: 16px; + overflow-x: auto; + padding: var(--spacer) var(--gutter-sm) var(--gutter-sm); + scroll-snap-type: x mandatory; + scrollbar-width: none; + -webkit-overflow-scrolling: touch; + + &::-webkit-scrollbar { display: none; } +} + +.why-flutter-case-card, +.why-flutter-case-card-compact { + border-radius: var(--ui-border-radius); + position: relative; + + // One gradient per company, matching each brand's showcase story. + &.theme-headspace { background: linear-gradient(90deg, #e3faff 0%, #f9eac6 100%); } + &.theme-earth { background: linear-gradient(95.966deg, #02244c 25.93%, var(--blue-6) 110.29%); } + &.theme-universal { background: linear-gradient(104.186deg, #e3faff 2.03%, #c0deff 119.68%); } + &.theme-lg { background: linear-gradient(97.655deg, #ffeeee 24.42%, #ff6767 117.82%); } + + &.is-dark .why-flutter-case-company, + &.is-dark .why-flutter-case-quote { color: white; } +} + +.why-flutter-case-card-compact { + box-sizing: border-box; + display: flex; + flex-direction: column; + flex-shrink: 0; + gap: 16px; + height: 580px; + overflow: hidden; + padding: var(--spacer) var(--spacer-md) 0; + scroll-snap-align: center; + width: min(350px, calc(100vw - 64px)); + + .why-flutter-case-quote { + font-size: var(--font-size-heading-3); + line-height: 1.3; + } + + .why-flutter-case-media { + bottom: -8px; + height: 275px; + left: 50%; + overflow: hidden; + position: absolute; + transform: translateX(-50%); + width: 270px; + } +} + +.why-flutter-case-company { + color: var(--site-base-fgColor); + font-size: var(--font-size-heading-4); + font-weight: var(--font-weight-bold); + margin: 0; +} + +.why-flutter-case-quote { + color: var(--site-base-fgColor); + font-size: var(--font-size-heading-2); + letter-spacing: -0.18px; + line-height: 1.2; + margin: 0; +} + +// Explicit colour: this sits inside an ``, so it would otherwise inherit +// the global link colour. +.why-flutter-case-link { + align-items: center; + color: var(--site-base-fgColor); + display: flex; + font-size: var(--font-size-default); + font-weight: var(--font-weight-bold); + gap: 4px; + + &:hover { color: var(--site-base-fgColor); } +} + +.why-flutter-case-media { + position: relative; + + img { + display: block; + position: absolute; + } + + // Percentages of the 385px-square media box, matching the design's + // 347px frame over a 313px screenshot. + .why-flutter-case-frame { + left: 1.3%; + top: 3.1%; + width: 90.1%; + } + + .why-flutter-case-screenshot { + border-radius: 22px; + left: 5.5%; + top: 5.7%; + width: 81.3%; + } +} + +.why-flutter-case-arrows { + display: flex; + gap: var(--gutter-sm); + justify-content: center; + padding-bottom: var(--spacer); +} + +.why-flutter-arrow { + align-items: center; + background: white; + border: none; + border-radius: 50%; + box-shadow: 0 0 0 2px #e1e1e1; + cursor: pointer; + display: flex; + height: 56px; + justify-content: center; + transition: background-color 0.2s, box-shadow 0.2s; + width: 56px; + + &:hover { + background: var(--grey-1); + box-shadow: 0 0 0 2px var(--grey-1); + } + + .icon { color: var(--grey-6); } +} + +// ── Resources ────────────────────────────────────────────────────────────── + +.why-flutter-resource-list { + display: flex; + flex-direction: column; + gap: var(--spacer-md); +} + +.why-flutter-resource { + align-items: flex-start; + background: var(--grey-1); + border-radius: var(--ui-border-radius); + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 16px; + padding: var(--gutter-sm); + transition: background-color 0.2s; + + &:hover { background: var(--grey-2); } + + h3 { + color: var(--blue-9); + font-size: var(--font-size-heading-3); + font-weight: var(--font-weight-bold); + line-height: 1.2; + margin: 0 0 6px; + } + + p { margin: 0; } +} + +.why-flutter-resource-logo { + border-radius: 12px; + display: block; + flex-shrink: 0; + height: 120px; + width: 120px; +} + +.why-flutter-resource-copy { + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; + min-width: 0; +} + +.why-flutter-resource-tile { display: none; } + +.why-flutter-resource-button { + align-items: center; + align-self: stretch; + background: var(--blue-3); + border-radius: var(--ui-border-radius); + color: var(--blue-9); + display: flex; + gap: var(--spacer-sm); + height: 48px; + justify-content: center; + margin-top: var(--spacer-sm); +} + +// ═══ Medium and up ═════════════════════════════════════════════════════════ + +@include breakpoints.screen(md) { + .why-flutter-row { + align-items: center; + display: flex; + flex-direction: row; + gap: var(--gutter-md); + justify-content: space-between; + } + + // Media first in the DOM already yields media-left at this width. + .why-flutter-row-reverse { flex-direction: row; } + + .why-flutter-shared-media { transform: none; } + + .why-flutter-productivity .why-flutter-chart-label { display: none; } + + .why-flutter-chart { + transform: none; + + .why-flutter-chart-axis-label { display: block; } + } + + .why-flutter-feature-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 557px)); + justify-content: center; + margin-inline: auto; + max-width: 1130px; + overflow: visible; + padding-inline: 0; + } + + .why-flutter-feature-card { height: 400px; width: auto; } + + .why-flutter-chips { justify-content: flex-start; } + + // The desktop carousel replaces the snap-scroll strip. + .why-flutter-case-viewport { + display: block; + margin-inline: auto; + max-width: 1180px; + overflow: hidden; + padding-block: var(--gutter-lg) var(--gutter-md); + } + + .why-flutter-case-strip { display: none; } + + .why-flutter-case-track { + display: flex; + gap: var(--spacer); + transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1); + } + + .why-flutter-case-card { + align-items: flex-start; + display: flex; + flex-shrink: 0; + height: 328px; + width: 1180px; + + .why-flutter-case-copy { + box-sizing: border-box; + display: flex; + flex: 1; + flex-direction: column; + height: 318px; + justify-content: space-between; + padding: var(--gutter-md); + position: relative; + z-index: 2; + } + + .why-flutter-case-quote { max-width: 616px; } + + .why-flutter-case-media { + height: 385px; + overflow: hidden; + position: absolute; + right: 0; + top: -57px; + width: 385px; + } + + &.theme-earth .why-flutter-case-media, + &.theme-lg .why-flutter-case-media { + width: 487px; + + img { + height: 100%; + left: 0; + object-fit: cover; + top: 0; + width: 100%; + } + } + } + + .why-flutter-resource { + align-items: center; + flex-direction: row; + gap: var(--spacer-md); + padding: var(--gutter-sm) var(--spacer-md); + } + + .why-flutter-resource-tile { + align-items: center; + background: var(--blue-3); + border-radius: var(--ui-border-radius-sm); + display: flex; + flex-shrink: 0; + height: 120px; + justify-content: center; + overflow: hidden; + transition: border-radius 0.35s cubic-bezier(0.22, 1, 0.36, 1); + width: 120px; + + .icon { + color: var(--blue-9); + font-size: 38px; + transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); + } + } + + .why-flutter-resource:hover .why-flutter-resource-tile { + border-radius: 60px; + + .icon { transform: translate(6px, -6px); } + } + + .why-flutter-resource-button { display: none; } +} + +// ═══ Large and up ══════════════════════════════════════════════════════════ + +@include breakpoints.screen(lg) { + .why-flutter-hero { + .why-flutter-hero-stage { + height: var(--why-flutter-hero-stage-height); + margin-inline: auto; + width: 1440px; + } + + .why-flutter-hero-copy { + left: 159px; + padding: 0; + position: absolute; + top: 234px; + width: 446px; + } + } + + .why-flutter-screens { + height: 1265px; + left: 748px; + position: absolute; + top: -550px; + + .why-flutter-screens-track { padding-top: 116px; } + } + + .why-flutter-hero-dash { + left: 866px; + position: absolute; + top: 440px; + } + + .why-flutter-productivity { + left: 0; + padding: var(--gutter-md) 0; + position: absolute; + top: 810px; + width: 1440px; + } +} diff --git a/sites/www/lib/styles/styles.scss b/sites/www/lib/styles/styles.scss index 972a5c113c0..6e33c6137b4 100644 --- a/sites/www/lib/styles/styles.scss +++ b/sites/www/lib/styles/styles.scss @@ -50,6 +50,7 @@ @use 'pages/notfound'; @use 'pages/partner'; @use 'pages/showcase'; +@use 'pages/why_flutter'; @use 'package:site_shared/_sass/components/breadcrumbs'; @use 'package:site_shared/_sass/components/blog';