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
115 changes: 107 additions & 8 deletions sites/docs/lib/src/components/layout/banner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,122 @@
import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';

/// The site-wide banner.
class DashBanner extends StatelessComponent {
const DashBanner(this.inlineHtmlContent, {super.key});
/// The information to display in the site banner,
/// as configured in `src/data/banner.yml`.
@immutable
final class BannerContent {
/// The ordered content parts to render in the banner.
final List<BannerPart> parts;

/// Creates banner content from the specified [parts].
const BannerContent({required this.parts});

/// Creates banner content from the parsed [bannerData].
///
/// The [bannerData] list is expected to contain
/// `text`, `link`, or `newLine` entries from `src/data/banner.yml`.
///
/// Throws if any entry has an unsupported structure.
factory BannerContent.fromList(List<Object?> bannerData) => BannerContent(
parts: [
for (final item in bannerData)
switch (item) {
{'text': final String text} => .text(text),
{'link': final Map<Object?, Object?> link} => .link(
Comment thread
parlough marked this conversation as resolved.
text: link['text'] as String,
url: link['url'] as String,
newTab: link['newTab'] as bool? ?? false,
),
{'newLine': _} => const .newLine(),
_ => throw FormatException('Invalid banner item: $item'),
},
],
);
}

/// A single renderable piece of banner content.
@immutable
sealed class BannerPart {
/// Creates a banner content part.
const BannerPart();

/// Creates a text part with the specified [text].
const factory BannerPart.text(String text) = _BannerText;

/// Creates a link part with the specified [text] and [url].
///
/// Unless [newTab] is `true`, the link opens in the same tab.
const factory BannerPart.link({
required String text,
required String url,
bool newTab,
}) = _BannerLink;

/// Creates a new line part that renders a line break.
const factory BannerPart.newLine() = _BannerNewLine;
}

/// Plain text within a site banner.
final class _BannerText extends BannerPart {
/// Creates a text banner part with the specified [text].
const _BannerText(this.text);

/// The text to render in the banner.
final String text;
}

/// The raw, inline HTML content to render in the banner.
/// A link within a site banner.
final class _BannerLink extends BannerPart {
/// Creates a link banner part with the specified [text] and [url].
///
/// This should only be sourced from managed content,
/// such as our checked-in data files.
final String inlineHtmlContent;
/// Unless [newTab] is `true`, the link opens in the same tab.
const _BannerLink({
required this.text,
required this.url,
this.newTab = false,
});

/// The link label to render in the banner.
final String text;

/// The destination URL for this link.
final String url;

/// Whether this link opens in a new browser tab.
final bool newTab;
}

/// A line break within a site banner.
final class _BannerNewLine extends BannerPart {
/// Creates a line break banner part.
const _BannerNewLine();
}

/// A site-wide banner rendered from structured content.
class DashBanner extends StatelessComponent {
/// Creates a site banner that displays the specified [content].
const DashBanner(this.content, {super.key});

/// The structured content to render in this banner.
final BannerContent content;

@override
Component build(BuildContext context) => div(
id: 'site-banner',
attributes: {'role': 'alert'},
[
p([
RawText(inlineHtmlContent),
for (final part in content.parts)
switch (part) {
_BannerText(:final text) => .text(text),
_BannerLink(:final text, :final url, :final newTab) => a(
href: url,
target: newTab ? Target.blank : null,
attributes: newTab ? const {'rel': 'noopener'} : null,
[.text(text)],
),
Comment thread
parlough marked this conversation as resolved.
_BannerNewLine() => const br(),
},
]),
],
);
Expand Down
16 changes: 16 additions & 0 deletions sites/docs/lib/src/layouts/dash_layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:jaspr/jaspr.dart';
import 'package:jaspr_content/jaspr_content.dart';

import '../components/common/client/cookie_notice.dart';
import '../components/layout/banner.dart';
import '../components/layout/footer.dart';
import '../components/layout/header.dart';
import '../components/layout/sidenav.dart';
Expand Down Expand Up @@ -270,6 +271,21 @@ if (sidenav) {
);
}

/// Builds the banner component for the given [page].
Component? buildBanner(Page page) {
final showBanner =
(page.data.page['showBanner'] as bool?) ??
(page.data.site['showBanner'] as bool?) ??
false;
if (showBanner) {
if (page.data['banner'] case final List<Object?> bannerData) {
return DashBanner(BannerContent.fromList(bannerData));
}
}

return null;
}

/// Builds the speculation rules `<script>` and `<link rel="prefetch">`
/// fallback tags for the given [page].
///
Expand Down
11 changes: 1 addition & 10 deletions sites/docs/lib/src/layouts/doc_layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import 'package:jaspr_content/jaspr_content.dart';

import '../components/common/page_header.dart';
import '../components/common/prev_next.dart';
import '../components/layout/banner.dart';
import '../components/layout/toc.dart';
import '../components/layout/trailing_content.dart';
import '../models/page_navigation_model.dart';
Expand Down Expand Up @@ -48,14 +47,9 @@ class DocLayout extends FlutterDocsLayout {
@override
Component buildBody(Page page, Component child) {
final pageData = page.data.page;
final siteData = page.data.site;

final pageTitle = pageData['title'] as String;
final pageDescription = (pageData['description'] as String?)?.trim();
final showBanner =
(pageData['showBanner'] as bool?) ??
(siteData['showBanner'] as bool?) ??
false;
final navigationData = page.navigationData;

return super.buildBody(
Expand All @@ -75,10 +69,7 @@ class DocLayout extends FlutterDocsLayout {
PageNavBar(navigationData),
],
),
if (showBanner)
if (siteData['bannerHtml'] case final String bannerHtml
when bannerHtml.trim().isNotEmpty)
DashBanner(bannerHtml),
?buildBanner(page),
div(classes: 'after-leading-content', [
if (navigationData case PageNavigationData(
toc: final toc?,
Expand Down
11 changes: 11 additions & 0 deletions sites/docs/src/data/banner.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# The following entries are rendered in order as part of the site banner.
# Three types of entries are supported:
# - `text` for plain text.
# - `link` with `text`, `url`, and optional `newTab: true`.
# - `newLine: true` for inserting a line break.

- text: "Help improve Flutter! "
- link:
text: "Take our Q2 survey"
url: https://google.qualtrics.com/jfe/form/SV_3drKjSfjNeLZfq6?Source=Website
newTab: true
6 changes: 0 additions & 6 deletions sites/docs/src/data/site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ email: flutter-dev@googlegroups.com

showBanner: true

# The raw, inline HTML to display in the banner.
# Is automatically wrapped in a paragraph tag.
bannerHtml: >-
Help improve Flutter!
<a href="https://google.qualtrics.com/jfe/form/SV_3drKjSfjNeLZfq6?Source=Website" target="_blank" rel="noopener">Take our Q2 survey</a>

branch: main
repo:
organization: https://github.com/flutter
Expand Down
Loading