Skip to content
Open
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
24 changes: 24 additions & 0 deletions packages/site_shared/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# site_shared

This package is the core library containing shared logic, UI components, and the design system for Dart and Flutter documentation sites.

It provides a centralized location for APIs, user interface elements, and logic intended for use by both the `dart.dev` and `docs.flutter.dev` websites. Using a shared package ensures a consistent design language and feature set across Dart and Flutter web documentation platforms.

## What's included

The `site_shared` package provides several key features to build documentation websites using Jaspr:

- **UI Components** (`lib/components`): Reusable, modular components built for use across documentation pages.
- **Common components** (`lib/components/common`): Everyday UI elements such as breadcrumbs, buttons, cards, chips, tooltips, tabs, dropdowns, embedded YouTube videos, search bars, and wrapped code blocks.
- **Layout components** (`lib/components/layout`): Structural layout elements like theme switchers, site switchers, banners, and menu toggles.
- **Interactive components**: Integrations such as Dartpad (`lib/components/dartpad`), tutorials, and user client-side feedback tools.
- **Markdown Extensions & Processors** (`lib/extensions`): Custom processors that hook into the Dart markdown parser to extend its default syntax and behavior, such as `attribute_processor.dart`, `code_block_processor.dart`, `header_processor.dart`, and `table_processor.dart`.
- **Core Styles** (`lib/_sass`): The shared base styles and component-specific SCSS styling. These resources define the unified visual identity used by both websites.
- **Utilities and Builders** (`lib/src`): Reusable logic for code syntax highlighting (`lib/src/highlight`), analytics integrations (`lib/src/analytics`), builders (`lib/src/builders`), and various helper utilities.

## Goals

The primary aims of this shared package are to:
1. Streamline styling and standardize UI component implementation across our primary web documentation.
2. Prevent code duplication between the `dart.dev` and `docs.flutter.dev` repositories.
3. Establish a robust codebase that can be updated, maintained, and improved in a unified way.
4 changes: 4 additions & 0 deletions packages/site_shared/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include: package:analysis_defaults/analysis.yaml

formatter:
trailing_commas: preserve
14 changes: 14 additions & 0 deletions packages/site_shared/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
builders:
stylesHashBuilder:
import: "package:site_shared/src/builders/styles_hash_builder.dart"
builder_factories: ["stylesHashBuilder"]
build_extensions:
"web/assets/css/main.css":
- "lib/src/style_hash.dart"
auto_apply: dependents
build_to: source
required_inputs:
- ".css"
defaults:
dev_options:
fixed_hash: true
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ button {
cursor: pointer;

&.filled-button,
&.text-button, &.outlined-button {
&.text-button,
&.outlined-button {
display: flex;
align-items: center;
width: fit-content;
Expand Down Expand Up @@ -125,4 +126,4 @@ button {
border-bottom-left-radius: 0;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@
}
}

&.install-card {
&.install-card {
gap: 0.25rem;

.card-leading {
Expand Down Expand Up @@ -281,4 +281,4 @@ button.card {
width: 100%;
max-height: 100%;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,4 @@
display: block;
}
}
}
}
26 changes: 26 additions & 0 deletions packages/site_shared/lib/_sass/components/_menu-toggle.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Toggle between menu and close buttons if sidenav is open or not.
body:not(.sidenav-closed) #menu-toggle {
@media (min-width: 1024px) {
display: none;
}
}

#menu-toggle span.material-symbols {
&:first-child {
display: inline;
}

&:last-child {
display: none;
}
}

body.open_menu #menu-toggle span.material-symbols {
&:first-child {
display: none;
}

&:last-child {
display: inline;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@use '../base/mixins';

#site-switcher {
position: relative;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,4 @@ ul.nav-tabs {
}
}
}
}
}
21 changes: 21 additions & 0 deletions packages/site_shared/lib/analytics.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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:meta/meta.dart';

