Skip to content

Commit 0b10add

Browse files
committed
add SummaryCard component
1 parent 775b26f commit 0b10add

File tree

8 files changed

+264
-1
lines changed

8 files changed

+264
-1
lines changed

site/lib/_sass/_site.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
@use 'components/sidebar';
3535
@use 'components/side-menu';
3636
@use 'components/site-switcher';
37+
@use 'components/summary-card';
3738
@use 'components/tabs';
3839
@use 'components/theming';
3940
@use 'components/toc';
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
.summary-card {
2+
background-color: var(--site-raised-bgColor-translucent);
3+
border-radius: var(--site-radius);
4+
border: 1px solid var(--site-inset-borderColor);
5+
6+
header {
7+
padding: 1rem 1.2rem 1.2rem 1.2rem;
8+
9+
h3 {
10+
margin: 0;
11+
font-size: 1.25rem;
12+
font-weight: 600;
13+
color: var(--site-base-fgColor);
14+
}
15+
16+
span {
17+
color: var(--site-base-fgColor-alt);
18+
}
19+
}
20+
21+
.summary-card-item {
22+
display: flex;
23+
align-items: center;
24+
gap: 1rem;
25+
26+
border-top: 1px solid var(--site-inset-borderColor);
27+
padding: .8rem 1.2rem;
28+
29+
>:first-child {
30+
font-size: .875rem;
31+
background-color: var(--site-primary-color-highlight);
32+
color: var(--site-primary-color);
33+
border-radius: 0.5rem;
34+
35+
padding: 0.5rem;
36+
}
37+
38+
.summary-card-item-title {
39+
flex: 1;
40+
41+
font-size: 1rem;
42+
font-weight: 500;
43+
}
44+
}
45+
46+
details {
47+
margin: 0;
48+
49+
summary {
50+
margin: 0;
51+
52+
>.material-symbols {
53+
transition: transform .25s ease-out;
54+
transform: rotate(180deg);
55+
transform-origin: center;
56+
}
57+
}
58+
59+
&[open] {
60+
summary>.material-symbols {
61+
transform: rotate(0);
62+
}
63+
}
64+
65+
.summary-card-item-details {
66+
margin: 0;
67+
border-top: 1px solid var(--site-inset-borderColor);
68+
padding: .8rem 1.2rem;
69+
color: var(--site-base-fgColor-alt);
70+
71+
>:last-child {
72+
margin-bottom: 0;
73+
}
74+
}
75+
}
76+
}

