Skip to content

Commit c20d32c

Browse files
committed
Directly migrate widget catalog pages and speed up release note rendering
1 parent 8392adf commit c20d32c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+503
-201
lines changed

site/lib/_sass/components/_content.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
}
1616

1717
article {
18+
flex-grow: 1;
1819
min-width: 8rem;
1920
max-width: 960px;
2021
min-height: calc(100vh - var(--site-header-height) - var(--site-subheader-height));

site/lib/main.dart

Lines changed: 5 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import 'package:jaspr/server.dart';
66
import 'package:jaspr_content/jaspr_content.dart';
77
import 'package:jaspr_content/theme.dart';
8-
import 'package:liquify/liquify.dart' show FilterRegistry;
98
import 'package:path/path.dart' as path;
109

1110
import 'jaspr_options.dart'; // Generated. Do not remove or edit.
@@ -14,20 +13,20 @@ import 'src/components/pages/learning_resource_index.dart';
1413
import 'src/components/tabs.dart';
1514
import 'src/data/learning_resources.dart';
1615
import 'src/extensions/registry.dart';
16+
import 'src/layouts/catalog_page_layout.dart';
1717
import 'src/layouts/doc_layout.dart';
1818
import 'src/layouts/toc_layout.dart';
1919
import 'src/loaders/data_processor.dart';
2020
import 'src/markdown/markdown_parser.dart';
2121
import 'src/pages/custom_pages.dart';
2222
import 'src/pages/robots_txt.dart';
23+
import 'src/templating/dash_template_engine.dart';
2324
import 'src/util.dart';
2425

2526
void main() {
2627
// Initializes the server environment with the generated default options.
2728
Jaspr.initializeApp(options: defaultJasprOptions);
2829

29-
_setUpLiquid();
30-
3130
runApp(_docsFlutterDevSite);
3231
}
3332

@@ -42,8 +41,8 @@ Component get _docsFlutterDevSite => ContentApp.custom(
4241
FilesystemDataLoader(path.join(siteSrcDirectoryPath, 'data')),
4342
DataProcessor(),
4443
],
45-
templateEngine: LiquidTemplateEngine(
46-
includesPath: path.canonicalize(
44+
templateEngine: DashTemplateEngine(
45+
partialDirectoryPath: path.canonicalize(
4746
path.join(siteSrcDirectoryPath, '_includes'),
4847
),
4948
),
@@ -54,7 +53,7 @@ Component get _docsFlutterDevSite => ContentApp.custom(
5453
rawOutputPattern: RegExp(r'.*\.(txt|json|pdf)$'),
5554
extensions: allNodeProcessingExtensions,
5655
components: _embeddableComponents,
57-
layouts: const [DocLayout(), TocLayout()],
56+
layouts: const [DocLayout(), TocLayout(), CatalogPageLayout()],
5857
theme: const ContentTheme.none(),
5958
secondaryOutputs: [const RobotsTxtOutput(), MarkdownOutput()],
6059
),
@@ -116,40 +115,3 @@ List<CustomComponent> get _embeddableComponents => [
116115
builder: (_, _, _) => LearningResourceIndex(allLearningResources),
117116
),
118117
];
119-
120-
/// Set up the Liquid templating engine from `package:liquify`,
121-
/// adding filters, tags, and other functionality our content relies on.
122-
void _setUpLiquid() {
123-
// TODO(https://github.com/dart-lang/site-www/issues/6840):
124-
// Eventually migrate away from the remaining Liquid filter usages.
125-
FilterRegistry.register('slugify', (value, _, _) {
126-
if (value is! String) return value;
127-
128-
return slugify(value);
129-
});
130-
131-
FilterRegistry.register('arrayToSentenceString', (value, _, _) {
132-
if (value is! List) return value;
133-
134-
if (value.isEmpty) {
135-
return '';
136-
}
137-
138-
if (value.length == 1) {
139-
return value[0];
140-
}
141-
142-
final result = StringBuffer();
143-
144-
for (var i = 0; i < value.length; i++) {
145-
final item = value[i].toString();
146-
if (i == value.length - 1) {
147-
result.write('and $item');
148-
} else {
149-
result.write('$item, ');
150-
}
151-
}
152-
153-
return result.toString();
154-
});
155-
}
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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+
8+
import '../markdown/markdown_parser.dart';
9+
import '../util.dart';
10+
import 'doc_layout.dart';
11+
12+
/// Used as the layout for the widget catalog pages.
13+
// TODO: This is directly converted from the original Liquid logic.
14+
// We should either completely replace it with a new widget catalog
15+
// or clean it up.
16+
final class CatalogPageLayout extends DocLayout {
17+
static const String _placeholderImagePath =
18+
'/assets/images/docs/catalog-widget-placeholder.png';
19+
20+
const CatalogPageLayout();
21+
22+
@override
23+
String get name => 'widget-catalog-page';
24+
25+
@override
26+
Component buildBody(Page page, Component child) {
27+
final pageData = page.data.page;
28+
final widgetCategory = pageData['widgetCategory'] as String;
29+
final isMaterialCatalog = pageData['materialCatalog'] == true;
30+
31+
final catalogData = page.data['catalog'] as Map<String, Object?>;
32+
final catalogIndex = (catalogData['index'] as List<Object?>)
33+
.cast<Map<String, Object?>>();
34+
final category = _CategoryInfo(
35+
catalogIndex.firstWhere(
36+
(c) => c['name'] == widgetCategory,
37+
orElse: () => const <String, Object?>{},
38+
),
39+
);
40+
41+
final catalogWidgets = (catalogData['widgets'] as List<Object?>)
42+
.cast<Map<String, Object?>>();
43+
44+
final widgetsInCategory = catalogWidgets
45+
.map(_WidgetInfo.new)
46+
.where((w) => w.categories.contains(widgetCategory))
47+
.toList(growable: false);
48+
49+
final subcategories = category.subcategories;
50+
51+
return super.buildBody(
52+
page,
53+
Component.fragment([
54+
child,
55+
// Only show description for non-material catalogs.
56+
if (!isMaterialCatalog)
57+
if (category.description case final String description
58+
when description.isNotEmpty)
59+
DashMarkdown(content: description),
60+
61+
// Only show main category widgets for non-material catalogs.
62+
if (!isMaterialCatalog && widgetsInCategory.isNotEmpty)
63+
_buildCardGrid(
64+
widgetsInCategory,
65+
isMaterialCatalog: isMaterialCatalog,
66+
),
67+
68+
if (subcategories.isNotEmpty) ...[
69+
for (final sub in subcategories)
70+
..._buildSubcategorySection(
71+
sub,
72+
catalogWidgets,
73+
isMaterialCatalog: isMaterialCatalog,
74+
),
75+
],
76+
77+
if (isMaterialCatalog)
78+
p([
79+
text('Find more widgets in the '),
80+
a(href: '/ui/widgets/material2', [
81+
text('Material 2 widget catalog'),
82+
]),
83+
text(' and other categories of the '),
84+
a(href: '/ui/widgets', [text('widget catalog')]),
85+
text('.'),
86+
])
87+
else
88+
p([
89+
text('Find more widgets in the '),
90+
a(href: '/ui/widgets', [text('widget catalog')]),
91+
text('.'),
92+
]),
93+
]),
94+
);
95+
}
96+
97+
List<Component> _buildSubcategorySection(
98+
_SubcategoryInfo subcategory,
99+
List<Map<String, Object?>> allWidgets, {
100+
required bool isMaterialCatalog,
101+
}) {
102+
final subName = subcategory.name;
103+
if (subName.isEmpty) return const [];
104+
105+
final widgets = allWidgets
106+
.map(_WidgetInfo.new)
107+
.where((w) => w.subcategories.contains(subName))
108+
.toList(growable: false);
109+
110+
if (widgets.isEmpty) return const [];
111+
112+
return [
113+
h2([text(subName)]),
114+
_buildCardGrid(
115+
widgets,
116+
isMaterialCatalog: isMaterialCatalog,
117+
subcategory: subcategory,
118+
),
119+
];
120+
}
121+
122+
Component _buildCardGrid(
123+
List<_WidgetInfo> widgets, {
124+
required bool isMaterialCatalog,
125+
_SubcategoryInfo? subcategory,
126+
}) {
127+
final gridClasses = isMaterialCatalog
128+
? 'card-grid material-cards'
129+
: 'card-grid';
130+
131+
return div(
132+
classes: gridClasses,
133+
[
134+
for (final widget in widgets)
135+
_buildWidgetCard(
136+
widget,
137+
isMaterialCatalog: isMaterialCatalog,
138+
subcategory: subcategory,
139+
),
140+
],
141+
);
142+
}
143+
144+
Component _buildWidgetCard(
145+
_WidgetInfo widget, {
146+
required bool isMaterialCatalog,
147+
_SubcategoryInfo? subcategory,
148+
}) {
149+
return a(
150+
classes: 'card outlined-card',
151+
href: widget.link,
152+
[
153+
_buildCardImageHolder(
154+
name: widget.name,
155+
vector: widget.vector,
156+
imageSrc: widget.imageSrc,
157+
hoverBackgroundSrc: widget.hoverBackgroundSrc,
158+
isMaterialCatalog: isMaterialCatalog,
159+
subcategoryColor: subcategory?.color,
160+
),
161+
div(
162+
classes: 'card-header',
163+
[
164+
header(
165+
classes: 'card-title',
166+
[text(widget.name)],
167+
),
168+
],
169+
),
170+
div(
171+
classes: 'card-content',
172+
[
173+
DashMarkdown(
174+
content: truncateWords(widget.description, 25),
175+
),
176+
],
177+
),
178+
],
179+
);
180+
}
181+
182+
Component _buildCardImageHolder({
183+
required String name,
184+
required String? vector,
185+
required String? imageSrc,
186+
required String? hoverBackgroundSrc,
187+
required bool isMaterialCatalog,
188+
required String? subcategoryColor,
189+
}) {
190+
final holderClass = isMaterialCatalog
191+
? 'card-image-holder-material-3'
192+
: 'card-image-holder';
193+
194+
final imageAlt = isMaterialCatalog
195+
? 'Rendered example of the $name Material widget.'
196+
: 'Rendered image or visualization of the $name widget.';
197+
198+
final placeholderAlt =
199+
'Placeholder Flutter logo in place of '
200+
'missing widget image or visualization.';
201+
202+
final styleAttributes = isMaterialCatalog && subcategoryColor != null
203+
? {'style': '--bg-color: $subcategoryColor'}
204+
: <String, String>{};
205+
206+
return div(
207+
classes: holderClass,
208+
attributes: styleAttributes,
209+
[
210+
if (isMaterialCatalog) ...[
211+
// Material catalog always expects an image.
212+
if (imageSrc != null && imageSrc.isNotEmpty)
213+
img(alt: imageAlt, src: imageSrc)
214+
else
215+
img(
216+
alt: placeholderAlt,
217+
src: _placeholderImagePath,
218+
attributes: {'aria-hidden': 'true'},
219+
),
220+
if (hoverBackgroundSrc != null && hoverBackgroundSrc.isNotEmpty)
221+
div(
222+
classes: 'card-image-material-3-hover',
223+
[
224+
img(
225+
alt:
226+
'Decorated background for '
227+
'Material widget visualizations.',
228+
src: hoverBackgroundSrc,
229+
attributes: {'aria-hidden': 'true'},
230+
),
231+
],
232+
),
233+
] else ...[
234+
// Standard catalog prefers vector, then image, then placeholder.
235+
if (vector != null && vector.isNotEmpty)
236+
raw(vector)
237+
else if (imageSrc != null && imageSrc.isNotEmpty)
238+
img(alt: imageAlt, src: imageSrc)
239+
else
240+
img(
241+
alt: placeholderAlt,
242+
src: _placeholderImagePath,
243+
attributes: {'aria-hidden': 'true'},
244+
),
245+
],
246+
],
247+
);
248+
}
249+
}
250+
251+
extension type _WidgetInfo(Map<String, Object?> _data) {
252+
String get name => _data['name'] as String;
253+
String get link => _data['link'] as String;
254+
String get description => _data['description'] as String? ?? '';
255+
String? get vector => _data['vector'] as String?;
256+
Map<String, Object?>? get image => _data['image'] as Map<String, Object?>?;
257+
String? get imageSrc => image?['src'] as String?;
258+
Map<String, Object?>? get hoverBackground =>
259+
_data['hoverBackground'] as Map<String, Object?>?;
260+
String? get hoverBackgroundSrc => hoverBackground?['src'] as String?;
261+
262+
List<String> get categories {
263+
final value = _data['categories'];
264+
if (value is List<Object?>) {
265+
return value.cast<String>();
266+
}
267+
return const [];
268+
}
269+
270+
List<String> get subcategories {
271+
final value = _data['subcategories'];
272+
if (value is List<Object?>) {
273+
return value.cast<String>();
274+
}
275+
return const [];
276+
}
277+
}
278+
279+
extension type _CategoryInfo(Map<String, Object?> _data) {
280+
String get name => _data['name'] as String? ?? '';
281+
String get description => _data['description'] as String? ?? '';
282+
List<_SubcategoryInfo> get subcategories {
283+
final value = _data['subcategories'] as List<Object?>?;
284+
if (value == null) return const [];
285+
return value
286+
.cast<Map<String, Object?>>()
287+
.map(_SubcategoryInfo.new)
288+
.toList(growable: false);
289+
}
290+
}
291+
292+
extension type _SubcategoryInfo(Map<String, Object?> _data) {
293+
String get name => _data['name'] as String? ?? '';
294+
String? get color => _data['color'] as String?;
295+
}

0 commit comments

Comments
 (0)