import 'src/analytics/analytics_server.dart'
if (dart.library.js_interop) 'src/analytics/analytics_web.dart';

/// Used to report analytic events.
final analytics = AnalyticsImplementation();

/// Contains methods for reporting analytics events.
abstract class Analytics {
@protected
void sendEvent(String eventName, Map<String, Object?> parameters);

void sendFeedback(bool helpful) {
sendEvent('feedback', {'feedback_type': helpful ? 'up' : 'down'});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ import 'material_icon.dart';
/// - https://schema.org/BreadcrumbList
/// - https://www.w3.org/TR/wai-aria-practices/examples/breadcrumb/index.html
class PageBreadcrumbs extends StatelessComponent {
const PageBreadcrumbs({super.key});
const PageBreadcrumbs({this.crumbs, super.key});

final List<BreadcrumbItem>? crumbs;

@override
Component build(BuildContext context) {
final crumbs = _breadcrumbsForPage(context.pages, context.page);
final crumbs =
this.crumbs ?? _breadcrumbsForPage(context.pages, context.page);
if (crumbs == null || crumbs.isEmpty) {
return const Component.empty();
}
Expand Down Expand Up @@ -54,7 +57,7 @@ class PageBreadcrumbs extends StatelessComponent {
///
/// Uses page metadata to generate breadcrumb titles with fallbacks:
/// `breadcrumb` > `shortTitle` > `title`.
List<_BreadcrumbItem>? _breadcrumbsForPage(List<Page> pages, Page page) {
List<BreadcrumbItem>? _breadcrumbsForPage(List<Page> pages, Page page) {
final pageUrl = page.url;

// Only show breadcrumbs if the URL isn't empty.
Expand All @@ -71,7 +74,7 @@ class PageBreadcrumbs extends StatelessComponent {
.toList(growable: false);
if (segments.isEmpty) return null;

final breadcrumbs = <_BreadcrumbItem>[];
final breadcrumbs = <BreadcrumbItem>[];
var currentPath = '';

// Build breadcrumbs for each segment except the current page.
Expand All @@ -88,7 +91,7 @@ class PageBreadcrumbs extends StatelessComponent {

if (indexPage.breadcrumb case final indexBreadcrumb?) {
breadcrumbs.add(
_BreadcrumbItem(
BreadcrumbItem(
title: indexBreadcrumb,
url: indexPage.url,
),
Expand All @@ -104,7 +107,7 @@ class PageBreadcrumbs extends StatelessComponent {

// Add the current page as the final breadcrumb.
breadcrumbs.add(
_BreadcrumbItem(
BreadcrumbItem(
title: pageBreadcrumb,
url: pageUrl,
),
Expand All @@ -127,8 +130,8 @@ extension on Page {
}
}

final class _BreadcrumbItem {
const _BreadcrumbItem({required this.title, required this.url});
final class BreadcrumbItem {
const BreadcrumbItem({required this.title, required this.url});

final String title;
final String url;
Expand All @@ -142,7 +145,7 @@ final class _BreadcrumbItemComponent extends StatelessComponent {
required this.isLast,
});

final _BreadcrumbItem crumb;
final BreadcrumbItem crumb;
final int index;
final bool isLast;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Button extends StatelessComponent {
const Button({
super.key,
this.icon,
this.trailingIcon,
this.href,
this.content,
this.style = ButtonStyle.text,
Expand All @@ -30,6 +31,7 @@ class Button extends StatelessComponent {
final String? title;
final ButtonStyle style;
final String? icon;
final String? trailingIcon;
final String? id;
final String? href;
final Map<String, String> attributes;
Expand All @@ -48,14 +50,16 @@ class Button extends StatelessComponent {

final mergedClasses = [
style.cssClass,
if (icon != null && content == null) 'icon-button',
if ((icon != null || trailingIcon != null) && content == null)
'icon-button',
...?classes,
].toClasses;

final children = <Component>[
if (icon case final iconId?) MaterialIcon(iconId),
if (content case final contentText?)
asRaw ? RawText(contentText) : .text(contentText),
if (trailingIcon case final iconId?) MaterialIcon(iconId),
];

if (href case final href?) {
Expand All @@ -82,25 +86,12 @@ class Button extends StatelessComponent {
enum ButtonStyle {
filled,
outlined,
text;
text
;

String get cssClass => switch (this) {
ButtonStyle.filled => 'filled-button',
ButtonStyle.outlined => 'outlined-button',
ButtonStyle.text => 'text-button',
};
}

class SegmentedButton extends StatelessComponent {
const SegmentedButton({
super.key,
required this.children,
});

final List<Component> children;

@override
Component build(BuildContext context) {
return span(classes: ['segmented-button'].toClasses, children);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:jaspr/jaspr.dart';
import 'package:universal_web/web.dart' as web;

import '../../util.dart';
import '../util/global_event_listener.dart';
import '../utils/global_event_listener.dart';
import 'material_icon.dart';

/// A set of Material Design-like chips for configuration.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ import '../button.dart';
/// The cookie banner to show on a user's first time visiting the site.
@client
final class CookieNotice extends StatefulComponent {
const CookieNotice({super.key});
const CookieNotice({
super.key,
required this.host,
this.alwaysDarkMode = false,
});

final String host;
final bool alwaysDarkMode;

@override
State<CookieNotice> createState() => _CookieNoticeState();
Expand Down Expand Up @@ -60,13 +67,16 @@ final class _CookieNoticeState extends State<CookieNotice> {
Component build(BuildContext context) {
return section(
id: 'cookie-notice',
classes: [if (showNotice) 'show'].toClasses,
classes: [
if (showNotice) 'show',
if (component.alwaysDarkMode) 'always-dark-mode',
].toClasses,
attributes: {'data-nosnippet': 'true'},
[
div(classes: 'container', [
const p([
p([
.text(
'docs.flutter.dev uses cookies from Google to deliver and '
'${component.host} uses cookies from Google to deliver and '
'enhance the quality of its services and to analyze traffic.',
),
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import '../button.dart';
class CopyButton extends StatefulComponent {
const CopyButton({
this.buttonText,
this.toCopy,
this.classes = const [],
this.title,
});

final String? title;
final String? toCopy;
final String? buttonText;
final List<String> classes;

Expand All @@ -32,9 +34,11 @@ class _CopyButtonState extends State<CopyButton> {
@override
void initState() {
if (kIsWeb) {
// Extract the code content and unhide the copy button on the client.
context.binding.addPostFrameCallback(() {
setState(() {
if (component.toCopy != null) {
content = component.toCopy;
} else {
// Extract the code content and unhide the copy button on the client.
context.binding.addPostFrameCallback(() {
final codeElement = buttonKey.currentNode
?.closest('.code-block-wrapper')
?.querySelector('pre code')
Expand All @@ -46,6 +50,7 @@ class _CopyButtonState extends State<CopyButton> {
codeElement,
/* NodeFilter.SHOW_ELEMENT */ 1,
);

web.Node? currentNode;
while ((currentNode = iterator.nextNode()) != null) {
final element = currentNode as web.Element;
Expand All @@ -55,15 +60,19 @@ class _CopyButtonState extends State<CopyButton> {
}

// Remove zero-width spaces
content = codeElement.textContent?.replaceAll('\u200B', '');
});
final extracted = codeElement.textContent?.replaceAll('\u200B', '');

assert(
extracted != null,
'CopyButton: Unable to find code content to copy. '
'Is the CopyButton inside a code block?',
);

assert(
content != null,
'CopyButton: Unable to find code content to copy. '
'Is the CopyButton inside a code block?',
);
});
setState(() {
content = extracted;
});
});
}
}

super.initState();
Expand Down
Loading
Loading