site/lib/main.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'src/components/pages/expansion_list.dart';
2121
import 'src/components/pages/learning_resource_index.dart';
2222
import 'src/components/tutorial/progress_ring.dart';
2323
import 'src/components/tutorial/quiz.dart';
24+
import 'src/components/tutorial/summary_card.dart';
2425
import 'src/extensions/registry.dart';
2526
import 'src/layouts/catalog_page_layout.dart';
2627
import 'src/layouts/doc_layout.dart';
@@ -100,6 +101,7 @@ List<CustomComponent> get _embeddableComponents => [
100101
const FileTree(),
101102
const Quiz(),
102103
const ProgressRing(),
104+
const SummaryCard(),
103105
CustomComponent(
104106
pattern: RegExp('OSSelector', caseSensitive: false),
105107
builder: (_, _, _) => const OsSelector(),
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2025 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:jaspr/jaspr.dart';
6+
import 'package:jaspr_content/jaspr_content.dart';
7+
import 'package:yaml/yaml.dart';
8+
9+
import '../../markdown/markdown_parser.dart';
10+
import '../../models/summary_card_model.dart';
11+
import '../common/material_icon.dart';
12+
13+
class SummaryCard extends CustomComponent {
14+
const SummaryCard() : super.base();
15+
16+
@override
17+
Component? create(Node node, NodesBuilder builder) {
18+
if (node is ElementNode && node.tag.toLowerCase() == 'summarycard') {
19+
if (node.children?.whereType<ElementNode>().isNotEmpty ?? false) {
20+
throw Exception(
21+
'Invalid SummaryCard content. Remove any leading empty lines to '
22+
'avoid parsing as markdown.',
23+
);
24+
}
25+
26+
final content = node.children?.map((n) => n.innerText).join('\n') ?? '';
27+
final data = loadYamlNode(content);
28+
assert(
29+
data is YamlMap,
30+
'Invalid SummaryCard content. Expected a YAML map.',
31+
);
32+
final model = SummaryCardModel.fromMap(data as YamlMap);
33+
assert(
34+
model.items.isNotEmpty,
35+
'SummaryCard must contain at least one item.',
36+
);
37+
return SummaryCardComponent(model: model);
38+
}
39+
return null;
40+
}
41+
}
42+
43+
class SummaryCardComponent extends StatelessComponent {
44+
const SummaryCardComponent({super.key, required this.model});
45+
46+
final SummaryCardModel model;
47+
48+
@override
49+
Component build(BuildContext context) {
50+
return div(classes: 'summary-card', [
51+
header([
52+
h3([text(model.title)]),
53+
if (model.subtitle case final subtitle?) span([text(subtitle)]),
54+
]),
55+
for (final item in model.items) buildSummaryItem(item),
56+
]);
57+
}
58+
59+
Component buildSummaryItem(SummaryCardItem item) {
60+
if (item.details case final d?) {
61+
return details([
62+
summary(classes: 'summary-card-item', [
63+
span([MaterialIcon(item.icon)]),
64+
span(classes: 'summary-card-item-title', [text(item.title)]),
65+
const MaterialIcon('keyboard_arrow_up'),
66+
]),
67+
div(classes: 'summary-card-item-details', [
68+
DashMarkdown(content: d),
69+
]),
70+
]);
71+
}
72+
return div(classes: 'summary-card-item', [
73+
span([MaterialIcon(item.icon)]),
74+
span(classes: 'summary-card-item-title', [text(item.title)]),
75+
]);
76+
}
77+
}

site/lib/src/models/quiz_model.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// Copyright 2025 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
15
import 'package:jaspr/jaspr.dart';
26

37
class Question {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2025 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:jaspr/jaspr.dart';
6+
7+
class SummaryCardModel {
8+
const SummaryCardModel({
9+
required this.title,
10+
this.subtitle,
11+
required this.items,
12+
});
13+
14+
final String title;
15+
final String? subtitle;
16+
final List<SummaryCardItem> items;
17+
18+
@decoder
19+
factory SummaryCardModel.fromMap(Map<Object?, Object?> json) {
20+
return SummaryCardModel(
21+
title: json['title'] as String,
22+
subtitle: json['subtitle'] as String?,
23+
items: (json['items'] as List<Object?>)
24+
.map((e) => SummaryCardItem.fromMap(e as Map<Object?, Object?>))
25+
.toList(),
26+
);
27+
}
28+
29+
@encoder
30+
Map<Object?, Object?> toJson() => {
31+
'title': title,
32+
'subtitle': subtitle,
33+
'items': items.map((e) => e.toJson()).toList(),
34+
};
35+
}
36+
37+
class SummaryCardItem {
38+
const SummaryCardItem({
39+
required this.title,
40+
required this.icon,
41+
this.details,
42+
});
43+
44+
final String title;
45+
final String icon;
46+
final String? details;
47+
48+
@decoder
49+
factory SummaryCardItem.fromMap(Map<Object?, Object?> json) {
50+
return SummaryCardItem(
51+
title: json['title'] as String,
52+
icon: json['icon'] as String,
53+
details: json['details'] as String?,
54+
);
55+
}
56+
57+
@encoder
58+
Map<Object?, Object?> toJson() => {
59+
'title': title,
60+
'icon': icon,
61+
'details': details,
62+
};
63+
}

site/lib/src/pages/custom_pages.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,5 +176,45 @@ class MyApp extends StatelessWidget {
176176
}
177177
```
178178
179+
## Summary Card
180+
181+
<SummaryCard>
182+
title: What you'll learn in this Flutter lesson
183+
items:
184+
- title: Introduction to Flutter and Dart programming
185+
icon: flutter
186+
- title: How to build beautiful UIs with widgets
187+
icon: mobile_layout
188+
- title: Adding navigation between different screens
189+
icon: conversion_path
190+
</SummaryCard>
191+
192+
---
193+
194+
<SummaryCard>
195+
title: What you accomplished
196+
subtitle: Here's a summary of what you accomplished in this lesson.
197+
items:
198+
- title: Reviewed the core concepts of Flutter
199+
icon: flutter
200+
details: >-
201+
Solidified understanding of Flutter's core concepts, including the widget
202+
tree, state management principles (Stateless vs. Stateful widgets), and
203+
the basic project structure. Reviewed the essentials of the Dart
204+
programming language.
205+
- title: Practiced building layouts with widgets
206+
icon: mobile_layout
207+
details: >-
208+
Built and experimented with common layout widgets (Row, Column,
209+
Stack, and Flex), learned how to use padding, alignment, and
210+
constraints to create responsive layouts across screen sizes.
211+
- title: Implemented screen navigation and routing
212+
icon: conversion_path
213+
details: >-
214+
Implemented navigation between screens using routes and
215+
Navigator patterns; learned how to pass arguments between routes
216+
and manage back navigation and nested navigation scenarios.
217+
</SummaryCard>
218+
179219
''',
180220
);

site/lib/src/style_hash.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
// dart format off
33

44
/// The generated hash of the `main.css` file.
5-
const generatedStylesHash = 'KqQfpDeGsOtA';
5+
const generatedStylesHash = 'kgeOsQJ5X5Io';

0 commit comments

Comments
 (0)