Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions site/lib/_sass/_site.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
85 changes: 85 additions & 0 deletions site/lib/_sass/components/_summary-card.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
2 changes: 2 additions & 0 deletions site/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -101,6 +102,7 @@ List<CustomComponent> get _embeddableComponents => [
const FileTree(),
const Quiz(),
const ProgressRing(),
const SummaryCard(),
CustomComponent(
pattern: RegExp('OSSelector', caseSensitive: false),
builder: (_, _, _) => const OsSelector(),
Expand Down
83 changes: 83 additions & 0 deletions site/lib/src/components/tutorial/summary_card.dart
Original file line number Diff line number Diff line change
@@ -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<ElementNode>().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)]),
]);
}
}
4 changes: 4 additions & 0 deletions site/lib/src/models/quiz_model.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
67 changes: 67 additions & 0 deletions site/lib/src/models/summary_card_model.dart
Original file line number Diff line number Diff line change
@@ -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<SummaryCardItem> items;

@decoder
factory SummaryCardModel.fromMap(Map<Object?, Object?> json) {
return SummaryCardModel(
title: json['title'] as String,
subtitle: json['subtitle'] as String?,
completed: json['completed'] as bool? ?? false,
items: (json['items'] as List<Object?>)
.map((e) => SummaryCardItem.fromMap(e as Map<Object?, Object?>))
.toList(),
);
}

@encoder
Map<Object?, Object?> 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<Object?, Object?> json) {
return SummaryCardItem(
title: json['title'] as String,
icon: json['icon'] as String,
details: json['details'] as String?,
);
}

@encoder
Map<Object?, Object?> toJson() => {
'title': title,
'icon': icon,
'details': details,
};
}
41 changes: 41 additions & 0 deletions site/lib/src/pages/custom_pages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,5 +176,46 @@ class MyApp extends StatelessWidget {
}
```

## Summary Card

<SummaryCard>
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
</SummaryCard>

---

<SummaryCard>
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.
</SummaryCard>

''',
);