diff --git a/site/lib/_sass/_site.scss b/site/lib/_sass/_site.scss index fed73d29595..4eacff3c4f7 100644 --- a/site/lib/_sass/_site.scss +++ b/site/lib/_sass/_site.scss @@ -35,6 +35,7 @@ @use 'components/sidebar'; @use 'components/side-menu'; @use 'components/site-switcher'; +@use 'components/summary-card'; @use 'components/tabs'; @use 'components/theming'; @use 'components/tooltip'; diff --git a/site/lib/_sass/components/_summary-card.scss b/site/lib/_sass/components/_summary-card.scss new file mode 100644 index 00000000000..0ff63af00bb --- /dev/null +++ b/site/lib/_sass/components/_summary-card.scss @@ -0,0 +1,85 @@ +.summary-card { + background-color: var(--site-raised-bgColor-translucent); + border-radius: var(--site-radius); + border: 1px solid var(--site-inset-borderColor); + + header { + padding: 1rem 1.2rem 1.2rem; + display: flex; + align-items: center; + + >div { + flex: 1; + color: var(--site-base-fgColor-alt); + } + + h3 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--site-base-fgColor); + } + + .summary-card-completed { + color: var(--site-alert-tip-color); + } + } + + .summary-card-item { + display: flex; + align-items: center; + gap: 1rem; + + border-top: 1px solid var(--site-inset-borderColor); + padding: 0.8rem 1.2rem; + + >:first-child { + font-size: 0.875rem; + background-color: var(--site-primary-color-highlight); + color: var(--site-primary-color); + border-radius: 0.5rem; + + padding: 0.5rem; + } + + .summary-card-item-title { + flex: 1; + + font-size: 1rem; + font-weight: 500; + } + } + + details { + margin: 0; + + summary { + margin: 0; + cursor: pointer; + list-style: none; + + >.material-symbols { + transition: transform .25s ease-out; + transform: rotate(180deg); + transform-origin: center; + } + } + + &[open] { + summary>.material-symbols { + transform: rotate(0); + } + } + + .summary-card-item-details { + margin: 0; + border-top: 1px solid var(--site-inset-borderColor); + padding: .8rem 1.2rem; + color: var(--site-base-fgColor-alt); + + >:last-child { + margin-bottom: 0; + } + } + } +} diff --git a/site/lib/main.dart b/site/lib/main.dart index 2b41724cdb7..40aa4d5e137 100644 --- a/site/lib/main.dart +++ b/site/lib/main.dart @@ -21,6 +21,7 @@ import 'src/components/pages/expansion_list.dart'; import 'src/components/pages/learning_resource_index.dart'; import 'src/components/tutorial/progress_ring.dart'; import 'src/components/tutorial/quiz.dart'; +import 'src/components/tutorial/summary_card.dart'; import 'src/components/util/component_ref.dart'; import 'src/extensions/registry.dart'; import 'src/layouts/catalog_page_layout.dart'; @@ -101,6 +102,7 @@ List get _embeddableComponents => [ const FileTree(), const Quiz(), const ProgressRing(), + const SummaryCard(), CustomComponent( pattern: RegExp('OSSelector', caseSensitive: false), builder: (_, _, _) => const OsSelector(), diff --git a/site/lib/src/components/tutorial/summary_card.dart b/site/lib/src/components/tutorial/summary_card.dart new file mode 100644 index 00000000000..7250028fce1 --- /dev/null +++ b/site/lib/src/components/tutorial/summary_card.dart @@ -0,0 +1,83 @@ +// Copyright 2025 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/jaspr.dart'; +import 'package:jaspr_content/jaspr_content.dart'; +import 'package:yaml/yaml.dart'; + +import '../../markdown/markdown_parser.dart'; +import '../../models/summary_card_model.dart'; +import '../common/material_icon.dart'; + +class SummaryCard extends CustomComponent { + const SummaryCard() : super.base(); + + @override + Component? create(Node node, NodesBuilder builder) { + if (node is ElementNode && node.tag.toLowerCase() == 'summarycard') { + if (node.children?.whereType().isNotEmpty ?? false) { + throw Exception( + 'Invalid SummaryCard content. Remove any leading empty lines to ' + 'avoid parsing as markdown.', + ); + } + + final content = node.children?.map((n) => n.innerText).join('\n') ?? ''; + final data = loadYamlNode(content); + assert( + data is YamlMap, + 'Invalid SummaryCard content. Expected a YAML map.', + ); + final model = SummaryCardModel.fromMap(data as YamlMap); + assert( + model.items.isNotEmpty, + 'SummaryCard must contain at least one item.', + ); + return SummaryCardComponent(model: model); + } + return null; + } +} + +class SummaryCardComponent extends StatelessComponent { + const SummaryCardComponent({super.key, required this.model}); + + final SummaryCardModel model; + + @override + Component build(BuildContext context) { + return div(classes: 'summary-card', [ + header([ + div([ + h3([text(model.title)]), + if (model.subtitle case final subtitle?) span([text(subtitle)]), + ]), + if (model.completed) + span(classes: 'summary-card-completed', [ + const MaterialIcon('check_circle'), + ]), + ]), + for (final item in model.items) _buildSummaryItem(item), + ]); + } + + Component _buildSummaryItem(SummaryCardItem item) { + if (item.details case final d?) { + return details([ + summary(classes: 'summary-card-item', [ + span([MaterialIcon(item.icon)]), + span(classes: 'summary-card-item-title', [text(item.title)]), + const MaterialIcon('keyboard_arrow_up'), + ]), + div(classes: 'summary-card-item-details', [ + DashMarkdown(content: d), + ]), + ]); + } + return div(classes: 'summary-card-item', [ + span([MaterialIcon(item.icon)]), + span(classes: 'summary-card-item-title', [text(item.title)]), + ]); + } +} diff --git a/site/lib/src/models/quiz_model.dart b/site/lib/src/models/quiz_model.dart index 4f38e27b687..cdb9beca3ac 100644 --- a/site/lib/src/models/quiz_model.dart +++ b/site/lib/src/models/quiz_model.dart @@ -1,3 +1,7 @@ +// Copyright 2025 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/jaspr.dart'; class Question { diff --git a/site/lib/src/models/summary_card_model.dart b/site/lib/src/models/summary_card_model.dart new file mode 100644 index 00000000000..c604330c6e3 --- /dev/null +++ b/site/lib/src/models/summary_card_model.dart @@ -0,0 +1,67 @@ +// Copyright 2025 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/jaspr.dart'; + +class SummaryCardModel { + const SummaryCardModel({ + required this.title, + this.subtitle, + this.completed = false, + required this.items, + }); + + final String title; + final String? subtitle; + final bool completed; + final List items; + + @decoder + factory SummaryCardModel.fromMap(Map json) { + return SummaryCardModel( + title: json['title'] as String, + subtitle: json['subtitle'] as String?, + completed: json['completed'] as bool? ?? false, + items: (json['items'] as List) + .map((e) => SummaryCardItem.fromMap(e as Map)) + .toList(), + ); + } + + @encoder + Map toJson() => { + 'title': title, + 'subtitle': subtitle, + 'completed': completed, + 'items': items.map((e) => e.toJson()).toList(), + }; +} + +class SummaryCardItem { + const SummaryCardItem({ + required this.title, + required this.icon, + this.details, + }); + + final String title; + final String icon; + final String? details; + + @decoder + factory SummaryCardItem.fromMap(Map json) { + return SummaryCardItem( + title: json['title'] as String, + icon: json['icon'] as String, + details: json['details'] as String?, + ); + } + + @encoder + Map toJson() => { + 'title': title, + 'icon': icon, + 'details': details, + }; +} diff --git a/site/lib/src/pages/custom_pages.dart b/site/lib/src/pages/custom_pages.dart index 2cffefadcad..fb15286045f 100644 --- a/site/lib/src/pages/custom_pages.dart +++ b/site/lib/src/pages/custom_pages.dart @@ -176,5 +176,46 @@ class MyApp extends StatelessWidget { } ``` +## Summary Card + + +title: What you'll learn in this Flutter lesson +items: + - title: Introduction to Flutter and Dart programming + icon: flutter + - title: How to build beautiful UIs with widgets + icon: mobile_layout + - title: Adding navigation between different screens + icon: conversion_path + + +--- + + +title: What you accomplished +subtitle: Here's a summary of what you accomplished in this lesson. +completed: true +items: + - title: Reviewed the core concepts of Flutter + icon: flutter + details: >- + Solidified understanding of Flutter's core concepts, including the widget + tree, state management principles (Stateless vs. Stateful widgets), and + the basic project structure. Reviewed the essentials of the Dart + programming language. + - title: Practiced building layouts with widgets + icon: mobile_layout + details: >- + Built and experimented with common layout widgets (Row, Column, + Stack, and Flex), learned how to use padding, alignment, and + constraints to create responsive layouts across screen sizes. + - title: Implemented screen navigation and routing + icon: conversion_path + details: >- + Implemented navigation between screens using routes and + Navigator patterns; learned how to pass arguments between routes + and manage back navigation and nested navigation scenarios. + + ''', );