diff --git a/sites/docs/lib/_sass/_site.scss b/sites/docs/lib/_sass/_site.scss index df8a1d9de6..95728a012d 100644 --- a/sites/docs/lib/_sass/_site.scss +++ b/sites/docs/lib/_sass/_site.scss @@ -53,10 +53,12 @@ // Styles for specific pages, alphabetically ordered. @use 'pages/cuj-index'; +@use 'pages/flutter-bench'; @use 'pages/glossary'; @use 'pages/learning-resources-index'; @use 'pages/not-found'; @use 'pages/search'; +@use 'pages/story-layout'; // Must be imported last to ensure that // the print overrides take priority over earlier defined styles. diff --git a/sites/docs/lib/_sass/pages/_flutter-bench.scss b/sites/docs/lib/_sass/pages/_flutter-bench.scss new file mode 100644 index 0000000000..d5a949be86 --- /dev/null +++ b/sites/docs/lib/_sass/pages/_flutter-bench.scss @@ -0,0 +1,1388 @@ +@use 'sass:color'; +@use 'package:site_shared/_sass/base/breakpoints'; + +// Styles for FlutterBench and Methodology pages + +// ----------------------------------------------------------------------------- +// Variables & Theme +// ----------------------------------------------------------------------------- + +// Core Category Colors +// NOTE: These colors are intentionally coordinated with _story-layout.scss +// chapter accent colors to maintain visual consistency across FlutterBench pages +$color-outcome: #059669; // Green - successful outcomes +$color-quality: #6366f1; // Indigo - code quality metrics +$color-dx: #d97706; // Amber - developer experience +$color-functional: #0284c7; // Sky blue - functional correctness +$color-llm: #7c3aed; // Violet - LLM grading +$color-heuristic: #2563eb; // Blue - heuristic grading +$color-poor: #ea580c; // Orange - poor performance +$color-failure: #e11d48; // Rose - failures +$color-grey: #64748b; // Slate - neutral/baseline + +// Hero & Special Colors +$color-white: #ffffff; +$color-gold: #fbbf24; +$hero-bg-light: linear-gradient(135deg, #0d2040 0%, #1e1b4b 50%, #0f172a 100%); +$hero-bg-dark: linear-gradient(135deg, #091728 0%, #131230 50%, #090e17 100%); +$north-star-bg-light: linear-gradient(135deg, #102a4e 0%, #1e1b4b 100%); +$north-star-bg-dark: linear-gradient(135deg, #0d1e38 0%, #151433 100%); + +// Typography Scale +$font-size-xs: 0.75rem; +$font-size-sm: 0.875rem; +$font-size-md: 1rem; +$font-size-lg: 1.25rem; +$font-size-xl: 1.5rem; +$font-size-xxl: 2.5rem; + +// Spacing Scale +$spacing-xs: 0.25rem; +$spacing-sm: 0.5rem; +$spacing-md: 1rem; +$spacing-lg: 1.5rem; +$spacing-xl: 2rem; + +// Dimensions & Layout +$border-width: 2px; +$icon-size: 2.75rem; +$card-width: 280px; +$bench-content-max-width: 1000px; + +// Shadows & Transitions +$shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); +$shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); +$shadow-hero: 0 4px 12px rgba(0, 0, 0, 0.2); +$shadow-hero-dark: 0 4px 12px rgba(0, 0, 0, 0.4); +$transition-normal: 0.2s ease; + +// ----------------------------------------------------------------------------- +// Mixins +// ----------------------------------------------------------------------------- + +// Centers content with consistent max-width +@mixin bench-element-width($max-width: $bench-content-max-width) { + width: 100%; + max-width: $max-width; + margin-left: $spacing-sm; + margin-right: $spacing-sm; +} + +// Consistent scrollbar styling across components +@mixin custom-scrollbar($orientation: 'vertical') { + scrollbar-width: thin; + scrollbar-color: var(--site-inset-borderColor) transparent; + -webkit-overflow-scrolling: touch; + + &::-webkit-scrollbar { + @if $orientation == 'horizontal' { + height: 6px; + } @else { + width: 6px; + } + } + + &::-webkit-scrollbar-thumb { + background-color: var(--site-inset-borderColor); + border-radius: 3px; + } +} + +// Category badge styling with optional border +@mixin category-badge($with-border: false) { + font-size: $font-size-xs; + font-weight: 600; + padding: $spacing-xs $spacing-sm; + border-radius: var(--site-radius, $spacing-xs); + + &.badge-outcome, + &.outcome { + background: rgba($color-outcome, 0.12); + color: $color-outcome; + @if $with-border { + border: 1px solid rgba($color-outcome, 0.25); + } + } + + &.badge-quality, + &.quality { + background: rgba($color-quality, 0.12); + color: $color-quality; + @if $with-border { + border: 1px solid rgba($color-quality, 0.25); + } + } + + &.badge-dx, + &.dx { + background: rgba($color-dx, 0.12); + color: $color-dx; + @if $with-border { + border: 1px solid rgba($color-dx, 0.25); + } + } + + &.badge-functional, + &.badge-blue { + background: rgba($color-functional, 0.12); + color: $color-functional; + @if $with-border { + border: 1px solid rgba($color-functional, 0.25); + } + } + + &.badge-llm { + background: rgba($color-llm, 0.12); + color: $color-llm; + @if $with-border { + border: 1px solid rgba($color-llm, 0.25); + } + } + + &.badge-heuristic { + background: rgba($color-heuristic, 0.12); + color: $color-heuristic; + @if $with-border { + border: 1px solid rgba($color-heuristic, 0.25); + } + } + + &.badge-poor { + background: rgba($color-poor, 0.12); + color: $color-poor; + @if $with-border { + border: 1px solid rgba($color-poor, 0.25); + } + } + + &.badge-failure { + background: rgba($color-failure, 0.12); + color: $color-failure; + @if $with-border { + border: 1px solid rgba($color-failure, 0.25); + } + } + + &.badge-grey { + background: rgba($color-grey, 0.12); + color: $color-grey; + @if $with-border { + border: 1px solid rgba($color-grey, 0.25); + } + } + + &.badge-deterministic { + background: rgba($color-outcome, 0.12); + color: $color-outcome; + @if $with-border { + border: 1px solid rgba($color-outcome, 0.25); + } + } + + &.badge-perfect, + &.badge-green { + background: rgba($color-outcome, 0.12); + color: $color-outcome; + @if $with-border { + border: 1px solid rgba($color-outcome, 0.25); + } + } + + &.badge-purple { + background: rgba($color-quality, 0.12); + color: $color-quality; + @if $with-border { + border: 1px solid rgba($color-quality, 0.25); + } + } + + &.badge-amber { + background: rgba($color-dx, 0.12); + color: $color-dx; + @if $with-border { + border: 1px solid rgba($color-dx, 0.25); + } + } +} + +// Icon background with category colors +@mixin category-icon-background { + width: $icon-size; + height: $icon-size; + border-radius: $spacing-sm; + display: flex; + align-items: center; + justify-content: center; + + &.outcome, + &.variant-outcome { + background-color: rgba($color-outcome, 0.15); + color: $color-outcome; + } + + &.quality, + &.variant-quality { + background-color: rgba($color-quality, 0.15); + color: $color-quality; + } + + &.dx, + &.variant-dx { + background-color: rgba($color-dx, 0.15); + color: $color-dx; + } + + &.variant-blue { + background-color: rgba($color-functional, 0.12); + color: $color-functional; + } + + &.variant-grey { + background-color: rgba($color-grey, 0.15); + color: $color-grey; + } + + .material-symbols { + font-size: $font-size-xl; + } +} + +// ----------------------------------------------------------------------------- +// Component Styles +// ----------------------------------------------------------------------------- + +.methodology-hero { + background: $hero-bg-light; + color: $color-white; + border-radius: var(--site-radius); + padding: $spacing-xl; + box-shadow: $shadow-hero; + + h1, + .hero-title { + color: $color-white; + font-weight: 700; + margin: 0 0 $spacing-sm 0; + } + + p { + color: #e2e8f0; + font-size: $font-size-sm; + margin: 0; + max-width: 48rem; + + code { + background: rgba(15, 23, 42, 0.8); + color: #93c5fd; + border: 1px solid rgba(59, 130, 246, 0.3); + padding: $spacing-xs $spacing-sm; + border-radius: var(--site-radius); + } + + strong { + color: $color-white; + } + } +} + +// Three Core Dimensions Grid +.dimension-cards-grid { + @include bench-element-width; + max-width: 700px; + margin-block: $spacing-lg $spacing-xl; + + .dimension-card { + background-color: var(--site-inset-bgColor); + border: 1px solid var(--site-inset-borderColor); + border-radius: var(--site-radius); + padding: $spacing-md; + margin-bottom: $spacing-md; + display: flex; + flex-direction: column; + + .card-header-row { + display: flex; + align-items: center; + + .card-icon-wrap { + @include category-icon-background; + margin: 0 $spacing-md $spacing-md 0; + } + + h4 { + flex: 2; + font-size: $font-size-md; + font-weight: 600; + margin: 0 0 $spacing-sm 0; + color: var(--site-base-fgColor); + } + } + + p { + font-size: $font-size-sm; + color: var(--site-base-fgColor-alt); + margin: 0; + flex-grow: 1; + } + + .card-footer-info { + margin-top: $spacing-md; + padding-top: $spacing-sm; + border-top: 1px solid var(--site-inset-borderColor); + display: flex; + align-items: center; + justify-content: space-between; + font-size: $font-size-xs; + color: var(--site-base-fgColor-lighter); + + .badge { + @include category-badge; + } + } + } +} + +// Grader Matrix Carousel +.grader-matrix { + @include bench-element-width; + background-color: var(--site-inset-bgColor); + border: 1px solid var(--site-inset-borderColor); + border-radius: var(--site-radius); + padding: $spacing-md; + margin-block: $spacing-md; + position: relative; + + .matrix-header { + display: flex; + flex-direction: column; + gap: $spacing-md; + margin-bottom: $spacing-md; + + @include breakpoints.screen('md') { + flex-direction: row; + align-items: center; + justify-content: space-between; + } + + .matrix-title-area { + h3 { + margin: 0 0 $spacing-xs 0; + font-size: $font-size-lg; + font-weight: 600; + color: var(--site-base-fgColor); + } + + p { + margin: 0; + font-size: $font-size-sm; + color: var(--site-base-fgColor-alt); + } + } + + .matrix-filters { + display: flex; + flex-wrap: wrap; + gap: $spacing-xs; + background-color: var(--site-raised-bgColor); + padding: $spacing-xs; + border-radius: $spacing-sm; + + .filter-btn { + background: transparent; + border: none; + border-radius: var(--site-radius); + padding: $spacing-xs $spacing-sm; + font-size: $font-size-xs; + font-weight: 500; + color: var(--site-base-fgColor-alt); + cursor: pointer; + transition: $transition-normal; + + &:hover { + color: var(--site-base-fgColor); + } + + &.active { + background-color: var(--site-base-bgColor); + color: var(--site-primary-color); + font-weight: 600; + box-shadow: $shadow-sm; + } + } + } + } + + .grader-carousel-wrapper { + position: relative; + width: 100%; + + .carousel-nav-btn { + position: absolute; + top: 50%; + transform: translateY(-50%); + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 50%; + border: 1px solid var(--site-inset-borderColor); + background-color: var(--site-base-bgColor); + color: var(--site-base-fgColor); + cursor: pointer; + transition: $transition-normal; + box-shadow: $shadow-sm; + z-index: var(--site-z-floating, 10); + + &.prev { + left: -$spacing-md; + } + + &.next { + right: -$spacing-md; + } + + &:hover:not(:disabled) { + background-color: var(--site-raised-bgColor); + border-color: var(--site-primary-color); + color: var(--site-primary-color); + transform: translateY(-50%) scale(1.05); + } + + &:active:not(:disabled) { + transform: translateY(-50%) scale(0.95); + } + + &:disabled { + opacity: 0.35; + cursor: not-allowed; + } + + .material-symbols { + font-size: $font-size-xl; + } + } + + .grader-cards-track { + @include custom-scrollbar('horizontal'); + width: 100%; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + gap: $spacing-md; + overflow-x: auto; + scroll-behavior: smooth; + scroll-snap-type: x mandatory; + padding: $spacing-sm; + background-color: var(--site-base-bgColor); + border-radius: var(--site-radius); + // Preserves padding when overflow-x clips it + border-left: $spacing-sm solid var(--site-base-bgColor); + border-right: $spacing-sm solid var(--site-base-bgColor); + + .grader-card { + flex: 0 0 $card-width; + max-width: $card-width; + min-width: 260px; + scroll-snap-align: start; + background-color: var(--site-base-bgColor); + border: 1px solid var(--site-inset-borderColor); + border-radius: var(--site-radius); + padding: $spacing-md; + display: flex; + flex-direction: column; + gap: $spacing-sm; + transition: border-color $transition-normal; + + &:hover { + border-color: var(--site-primary-color); + } + + &.hidden { + display: none !important; + } + + .grader-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: $spacing-sm; + + .grader-cat { + font-size: $font-size-xs; + font-weight: 700; + text-transform: uppercase; + + &.outcome { + color: $color-outcome; + } + + &.quality { + color: $color-quality; + } + + &.dx { + color: $color-dx; + } + } + + .grader-badge { + @include category-badge(true); + border-radius: 9999px; + } + } + + h4 { + font-size: $font-size-md; + font-weight: 600; + margin: 0; + color: var(--site-base-fgColor); + } + + p { + font-size: $font-size-sm; + color: var(--site-base-fgColor-alt); + margin: 0; + flex-grow: 1; + + code { + font-size: 0.9em; + background-color: rgba(0, 0, 0, 0.05); + padding: $spacing-xs; + border-radius: var(--site-radius); + } + } + } + } + } +} + +// Multi-Run Reliability +.reliability-comparison-grid { + @include bench-element-width; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); + gap: $spacing-md; + margin-block: $spacing-lg $spacing-xl; + + .reliability-card { + background-color: var(--site-inset-bgColor); + border: 1px solid var(--site-inset-borderColor); + border-radius: var(--site-radius); + padding: $spacing-lg; + display: flex; + flex-direction: column; + gap: $spacing-sm; + + .reliability-header { + display: flex; + align-items: center; + justify-content: space-between; + + .tag { + font-size: $font-size-xs; + font-weight: 700; + text-transform: uppercase; + color: var(--site-primary-color); + } + + .math-pill { + font-family: var(--site-code-fontFamily, monospace); + font-size: $font-size-sm; + font-weight: 700; + background-color: rgba(4, 104, 215, 0.1); + color: var(--site-primary-color); + padding: $spacing-xs $spacing-sm; + border-radius: var(--site-radius); + } + } + + h4 { + font-size: $font-size-lg; + font-weight: 600; + margin: 0; + color: var(--site-base-fgColor); + } + + p { + font-size: $font-size-sm; + color: var(--site-base-fgColor-alt); + margin: 0; + + strong, + em { + color: var(--site-base-fgColor); + } + } + + &.north-star { + background: $north-star-bg-light; + border: 1px solid rgba(59, 130, 246, 0.4); + color: $color-white; + box-shadow: $shadow-md; + + .reliability-header { + .tag { + color: $color-gold; + display: flex; + align-items: center; + gap: $spacing-xs; + + .material-symbols { + font-size: $font-size-md; + } + } + + .math-pill { + background-color: rgba(59, 130, 246, 0.25); + border: 1px solid rgba(147, 197, 253, 0.4); + color: #bfdbfe; + } + } + + h4 { + color: $color-white; + } + + p { + color: #e2e8f0; + + strong, + em { + color: $color-white; + } + } + } + } +} + +// Interactive Detail Card & Score Triage +.interactive-detail-card, +.score-triage { + @include bench-element-width; + background-color: var(--site-inset-bgColor); + border: 1px solid var(--site-inset-borderColor); + border-radius: var(--site-radius); + padding: $spacing-lg; + margin-block: $spacing-lg $spacing-xl; + + .card-header-area, + .triage-header { + margin-bottom: $spacing-md; + + h3 { + font-size: $font-size-lg; + font-weight: 600; + margin: 0 0 $spacing-xs 0; + color: var(--site-base-fgColor); + } + + p { + font-size: $font-size-sm; + color: var(--site-base-fgColor-alt); + margin: 0; + } + } + + .card-tabs-grid, + .triage-tiers-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr)); + gap: $spacing-sm; + margin-bottom: $spacing-md; + + .card-tab-btn, + .triage-tier-btn { + background-color: var(--site-base-bgColor); + border: 1px solid var(--site-inset-borderColor); + border-radius: var(--site-radius); + padding: $spacing-sm $spacing-md; + text-align: left; + cursor: pointer; + transition: $transition-normal; + opacity: 0.75; + + &:hover { + opacity: 1; + } + + .tab-primary-label, + .tier-score { + font-size: $font-size-sm; + font-weight: 800; + } + + .tab-secondary-label, + .tier-name { + font-size: $font-size-xs; + font-weight: 600; + margin-top: $spacing-xs; + color: var(--site-base-fgColor); + } + + // Color variants for tabs + &.variant-perfect, + &.tier-perfect { + .tab-primary-label, + .tier-score { + color: $color-outcome; + } + + &.active { + border: $border-width solid $color-outcome; + background-color: rgba($color-outcome, 0.08); + opacity: 1; + } + } + + &.variant-functional, + &.variant-blue, + &.tier-functional { + .tab-primary-label, + .tier-score { + color: $color-functional; + } + + &.active { + border: $border-width solid $color-functional; + background-color: rgba($color-functional, 0.08); + opacity: 1; + } + } + + &.variant-quality, + &.variant-purple { + .tab-primary-label, + .tier-score { + color: $color-quality; + } + + &.active { + border: $border-width solid $color-quality; + background-color: rgba($color-quality, 0.08); + opacity: 1; + } + } + + &.variant-partial, + &.variant-dx, + &.variant-amber, + &.tier-partial { + .tab-primary-label, + .tier-score { + color: $color-dx; + } + + &.active { + border: $border-width solid $color-dx; + background-color: rgba($color-dx, 0.08); + opacity: 1; + } + } + + &.variant-poor, + &.tier-poor { + .tab-primary-label, + .tier-score { + color: $color-poor; + } + + &.active { + border: $border-width solid $color-poor; + background-color: rgba($color-poor, 0.08); + opacity: 1; + } + } + + &.variant-failure, + &.tier-failure { + .tab-primary-label, + .tier-score { + color: $color-failure; + } + + &.active { + border: $border-width solid $color-failure; + background-color: rgba($color-failure, 0.08); + opacity: 1; + } + } + } + } + + .card-panels-container, + .triage-detail-card { + @include custom-scrollbar; + background-color: var(--site-base-bgColor); + border: 1px solid var(--site-inset-borderColor); + border-radius: var(--site-radius); + padding: $spacing-md; + position: relative; + min-height: 220px; + max-height: 380px; + overflow-y: auto; + + .card-panel, + .triage-panel { + display: none; + flex-direction: column; + gap: $spacing-sm; + + &.active { + display: flex; + } + + .panel-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: $spacing-sm; + + h4 { + font-size: $font-size-md; + font-weight: 700; + margin: 0; + color: var(--site-base-fgColor); + } + + .panel-badge { + @include category-badge; + } + } + + .panel-overview, + .criteria-text { + font-size: $font-size-sm; + color: var(--site-base-fgColor-alt); + margin: 0; + } + + .panel-items-section, + .actions-section { + border-top: 1px solid var(--site-inset-borderColor); + padding-top: $spacing-sm; + + .items-label, + .actions-label { + font-size: $font-size-xs; + font-weight: 700; + text-transform: uppercase; + color: var(--site-base-fgColor); + margin-bottom: $spacing-xs; + } + + ul { + margin: 0; + padding-left: $spacing-md; + + li { + font-size: $font-size-sm; + color: var(--site-base-fgColor); + margin-bottom: $spacing-xs; + + strong { + color: var(--site-primary-color); + } + + code { + font-size: 0.85em; + background-color: var(--site-raised-bgColor); + padding: $spacing-xs; + border-radius: var(--site-radius); + } + + &:last-child { + margin-bottom: 0; + } + } + } + } + + .panel-footer-text { + font-size: $font-size-sm; + color: var(--site-base-fgColor-alt); + margin: $spacing-sm 0 0 0; + font-style: italic; + } + } + } +} + +// Styled tables for FlutterBench pages +#page-content:has(.methodology-hero, .grader-matrix, .dimension-cards-grid) { + .table-wrapper:not(.task-specs-list *) { + @include bench-element-width; + background-color: var(--site-base-bgColor); + border: 1px solid var(--site-inset-borderColor); + border-radius: var(--site-radius); + margin-block: $spacing-md $spacing-lg; + overflow-x: auto; + box-shadow: $shadow-sm; + + table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + margin: 0; + + thead { + background-color: var(--site-raised-bgColor); + border-bottom: 1px solid var(--site-inset-borderColor); + + tr { + th { + padding: $spacing-sm $spacing-md; + font-family: var(--site-ui-fontFamily); + font-size: $font-size-xs; + font-weight: 700; + text-transform: uppercase; + color: var(--site-base-fgColor); + border: none; + text-align: left; + + &:first-child { + padding-left: $spacing-md; + } + + &:last-child { + padding-right: $spacing-md; + } + } + } + } + + tbody { + tr { + background-color: var(--site-base-bgColor); + border-top: 1px solid var(--site-inset-borderColor); + transition: background-color $transition-normal; + + &:first-child { + border-top: none; + } + + &:hover { + background-color: var(--site-inset-bgColor); + } + + td { + padding: $spacing-sm $spacing-md; + font-size: $font-size-sm; + color: var(--site-base-fgColor-alt); + border: none; + vertical-align: top; + + strong { + font-family: var(--site-ui-fontFamily); + font-size: $font-size-sm; + font-weight: 600; + color: var(--site-base-fgColor); + } + + code { + font-size: 0.85em; + background-color: var(--site-raised-bgColor); + padding: $spacing-xs $spacing-sm; + border-radius: var(--site-radius); + } + + &:first-child { + padding-left: $spacing-md; + font-weight: 600; + color: var(--site-base-fgColor); + white-space: nowrap; + } + + &:last-child { + padding-right: $spacing-md; + } + } + } + } + } + } +} + +// CUJ Diagram Component +.cuj-diagram-card { + @include bench-element-width; + background-color: var(--site-base-bgColor); + border: 1px solid var(--site-inset-borderColor); + border-radius: var(--site-radius); + padding: $spacing-md; + margin-block: $spacing-lg $spacing-xl; + box-shadow: $shadow-sm; + display: flex; + flex-direction: column; + gap: $spacing-sm; + + .cuj-diagram-section { + background-color: var(--site-inset-bgColor); + border: 1px solid var(--site-inset-borderColor); + border-radius: var(--site-radius); + padding: $spacing-md $spacing-lg; + display: grid; + grid-template-columns: 4.5rem 1fr; + align-items: center; + gap: $spacing-md; + + @include breakpoints.screen-below('sm') { + grid-template-columns: 1fr; + justify-items: center; + text-align: center; + padding: $spacing-md; + gap: $spacing-sm; + } + + .section-sidebar { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: $spacing-xs; + + .section-icon { + display: flex; + align-items: center; + justify-content: center; + + .material-symbols { + font-size: $font-size-xxl; + font-variation-settings: 'FILL' 1; + } + + &.variant-grey, + &.variant-blue { + color: $color-functional; + } + + &.variant-amber { + color: $color-dx; + } + + &.variant-green { + color: $color-outcome; + } + + &.variant-purple { + color: $color-quality; + } + } + + .section-label { + font-family: var(--site-ui-fontFamily); + font-size: $font-size-sm; + font-weight: 700; + color: var(--site-base-fgColor); + } + } + + .section-items { + display: flex; + flex-direction: column; + gap: $spacing-sm; + width: 100%; + + .cuj-pill { + width: 100%; + padding: $spacing-md $spacing-lg; + border-radius: 9999px; + font-size: $font-size-sm; + font-weight: 500; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + transition: transform $transition-normal; + + &.pill-grey { + background-color: #d1d5db; + border: $border-width solid #858d99; + color: #1f2937; + } + + &.pill-blue { + background-color: #bae6fd; + border: $border-width solid $color-functional; + color: #0c4a6e; + } + + &.pill-amber { + background-color: #fef08a; + border: $border-width solid #eab308; + color: #713f12; + } + + &.pill-green { + background-color: #bbf7d0; + border: $border-width solid #16a34a; + color: #14532d; + } + + &.pill-purple { + background-color: #e9d5ff; + border: $border-width solid #9333ea; + color: #581c87; + } + } + } + } +} + +// Task Specifications Expansion Panels +.task-specs-list { + @include bench-element-width; + background-color: var(--site-inset-bgColor); + border: 1px solid var(--site-inset-borderColor); + border-radius: var(--site-radius); + margin-block: $spacing-lg $spacing-xl; + overflow: hidden; + box-shadow: $shadow-sm; + + .task-spec-panel { + border-bottom: 1px solid var(--site-inset-borderColor); + + &:last-child { + border-bottom: none; + } + + >a.collapsible { + display: flex; + align-items: center; + justify-content: space-between; + padding: $spacing-md $spacing-lg; + background-color: var(--site-base-bgColor); + color: var(--site-base-fgColor); + text-decoration: none; + cursor: pointer; + gap: $spacing-md; + transition: background-color $transition-normal; + + &:hover { + background-color: var(--site-inset-bgColor); + text-decoration: none; + } + + &:not(.collapsed) { + background-color: var(--site-inset-bgColor); + + &::after { + transform: rotate(180deg); + } + } + + &::after { + content: 'keyboard_arrow_down'; + content: 'keyboard_arrow_down' / ''; + font-family: var(--site-icon-fontFamily); + font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; + font-weight: normal; + font-style: normal; + font-size: $font-size-xl; + transition: transform $transition-normal; + color: var(--site-base-fgColor-lighter); + flex-shrink: 0; + } + + .panel-header-left { + display: flex; + align-items: center; + gap: $spacing-md; + flex: 1; + min-width: 0; + + @include breakpoints.screen-below('sm') { + align-items: flex-start; + } + + .panel-icon-wrap { + @include category-icon-background; + flex-shrink: 0; + } + + .panel-header-content { + display: flex; + flex-direction: column; + gap: $spacing-xs; + flex: 1; + min-width: 0; + + .panel-title-row { + display: flex; + align-items: center; + gap: $spacing-sm; + flex-wrap: wrap; + + h4 { + font-family: var(--site-ui-fontFamily); + font-size: $font-size-md; + font-weight: 600; + margin: 0; + color: var(--site-base-fgColor); + } + + .badge { + @include category-badge; + } + } + + .panel-description { + font-size: $font-size-sm; + color: var(--site-base-fgColor-alt); + margin: 0; + } + } + } + } + + .task-spec-body { + padding: $spacing-md $spacing-lg; + background-color: var(--site-base-bgColor); + border-top: 1px solid var(--site-inset-borderColor); + display: none; + + &.show { + display: block; + } + + .lead-text { + margin: 0 0 $spacing-md 0; + + code { + font-size: 0.85em; + background-color: var(--site-raised-bgColor); + padding: $spacing-xs $spacing-sm; + border-radius: var(--site-radius); + } + } + + .table-subheading { + font-size: $font-size-sm; + font-weight: 700; + margin: $spacing-md 0 $spacing-xs 0; + + &:first-of-type { + margin-top: 0; + } + } + + .table-wrapper { + table.spec-table, + table { + width: 100%; + border-spacing: $spacing-sm; + border: none; + font-size: $font-size-sm; + + tbody { + background: transparent; + border: none; + + tr { + td { + vertical-align: top; + + strong { + font-weight: 600; + } + + code { + font-size: 0.85em; + padding: $spacing-xs $spacing-sm; + border-radius: var(--site-radius); + } + + &:first-child { + padding-left: 0; + font-weight: 600; + width: 200px; + } + + &:last-child { + color: var(--site-base-fgColor-alt); + padding-right: 0; + } + } + } + } + } + } + + .footer-text { + font-size: $font-size-sm; + color: var(--site-base-fgColor-alt); + margin: $spacing-sm 0 0 0; + font-style: italic; + + code { + font-size: 0.85em; + background-color: var(--site-raised-bgColor); + padding: $spacing-xs $spacing-sm; + border-radius: var(--site-radius); + font-style: normal; + } + } + } + } +} + +// Dark mode overrides +body.dark-mode { + .methodology-hero { + background: $hero-bg-dark; + box-shadow: $shadow-hero-dark; + } + + .cuj-diagram-card { + .cuj-diagram-section { + background-color: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.08); + + .section-items { + .cuj-pill { + &.pill-grey { + background-color: rgba(148, 163, 184, 0.18); + border-color: rgba(148, 163, 184, 0.45); + color: #f1f5f9; + } + + &.pill-blue { + background-color: rgba(56, 189, 248, 0.18); + border-color: rgba(56, 189, 248, 0.55); + color: #e0f2fe; + } + + &.pill-amber { + background-color: rgba(234, 179, 8, 0.18); + border-color: rgba(234, 179, 8, 0.55); + color: #fef08a; + } + + &.pill-green { + background-color: rgba(34, 197, 94, 0.18); + border-color: rgba(34, 197, 94, 0.55); + color: #dcfce7; + } + + &.pill-purple { + background-color: rgba(168, 85, 247, 0.18); + border-color: rgba(168, 85, 247, 0.55); + color: #f3e8ff; + } + } + } + } + } + + .grader-matrix { + .grader-cards-track .grader-card.cat-llm, + .grader-cards-grid .grader-card.cat-llm { + background-color: rgba($color-llm, 0.08); + } + } + + .reliability-comparison-grid .reliability-card.north-star { + background: $north-star-bg-dark; + border-color: rgba(59, 130, 246, 0.35); + } +} diff --git a/sites/docs/lib/_sass/pages/_story-layout.scss b/sites/docs/lib/_sass/pages/_story-layout.scss new file mode 100644 index 0000000000..0aa2e2ae37 --- /dev/null +++ b/sites/docs/lib/_sass/pages/_story-layout.scss @@ -0,0 +1,314 @@ +@use 'sass:color'; +@use 'package:site_shared/_sass/base/breakpoints'; + +// ----------------------------------------------------------------------------- +// Storytelling Layout Styles +// ----------------------------------------------------------------------------- + +// Color palette shared with _flutter-bench.scss for visual consistency +// NOTE: These colors intentionally match the FlutterBench category colors +$story-progress-gradient: linear-gradient(90deg, #0284c7 0%, #6366f1 50%, #059669 100%); + +$chapter-colors: ( + (rgb: "2, 132, 199", hex: #0284c7), + // Sky blue (functional) + (rgb: "5, 150, 105", hex: #059669), + // Green (outcome) + (rgb: "124, 58, 237", hex: #7c3aed), + // Violet (llm) + (rgb: "99, 102, 241", hex: #6366f1), + // Indigo (quality) + (rgb: "217, 119, 6", hex: #d97706), + // Amber (dx) + (rgb: "100, 116, 139", hex: #64748b) // Slate (grey) +); + +// Scroll-Driven Reading Progress Line +.story-reading-progress { + position: sticky; + top: var(--site-header-height, 4rem); + width: 100%; + height: 3px; + z-index: var(--site-z-top, 1000); + background: rgb(216, 216, 216); + pointer-events: none; + + .story-reading-progress-bar { + height: 100%; + width: 100%; + transform-origin: 0% 50%; + transform: scaleX(0); + background: $story-progress-gradient; + box-shadow: 0 1px 4px rgba(2, 132, 199, 0.25); + + @supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) { + animation: story-reading-progress-grow linear both; + animation-timeline: scroll(root block); + } + } +} + +@keyframes story-reading-progress-grow { + from { + transform: scaleX(0); + } + + to { + transform: scaleX(1); + } +} + +// A layout that provides generous space and visual cues to +// encourage reading a longer doc page. +body.layout-story { + scroll-padding-top: calc(var(--site-header-height, 4rem) + 1.5rem); + + #page-content article { + max-width: 1240px; + width: 100%; + + >#site-content-title, + >.site-breadcrumbs, + >p.description, + >.prev-next-nav, + >.trailing-content { + max-width: 800px; + width: 100%; + } + } + + .story-canvas { + display: flex; + flex-direction: column; + width: 100%; + } + + // Chapter Sections + .story-chapter { + position: relative; + padding-block: 3rem; + scroll-snap-align: start; + scroll-margin-top: calc(var(--site-header-height, 4rem) + 1.5rem); + transition: background-color 0.4s ease, border-color 0.4s ease; + + // Default fallback accent color + --chapter-accent: #0284c7; + --chapter-accent-rgb: 2, 132, 199; + --chapter-next-rgb: 5, 150, 105; + + // Generate chapter accent colors from palette + @for $i from 0 through 20 { + $formatted: if($i < 10, "0#{$i}", "#{$i}"); + $curr-idx: ( + $i % length($chapter-colors)) + 1; + $next-idx: ( + ($i + 1) % length($chapter-colors)) + 1; + + $curr: nth($chapter-colors, $curr-idx + ); + $next: nth($chapter-colors, $next-idx); + + &[data-chapter="#{$formatted}"] { + --chapter-accent: #{map-get($curr, hex)}; + --chapter-accent-rgb: #{map-get($curr, rgb)}; + --chapter-next-rgb: #{map-get($next, rgb)}; + } + } + + // Ambient atmosphere vignette + &::before { + content: ''; + position: absolute; + top: 0; + left: -2rem; + right: -2rem; + height: 100%; + background: + // Main section gradient + radial-gradient(650px circle at 50% 0%, rgba(var(--chapter-accent-rgb), 0.08), transparent 90%), + // Ease into next chapter's gradient + radial-gradient(650px circle at 50% 100%, rgba(var(--chapter-next-rgb), 0.08), transparent 90%); + pointer-events: none; + z-index: 0; + } + + &:first-of-type { + padding-top: clamp(2rem, 5vh, 4rem); + } + + &:last-of-type { + border-bottom: none; + padding-bottom: clamp(6rem, 12vh, 12rem); + } + + // Prologue (intro content before chapter 01) + &.story-prologue { + min-height: auto; + padding-bottom: clamp(3rem, 6vh, 5rem); + } + + // Sticky Chapter Header & Eyebrow Waypoint + .chapter-header-group { + position: sticky; + top: calc(var(--site-header-height, 4rem) + 3px); + z-index: var(--site-z-floating, 10); + padding-block: 0.85rem 0.5rem; + margin-bottom: 2rem; + border-bottom: 1px solid var(--site-outline-variant); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + + .chapter-kicker, + >.header-wrapper { + width: 100%; + margin-inline: auto; + } + + .chapter-kicker { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.35rem; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.75rem; + font-weight: 700; + color: var(--chapter-accent); + + .chapter-number { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.15rem 0.55rem; + border-radius: 9999px; + background: rgba(var(--chapter-accent-rgb), 0.08); + border: 1px solid rgba(var(--chapter-accent-rgb), 0.25); + font-family: var(--site-code-fontFamily); + font-size: 0.8rem; + line-height: 1.2; + color: var(--chapter-accent); + } + + .chapter-rule { + display: inline-block; + width: 2rem; + height: 1px; + background: currentColor; + opacity: 0.35; + } + } + + >.header-wrapper { + margin: 0; + + h2 { + font-weight: 700; + } + } + } + + // Prose Centering vs. Component Breakouts + .chapter-content { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + + // Body prose, lists, blockquotes, tables, notes centered in narrow column + >p, + >ul, + >ol, + >blockquote, + >.table-wrapper, + >.alert, + >.callout, + >.katex-display, + >math { + width: 100%; + max-width: 700px; + line-height: 1.75; + margin-left: 0.5rem; + margin-right: 0.5rem; + } + + >p.description { + font-size: 1.2rem; + line-height: 1.65; + color: var(--site-base-fgColor-alt); + max-width: 800px; + } + + // Sub-headings inside chapter centered to match prose + >.header-wrapper { + width: 100%; + + h3 { + font-size: clamp(1.3rem, 1.8vw, 1.55rem); + font-weight: 600; + margin-top: 2rem; + margin-bottom: 0.5rem; + } + + h4 { + font-size: 1.15rem; + font-weight: 600; + margin-top: 1.5rem; + margin-bottom: 0.5rem; + } + } + } +} +} + +// Dark mode adjustments +body.layout-story.dark-mode { + .story-reading-progress { + background: rgba(255, 255, 255, 0.06); + } + + .story-chapter { + border-bottom-color: rgba(255, 255, 255, 0.08); + + .chapter-header-group { + background: var(--site-base-bgColor); + border-bottom-color: rgba(255, 255, 255, 0.08); + } + + &::before { + background: transparent; + } + + .chapter-kicker .chapter-number { + background: rgba(var(--chapter-accent-rgb), 0.15); + border-color: rgba(var(--chapter-accent-rgb), 0.35); + } + } +} + +// Accessibility: Respect user motion preferences +@media (prefers-reduced-motion: reduce) { + html:has(body.layout-story) { + scroll-snap-type: none; + } + + body.layout-story { + scroll-behavior: auto; + + .story-reading-progress .story-reading-progress-bar { + animation: none !important; + display: none; + } + + .story-chapter { + transition: none !important; + + .chapter-header-group { + position: static; + backdrop-filter: none; + -webkit-backdrop-filter: none; + } + } + } +} \ No newline at end of file diff --git a/sites/docs/lib/main.server.dart b/sites/docs/lib/main.server.dart index 196bed0f2b..116ff26fd4 100644 --- a/sites/docs/lib/main.server.dart +++ b/sites/docs/lib/main.server.dart @@ -31,11 +31,13 @@ import 'src/components/pages/archive_table.dart'; import 'src/components/pages/cuj/cuj_index.dart'; import 'src/components/pages/devtools_release_notes_index.dart'; import 'src/components/pages/expansion_list.dart'; +import 'src/components/pages/flutter_bench_components.dart'; import 'src/components/pages/learning_resource_index.dart'; import 'src/components/pages/platforms_grid.dart'; import 'src/components/pages/widget_catalog.dart'; import 'src/extensions/registry.dart'; import 'src/layouts/doc_layout.dart'; +import 'src/layouts/story_layout.dart'; import 'src/layouts/toc_layout.dart'; import 'src/layouts/tutorial_layout.dart'; import 'src/loaders/data_processor.dart'; @@ -79,6 +81,7 @@ Component get _docsFlutterDevSite => ContentApp.custom( components: _embeddableComponents, layouts: const [ DocLayout(), + StoryLayout(), TocLayout(), TutorialLayout(), ], @@ -120,6 +123,14 @@ List get _embeddableComponents => [ defineComponentWithChild('Card', Card.fromAttributes), defineComponent('LearningResourceIndex', const LearningResourceIndex()), defineComponent('CujIndex', const CujIndex()), + defineComponent('ThreeDimensionsCards', const ThreeDimensionsCards()), + defineComponent('GraderMatrix', const GraderMatrix()), + defineComponent('ReliabilityCards', const ReliabilityCards()), + defineComponent('ScoreTriage', const ScoreTriage()), + defineComponent('EvaluationMatrix', const EvaluationMatrixTabs()), + defineComponent('EvaluationMatrixTabs', const EvaluationMatrixTabs()), + defineComponent('CujDiagram', const CujDiagram()), + defineComponent('TaskSpecifications', const TaskSpecifications()), defineComponentWithAttrs('ArchiveTable', ArchiveTable.fromAttributes), defineComponentWithAttrs( 'DownloadLatestButton', diff --git a/sites/docs/lib/src/client/global_scripts.dart b/sites/docs/lib/src/client/global_scripts.dart index fc71855973..02f9f7f26a 100644 --- a/sites/docs/lib/src/client/global_scripts.dart +++ b/sites/docs/lib/src/client/global_scripts.dart @@ -21,6 +21,8 @@ void setUpSite() { _setUpToc(); _setUpSteppers(); _setUpIdeExplorers(); + _setUpGraderMatrix(); + _setUpInteractiveDetailCards(); } void _setUpSearchKeybindings() { @@ -600,3 +602,223 @@ void _setUpIdeExplorer(web.Element explorer) { explorer.addEventListener('click', handleClick.toJS); } + +/// Set up interactivity for the FlutterBench grader matrix component. +void _setUpGraderMatrix() { + final matrices = web.document.querySelectorAll('.grader-matrix'); + for (var i = 0; i < matrices.length; i++) { + _setUpSingleGraderMatrix(matrices.item(i) as web.Element); + } +} + +void _setUpSingleGraderMatrix(web.Element matrix) { + final buttons = matrix.querySelectorAll('.matrix-filters .filter-btn'); + final cards = matrix.querySelectorAll('.grader-cards-track .grader-card'); + final track = matrix.querySelector('.grader-cards-track') as web.HTMLElement?; + final prevBtn = + matrix.querySelector('.carousel-nav-btn.prev') as web.HTMLElement?; + final nextBtn = + matrix.querySelector('.carousel-nav-btn.next') as web.HTMLElement?; + + if (track == null) return; + + List getVisibleCards() { + final list = []; + for (var i = 0; i < cards.length; i++) { + final card = cards.item(i) as web.HTMLElement; + if (!card.classList.contains('hidden')) { + list.add(card); + } + } + return list; + } + + void scrollNext() { + final visibleCards = getVisibleCards(); + if (visibleCards.isEmpty) return; + + final maxScroll = track.scrollWidth - track.clientWidth; + final currentScroll = track.scrollLeft; + + // Find the first visible card that starts after currentScroll + 10px. + var scrolled = false; + for (final card in visibleCards) { + final cardOffset = card.offsetLeft - track.offsetLeft; + if (cardOffset > currentScroll + 10) { + track.scrollTo( + web.ScrollToOptions( + left: cardOffset.toDouble(), + behavior: 'smooth', + ), + ); + scrolled = true; + break; + } + } + + // If already at or near the end, rotate back to the start. + if (!scrolled || currentScroll >= maxScroll - 10) { + track.scrollTo( + web.ScrollToOptions( + left: 0, + behavior: 'smooth', + ), + ); + } + } + + void scrollPrev() { + final visibleCards = getVisibleCards(); + if (visibleCards.isEmpty) return; + + final maxScroll = track.scrollWidth - track.clientWidth; + final currentScroll = track.scrollLeft; + + // If at or near the beginning, rotate to the end. + if (currentScroll <= 10) { + track.scrollTo( + web.ScrollToOptions( + left: maxScroll.toDouble(), + behavior: 'smooth', + ), + ); + return; + } + + // Find the last visible card that starts before currentScroll - 10px. + for (var i = visibleCards.length - 1; i >= 0; i--) { + final card = visibleCards[i]; + final cardOffset = card.offsetLeft - track.offsetLeft; + if (cardOffset < currentScroll - 10) { + track.scrollTo( + web.ScrollToOptions( + left: cardOffset.toDouble(), + behavior: 'smooth', + ), + ); + return; + } + } + + track.scrollTo( + web.ScrollToOptions( + left: 0, + behavior: 'smooth', + ), + ); + } + + if (nextBtn != null) { + nextBtn.addEventListener( + 'click', + ((web.Event e) { + e.preventDefault(); + scrollNext(); + }).toJS, + ); + } + + if (prevBtn != null) { + prevBtn.addEventListener( + 'click', + ((web.Event e) { + e.preventDefault(); + scrollPrev(); + }).toJS, + ); + } + + for (var i = 0; i < buttons.length; i++) { + final btn = buttons.item(i) as web.HTMLElement; + final filter = btn.dataset['filter']; + + void handleClick(web.Event event) { + event.preventDefault(); + for (var j = 0; j < buttons.length; j++) { + (buttons.item(j) as web.Element).classList.remove('active'); + } + btn.classList.add('active'); + + for (var k = 0; k < cards.length; k++) { + final card = cards.item(k) as web.HTMLElement; + if (filter == 'all') { + card.classList.remove('hidden'); + } else if (filter == 'llm') { + card.classList.toggle('hidden', !card.classList.contains('cat-llm')); + } else if (filter == 'deterministic') { + card.classList.toggle( + 'hidden', + !card.classList.contains('cat-deterministic'), + ); + } else { + card.classList.toggle( + 'hidden', + !card.classList.contains('cat-$filter'), + ); + } + } + + // Reset scroll position to beginning on filter change. + track.scrollTo( + web.ScrollToOptions( + left: 0, + behavior: 'smooth', + ), + ); + } + + btn.addEventListener('click', handleClick.toJS); + } +} + +/// Set up interactivity for FlutterBench interactive detail cards +/// (e.g. ScoreTriage and EvaluationMatrix). +void _setUpInteractiveDetailCards() { + final cards = web.document.querySelectorAll( + '.interactive-detail-card, .score-triage', + ); + for (var i = 0; i < cards.length; i++) { + _setUpSingleInteractiveDetailCard(cards.item(i) as web.Element); + } +} + +void _setUpSingleInteractiveDetailCard(web.Element card) { + final buttons = card.querySelectorAll( + '.card-tabs-grid .card-tab-btn, .triage-tiers-grid .triage-tier-btn', + ); + final panels = card.querySelectorAll( + '.card-panels-container .card-panel, .triage-detail-card .triage-panel', + ); + final detailCard = card.querySelector( + '.card-panels-container, .triage-detail-card', + ) as web.HTMLElement?; + + for (var i = 0; i < buttons.length; i++) { + final btn = buttons.item(i) as web.HTMLElement; + final tabAttr = btn.dataset['tab']; + final tabId = tabAttr.isNotEmpty ? tabAttr : btn.dataset['tier']; + + void handleClick(web.Event event) { + event.preventDefault(); + for (var j = 0; j < buttons.length; j++) { + (buttons.item(j) as web.Element).classList.remove('active'); + } + btn.classList.add('active'); + + for (var k = 0; k < panels.length; k++) { + final panel = panels.item(k) as web.HTMLElement; + final panelTabAttr = panel.dataset['tab']; + final panelId = panelTabAttr.isNotEmpty + ? panelTabAttr + : panel.dataset['tier']; + panel.classList.toggle('active', panelId == tabId); + } + + if (detailCard != null) { + detailCard.scrollTop = 0; + } + } + + btn.addEventListener('click', handleClick.toJS); + } +} diff --git a/sites/docs/lib/src/components/common/comparison_cards_grid.dart b/sites/docs/lib/src/components/common/comparison_cards_grid.dart new file mode 100644 index 0000000000..5bb77660d6 --- /dev/null +++ b/sites/docs/lib/src/components/common/comparison_cards_grid.dart @@ -0,0 +1,81 @@ +// 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/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/util.dart'; + +import '../../utils/inline_code.dart'; + +/// Data model for an individual card in [ComparisonCardsGrid]. +final class ComparisonCardItem { + const ComparisonCardItem({ + required this.tag, + this.tagIcon, + required this.mathPill, + required this.title, + this.isFeatured = false, + this.descriptionParts = const [], + this.description, + }); + + final String tag; + final String? tagIcon; + final String mathPill; + final String title; + final bool isFeatured; + final List> descriptionParts; + final String? description; +} + +/// A responsive comparison grid for metrics, benchmarks, or feature +/// evaluations. +final class ComparisonCardsGrid extends StatelessComponent { + const ComparisonCardsGrid({ + required this.cards, + this.classes = 'reliability-comparison-grid', + super.key, + }); + + final List cards; + final String classes; + + @override + Component build(BuildContext context) { + return div(classes: classes, [ + for (final card in cards) + div( + classes: [ + 'reliability-card', + if (card.isFeatured) 'north-star', + ].toClasses, + [ + div(classes: 'reliability-header', [ + span(classes: 'tag', [ + if (card.tagIcon case final icon?) ...[ + MaterialIcon(icon), + const .text(' '), + ], + .text(card.tag), + ]), + span(classes: 'math-pill', [.text(card.mathPill)]), + ]), + h4([.text(card.title)]), + if (card.descriptionParts.isNotEmpty) + p([ + for (final part in card.descriptionParts) ...[ + if (part['text'] case final String text) .text(text), + if (part['em'] case final String emText) em([.text(emText)]), + if (part['strong'] case final String strongText) + strong([.text(strongText)]), + ], + ]) + else if (card.description case final description?) + p([renderDescriptionWithCode(description)]), + ], + ), + ]); + } +} diff --git a/sites/docs/lib/src/components/common/dimension_cards_grid.dart b/sites/docs/lib/src/components/common/dimension_cards_grid.dart new file mode 100644 index 0000000000..12bf29a601 --- /dev/null +++ b/sites/docs/lib/src/components/common/dimension_cards_grid.dart @@ -0,0 +1,73 @@ +// 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/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/util.dart'; + +import '../../utils/inline_code.dart'; + +/// Data model for an individual card in [DimensionCardsGrid]. +final class DimensionCardItem { + const DimensionCardItem({ + required this.title, + required this.icon, + required this.description, + this.category, + this.footerItems, + this.badge, + }); + + final String title; + final String icon; + final String description; + final String? category; + final String? footerItems; + final String? badge; +} + +/// A grid of cards displaying key dimensions, categories, or metrics. +final class DimensionCardsGrid extends StatelessComponent { + const DimensionCardsGrid({ + required this.cards, + this.classes = 'dimension-cards-grid', + super.key, + }); + + final List cards; + final String classes; + + @override + Component build(BuildContext context) { + return div(classes: classes, [ + for (final card in cards) + div(classes: 'dimension-card', [ + div(classes: 'card-header-row', [ + div( + classes: [ + 'card-icon-wrap', + ?card.category, + ].toClasses, + [MaterialIcon(card.icon)], + ), + h4([.text(card.title)]), + ]), + p([renderDescriptionWithCode(card.description)]), + if (card.footerItems != null || card.badge != null) + div(classes: 'card-footer-info', [ + if (card.footerItems case final footer?) span([.text(footer)]), + if (card.badge case final badge?) + span( + classes: [ + 'badge', + ?card.category, + ].toClasses, + [.text(badge)], + ), + ]), + ]), + ]); + } +} diff --git a/sites/docs/lib/src/components/common/interactive_detail_card.dart b/sites/docs/lib/src/components/common/interactive_detail_card.dart new file mode 100644 index 0000000000..61358fbf38 --- /dev/null +++ b/sites/docs/lib/src/components/common/interactive_detail_card.dart @@ -0,0 +1,140 @@ +// 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/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/util.dart'; + +import '../../utils/inline_code.dart'; + +/// Model for a single tab and corresponding detail panel within +/// [InteractiveDetailCard]. +final class InteractiveDetailTab { + const InteractiveDetailTab({ + required this.id, + required this.primaryLabel, + required this.secondaryLabel, + required this.heading, + this.badge, + this.variant = 'blue', + required this.overview, + this.itemsLabel, + this.items = const [], + this.footerText, + }); + + final String id; + final String primaryLabel; + final String secondaryLabel; + final String heading; + final String? badge; + final String variant; + final String overview; + final String? itemsLabel; + final List items; + final String? footerText; +} + +/// Key-value item displayed within [InteractiveDetailTab]. +final class InteractiveDetailItem { + const InteractiveDetailItem({ + required this.label, + required this.detail, + }); + + final String label; + final String detail; +} + +/// A reusable interactive card featuring selectable tabs and responsive detail +/// panels. +final class InteractiveDetailCard extends StatelessComponent { + const InteractiveDetailCard({ + required this.title, + required this.description, + required this.tabs, + this.classes = 'interactive-detail-card', + super.key, + }); + + final String title; + final String description; + final List tabs; + final String classes; + + @override + Component build(BuildContext context) { + return div(classes: classes, [ + div(classes: 'card-header-area triage-header', [ + h3([.text(title)]), + p([.text(description)]), + ]), + div(classes: 'card-tabs-grid triage-tiers-grid', [ + for (var i = 0; i < tabs.length; i++) + button( + classes: [ + 'card-tab-btn', + 'triage-tier-btn', + 'variant-${tabs[i].variant}', + 'tier-${tabs[i].variant}', + if (i == 0) 'active', + ].toClasses, + attributes: {'data-tab': tabs[i].id, 'data-tier': tabs[i].id}, + [ + div( + classes: 'tab-primary-label tier-score', + [.text(tabs[i].primaryLabel)], + ), + div( + classes: 'tab-secondary-label tier-name', + [.text(tabs[i].secondaryLabel)], + ), + ], + ), + ]), + div(classes: 'card-panels-container triage-detail-card', [ + for (var i = 0; i < tabs.length; i++) + div( + classes: [ + 'card-panel', + 'triage-panel', + if (i == 0) 'active', + ].toClasses, + attributes: {'data-tab': tabs[i].id, 'data-tier': tabs[i].id}, + [ + div(classes: 'panel-heading', [ + h4([.text(tabs[i].heading)]), + if (tabs[i].badge case final badge?) + span( + classes: 'panel-badge badge-${tabs[i].variant}', + [.text(badge)], + ), + ]), + p( + classes: 'panel-overview criteria-text', + [renderDescriptionWithCode(tabs[i].overview)], + ), + if (tabs[i].items.isNotEmpty) + div(classes: 'panel-items-section actions-section', [ + if (tabs[i].itemsLabel case final label?) + div(classes: 'items-label actions-label', [.text(label)]), + ul([ + for (final item in tabs[i].items) + li([ + strong([.text('${item.label}: ')]), + renderDescriptionWithCode(item.detail), + ]), + ]), + ]), + if (tabs[i].footerText case final footer?) + p( + classes: 'panel-footer-text', + [renderDescriptionWithCode(footer)], + ), + ], + ), + ]), + ]); + } +} diff --git a/sites/docs/lib/src/components/common/journey_diagram.dart b/sites/docs/lib/src/components/common/journey_diagram.dart new file mode 100644 index 0000000000..1f8cfccfc7 --- /dev/null +++ b/sites/docs/lib/src/components/common/journey_diagram.dart @@ -0,0 +1,70 @@ +// 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/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/components/common/material_icon.dart'; + +/// Reusable section model for [JourneyDiagram]. +final class JourneySection { + const JourneySection({ + required this.label, + required this.icon, + this.variant = 'blue', + required this.items, + }); + + factory JourneySection.fromMap(Map map) { + return JourneySection( + label: map['label'] as String? ?? '', + icon: map['icon'] as String? ?? 'info', + variant: map['variant'] as String? ?? 'blue', + items: (map['items'] as List? ?? const []) + .map((item) => item.toString()) + .toList(), + ); + } + + final String label; + final String icon; + final String variant; + final List items; +} + +/// Backwards-compatible alias for [JourneySection]. +typedef CujDiagramSection = JourneySection; + +/// A structured, reusable diagram component visualizing a user journey flow +/// or step sequence across configurable sections and nested item pills. +final class JourneyDiagram extends StatelessComponent { + const JourneyDiagram({ + required this.sections, + this.classes = 'cuj-diagram-card', + super.key, + }); + + final List sections; + final String classes; + + @override + Component build(BuildContext context) { + return div(classes: classes, [ + for (final section in sections) + div(classes: 'cuj-diagram-section', [ + div(classes: 'section-sidebar', [ + div(classes: 'section-icon variant-${section.variant}', [ + MaterialIcon(section.icon), + ]), + span(classes: 'section-label', [.text(section.label)]), + ]), + div(classes: 'section-items', [ + for (final item in section.items) + div(classes: 'cuj-pill pill-${section.variant}', [ + .text(item), + ]), + ]), + ]), + ]); + } +} diff --git a/sites/docs/lib/src/components/common/matrix_carousel.dart b/sites/docs/lib/src/components/common/matrix_carousel.dart new file mode 100644 index 0000000000..83fc98e192 --- /dev/null +++ b/sites/docs/lib/src/components/common/matrix_carousel.dart @@ -0,0 +1,135 @@ +// 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/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/util.dart'; + +import '../../utils/inline_code.dart'; + +/// Data model for a filter tab in [MatrixCarousel]. +final class MatrixFilterItem { + const MatrixFilterItem({ + required this.id, + required this.label, + }); + + final String id; + final String label; +} + +/// Data model for an individual card in [MatrixCarousel]. +final class MatrixCardItem { + const MatrixCardItem({ + required this.title, + required this.description, + this.category, + this.categoryLabel, + this.type, + this.typeLabel, + }); + + final String title; + final String description; + final String? category; + final String? categoryLabel; + final String? type; + final String? typeLabel; +} + +/// An interactive, filterable carousel displaying a matrix of cards with +/// categorizations and badges. +final class MatrixCarousel extends StatelessComponent { + const MatrixCarousel({ + required this.title, + required this.description, + this.filters = const [], + required this.cards, + this.classes = 'grader-matrix', + this.previousAriaLabel = 'Previous card', + this.nextAriaLabel = 'Next card', + super.key, + }); + + final String title; + final String description; + final List filters; + final List cards; + final String classes; + final String previousAriaLabel; + final String nextAriaLabel; + + @override + Component build(BuildContext context) { + return div(classes: classes, [ + div(classes: 'matrix-header', [ + div(classes: 'matrix-title-area', [ + h3([.text(title)]), + p([renderDescriptionWithCode(description)]), + ]), + if (filters.isNotEmpty) + div(classes: 'matrix-filters', [ + for (var i = 0; i < filters.length; i++) + button( + classes: i == 0 ? 'filter-btn active' : 'filter-btn', + attributes: {'data-filter': filters[i].id}, + [.text(filters[i].label)], + ), + ]), + ]), + div(classes: 'grader-carousel-wrapper', [ + button( + classes: 'carousel-nav-btn prev', + attributes: { + 'aria-label': previousAriaLabel, + 'title': previousAriaLabel, + }, + const [MaterialIcon('chevron_left')], + ), + div(classes: 'grader-cards-track', [ + for (final card in cards) + div( + classes: [ + 'grader-card', + if (card.category case final category?) 'cat-$category', + if (card.type case final type?) 'cat-$type', + ].toClasses, + [ + if (card.categoryLabel != null || card.typeLabel != null) + div(classes: 'grader-header', [ + if (card.categoryLabel case final categoryLabel?) + span( + classes: [ + 'grader-cat', + ?card.category, + ].toClasses, + [.text(categoryLabel)], + ), + if (card.typeLabel case final typeLabel?) + span( + classes: [ + 'grader-badge', + if (card.type case final type?) 'badge-$type', + ].toClasses, + [.text(typeLabel)], + ), + ]), + h4([.text(card.title)]), + p([renderDescriptionWithCode(card.description)]), + ], + ), + ]), + button( + classes: 'carousel-nav-btn next', + attributes: { + 'aria-label': nextAriaLabel, + 'title': nextAriaLabel, + }, + const [MaterialIcon('chevron_right')], + ), + ]), + ]); + } +} diff --git a/sites/docs/lib/src/components/common/specification_panels.dart b/sites/docs/lib/src/components/common/specification_panels.dart new file mode 100644 index 0000000000..690a3c759a --- /dev/null +++ b/sites/docs/lib/src/components/common/specification_panels.dart @@ -0,0 +1,157 @@ +// 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/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/util.dart'; + +import '../../utils/inline_code.dart'; + +/// Data model for a single row within [SpecTableItem]. +final class SpecTableRowItem { + const SpecTableRowItem({ + required this.label, + required this.description, + }); + + final String label; + final String description; +} + +/// Data model for a specification table with optional title and rows. +final class SpecTableItem { + const SpecTableItem({ + this.title = '', + this.headers = const [], + this.rows = const [], + }); + + final String title; + final List headers; + final List rows; +} + +/// Data model for an individual panel within [SpecificationPanels]. +final class SpecPanelItem { + const SpecPanelItem({ + required this.id, + required this.title, + this.badge = '', + this.badgeVariant = 'blue', + this.icon = 'info', + this.expanded = false, + this.description = '', + this.leadText = '', + this.footerText = '', + this.tables = const [], + }); + + final String id; + final String title; + final String badge; + final String badgeVariant; + final String icon; + final bool expanded; + final String description; + final String leadText; + final String footerText; + final List tables; +} + +/// A collapsible list of specification panels with embedded tables and details. +final class SpecificationPanels extends StatelessComponent { + const SpecificationPanels({ + required this.panels, + this.classes = 'task-specs-list', + this.idPrefix = 'task-spec', + super.key, + }); + + final List panels; + final String classes; + final String idPrefix; + + @override + Component build(BuildContext context) { + return div(classes: classes, [ + for (final spec in panels) + div(classes: 'task-spec-panel', [ + a( + classes: [ + 'collapsible', + if (!spec.expanded) 'collapsed', + ].toClasses, + href: '#$idPrefix-${spec.id}', + attributes: { + 'data-toggle': 'collapse', + 'data-target': '#$idPrefix-${spec.id}', + 'role': 'button', + 'aria-expanded': spec.expanded ? 'true' : 'false', + 'aria-controls': '#$idPrefix-${spec.id}', + }, + [ + div(classes: 'panel-header-left', [ + div( + classes: [ + 'panel-icon-wrap', + 'variant-${spec.badgeVariant}', + ].toClasses, + [MaterialIcon(spec.icon)], + ), + div(classes: 'panel-header-content', [ + div(classes: 'panel-title-row', [ + h4([.text(spec.title)]), + if (spec.badge.isNotEmpty) + span( + classes: 'badge badge-${spec.badgeVariant}', + [.text(spec.badge)], + ), + ]), + if (spec.description.isNotEmpty) + p(classes: 'panel-description', [ + .text(spec.description), + ]), + ]), + ]), + ], + ), + div( + id: '$idPrefix-${spec.id}', + classes: [ + 'task-spec-body collapse', + if (spec.expanded) 'show', + ].toClasses, + [ + if (spec.leadText.isNotEmpty) + p(classes: 'lead-text', [ + renderDescriptionWithCode(spec.leadText), + ]), + for (final tableData in spec.tables) ...[ + if (tableData.title.isNotEmpty) + h5(classes: 'table-subheading', [.text(tableData.title)]), + div(classes: 'table-wrapper', [ + table(classes: 'spec-table', [ + tbody([ + for (final row in tableData.rows) + tr([ + td([ + strong([.text(row.label)]), + ]), + td([renderDescriptionWithCode(row.description)]), + ]), + ]), + ]), + ]), + ], + if (spec.footerText.isNotEmpty) + p(classes: 'footer-text', [ + renderDescriptionWithCode(spec.footerText), + ]), + ], + ), + ]), + ]); + } +} diff --git a/sites/docs/lib/src/components/pages/flutter_bench_components.dart b/sites/docs/lib/src/components/pages/flutter_bench_components.dart new file mode 100644 index 0000000000..d4a69b57d5 --- /dev/null +++ b/sites/docs/lib/src/components/pages/flutter_bench_components.dart @@ -0,0 +1,442 @@ +// 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 '../common/comparison_cards_grid.dart'; +import '../common/dimension_cards_grid.dart'; +import '../common/interactive_detail_card.dart'; +import '../common/journey_diagram.dart'; +import '../common/matrix_carousel.dart'; +import '../common/specification_panels.dart'; + +export '../common/comparison_cards_grid.dart'; +export '../common/dimension_cards_grid.dart'; +export '../common/interactive_detail_card.dart'; +export '../common/journey_diagram.dart'; +export '../common/matrix_carousel.dart'; +export '../common/specification_panels.dart'; + +/// Cards displaying the Three Core Dimensions of FlutterBench evaluation. +final class ThreeDimensionsCards extends StatelessComponent { + const ThreeDimensionsCards({super.key}); + + @override + Component build(BuildContext context) { + final data = FlutterBenchData.fromContext(context); + return DimensionCardsGrid( + cards: [ + for (final dim in data.dimensions) + DimensionCardItem( + title: dim.title, + icon: dim.icon, + category: dim.category, + description: dim.description, + footerItems: dim.footerItems, + badge: dim.badge, + ), + ], + ); + } +} + +/// Interactive filterable Grader Matrix showing evaluation graders in a +/// carousel. +final class GraderMatrix extends StatelessComponent { + const GraderMatrix({super.key}); + + @override + Component build(BuildContext context) { + final data = FlutterBenchData.fromContext(context); + final matrixData = data.graderMatrix; + + return MatrixCarousel( + title: matrixData.title, + description: matrixData.description, + previousAriaLabel: 'Previous grader card', + nextAriaLabel: 'Next grader card', + filters: [ + for (final filter in matrixData.filters) + MatrixFilterItem( + id: filter.id, + label: filter.label, + ), + ], + cards: [ + for (final grader in matrixData.graders) + MatrixCardItem( + title: grader.name, + description: grader.description, + category: grader.category, + categoryLabel: grader.categoryLabel, + type: grader.type, + typeLabel: grader.typeLabel, + ), + ], + ); + } +} + +/// Comparison cards for Capability (pass@k) vs Consistency (pass^k). +final class ReliabilityCards extends StatelessComponent { + const ReliabilityCards({super.key}); + + @override + Component build(BuildContext context) { + final data = FlutterBenchData.fromContext(context); + return ComparisonCardsGrid( + cards: [ + for (final card in data.reliability.cards) + ComparisonCardItem( + tag: card.tag, + tagIcon: card.tagIcon, + mathPill: card.mathPill, + title: card.title, + isFeatured: card.isNorthStar, + descriptionParts: card.descriptionParts, + ), + ], + ); + } +} + +/// Interactive score interpretation and engineering triage matrix. +final class ScoreTriage extends StatelessComponent { + const ScoreTriage({super.key}); + + @override + Component build(BuildContext context) { + final data = FlutterBenchData.fromContext(context); + final triage = data.scoreTriage; + + return InteractiveDetailCard( + title: triage.title, + description: triage.description, + classes: 'interactive-detail-card score-triage', + tabs: [ + for (final tier in triage.tiers) + InteractiveDetailTab( + id: tier.id, + primaryLabel: tier.score, + secondaryLabel: tier.name, + heading: tier.heading, + badge: tier.badge, + variant: tier.id, + overview: tier.criteria, + itemsLabel: tier.actionsLabel, + items: [ + for (final action in tier.actions) + InteractiveDetailItem( + label: action.label, + detail: action.detail, + ), + ], + ), + ], + ); + } +} + +/// Interactive 4-axis evaluation test matrix tabs component. +final class EvaluationMatrixTabs extends StatelessComponent { + const EvaluationMatrixTabs({super.key}); + + @override + Component build(BuildContext context) { + final data = FlutterBenchData.fromContext(context); + final matrix = data.evaluationMatrix; + + return InteractiveDetailCard( + title: matrix.title, + description: matrix.description, + classes: 'interactive-detail-card evaluation-matrix-card', + tabs: [ + for (final axis in matrix.axes) + InteractiveDetailTab( + id: axis.id, + primaryLabel: axis.tabLabel, + secondaryLabel: axis.tabSublabel, + heading: axis.heading, + badge: axis.badge, + variant: axis.variant, + overview: axis.overview, + itemsLabel: axis.itemsLabel, + items: [ + for (final item in axis.items) + InteractiveDetailItem( + label: item.label, + detail: item.detail, + ), + ], + footerText: axis.footerText, + ), + ], + ); + } +} + +/// Expansion panels displaying Task Structure, Categories, Tiers, and Quality +/// Assurance. +final class TaskSpecifications extends StatelessComponent { + const TaskSpecifications({super.key}); + + @override + Component build(BuildContext context) { + final data = FlutterBenchData.fromContext(context); + final specs = data.taskSpecifications; + + return SpecificationPanels( + panels: [ + for (final spec in specs) + SpecPanelItem( + id: spec.id, + title: spec.title, + badge: spec.badge, + badgeVariant: spec.badgeVariant, + icon: spec.icon, + expanded: spec.expanded, + description: spec.description, + leadText: spec.leadText, + footerText: spec.footerText, + tables: [ + for (final table in spec.tables) + SpecTableItem( + title: table.title, + headers: table.headers, + rows: [ + for (final row in table.rows) + SpecTableRowItem( + label: row.label, + description: row.description, + ), + ], + ), + ], + ), + ], + ); + } +} + +/// A structured diagram component visualizing a user journey flow +/// across configurable sections and nested elements. +final class CujDiagram extends StatelessComponent { + const CujDiagram({ + super.key, + this.sections, + }); + + final List? sections; + + @override + Component build(BuildContext context) { + final effectiveSections = + sections ?? FlutterBenchData.fromContext(context).cujExampleSections; + + return JourneyDiagram(sections: effectiveSections); + } +} + +// ----------------------------------------------------------------------------- +// Data Model & Extension Types +// ----------------------------------------------------------------------------- + +/// Root data model for FlutterBench content loaded from `src/data/flutterbench.yml`. +extension type FlutterBenchData._(Map _data) { + static FlutterBenchData fromContext(BuildContext context) { + final raw = + context.page.data['flutterbench'] as Map? ?? const {}; + return FlutterBenchData._(raw); + } + + List get taskSpecifications => + (_data['task_specifications'] as List? ?? const []) + .cast>() + .map(TaskSpecificationData._) + .toList(); + + List get cujExampleSections => + (_data['cuj_example'] as List? ?? const []) + .cast>() + .map(JourneySection.fromMap) + .toList(); + + List get dimensions => + (_data['dimensions'] as List? ?? const []) + .cast>() + .map(DimensionData._) + .toList(); + + GraderMatrixData get graderMatrix => GraderMatrixData._( + _data['grader_matrix'] as Map? ?? const {}, + ); + + ReliabilityData get reliability => ReliabilityData._( + _data['reliability'] as Map? ?? const {}, + ); + + ScoreTriageData get scoreTriage => ScoreTriageData._( + _data['score_triage'] as Map? ?? const {}, + ); + + EvaluationMatrixData get evaluationMatrix => EvaluationMatrixData._( + _data['evaluation_matrix'] as Map? ?? const {}, + ); +} + +extension type EvaluationMatrixData._(Map _data) { + String get title => _data['title'] as String? ?? ''; + String get description => _data['description'] as String? ?? ''; + List get axes => + (_data['axes'] as List? ?? const []) + .cast>() + .map(EvaluationMatrixAxisData._) + .toList(); +} + +extension type EvaluationMatrixAxisData._(Map _data) { + String get id => _data['id'] as String? ?? ''; + String get tabLabel => _data['tab_label'] as String? ?? ''; + String get tabSublabel => _data['tab_sublabel'] as String? ?? ''; + String get heading => _data['heading'] as String? ?? ''; + String get badge => _data['badge'] as String? ?? ''; + String get variant => _data['variant'] as String? ?? 'blue'; + String get overview => _data['overview'] as String? ?? ''; + String get itemsLabel => _data['items_label'] as String? ?? ''; + String get footerText => _data['footer_text'] as String? ?? ''; + List get items => + (_data['items'] as List? ?? const []) + .cast>() + .map(EvaluationMatrixItemData._) + .toList(); +} + +extension type EvaluationMatrixItemData._(Map _data) { + String get label => _data['label'] as String? ?? ''; + String get detail => _data['detail'] as String? ?? ''; +} + +extension type TaskSpecificationData._(Map _data) { + String get id => _data['id'] as String? ?? ''; + String get title => _data['title'] as String? ?? ''; + String get badge => _data['badge'] as String? ?? ''; + String get badgeVariant => _data['badge_variant'] as String? ?? 'blue'; + String get icon => _data['icon'] as String? ?? 'info'; + bool get expanded => _data['expanded'] as bool? ?? false; + String get description => _data['description'] as String? ?? ''; + String get leadText => _data['lead_text'] as String? ?? ''; + String get footerText => _data['footer_text'] as String? ?? ''; + List get tables => + (_data['tables'] as List? ?? const []) + .cast>() + .map(TaskSpecTableData._) + .toList(); +} + +extension type TaskSpecTableData._(Map _data) { + String get title => _data['title'] as String? ?? ''; + List get headers => (_data['headers'] as List? ?? const []) + .map((h) => h.toString()) + .toList(); + List get rows => + (_data['rows'] as List? ?? const []) + .cast>() + .map(TaskSpecTableRowData._) + .toList(); +} + +extension type TaskSpecTableRowData._(Map _data) { + String get label => _data['label'] as String? ?? ''; + String get description => _data['description'] as String? ?? ''; +} + +extension type DimensionData._(Map _data) { + String get title => _data['title'] as String? ?? ''; + String get icon => _data['icon'] as String? ?? ''; + String get category => _data['category'] as String? ?? ''; + String get description => _data['description'] as String? ?? ''; + String get footerItems => _data['footer_items'] as String? ?? ''; + String get badge => _data['badge'] as String? ?? ''; +} + +extension type GraderMatrixData._(Map _data) { + String get title => _data['title'] as String? ?? ''; + String get description => _data['description'] as String? ?? ''; + List get filters => + (_data['filters'] as List? ?? const []) + .cast>() + .map(GraderFilterData._) + .toList(); + List get graders => + (_data['graders'] as List? ?? const []) + .cast>() + .map(GraderCardData._) + .toList(); +} + +extension type GraderFilterData._(Map _data) { + String get id => _data['id'] as String? ?? ''; + String get label => _data['label'] as String? ?? ''; +} + +extension type GraderCardData._(Map _data) { + String get name => _data['name'] as String? ?? ''; + String get category => _data['category'] as String? ?? ''; + String get categoryLabel => _data['category_label'] as String? ?? ''; + String get type => _data['type'] as String? ?? ''; + String get typeLabel => _data['type_label'] as String? ?? ''; + String get description => _data['description'] as String? ?? ''; +} + +extension type ReliabilityData._(Map _data) { + List get cards => + (_data['cards'] as List? ?? const []) + .cast>() + .map(ReliabilityCardData._) + .toList(); +} + +extension type ReliabilityCardData._(Map _data) { + String get tag => _data['tag'] as String? ?? ''; + String? get tagIcon => _data['tag_icon'] as String?; + String get mathPill => _data['math_pill'] as String? ?? ''; + String get title => _data['title'] as String? ?? ''; + bool get isNorthStar => _data['is_north_star'] as bool? ?? false; + List> get descriptionParts => + (_data['description_parts'] as List? ?? const []) + .cast>() + .toList(); +} + +extension type ScoreTriageData._(Map _data) { + String get title => _data['title'] as String? ?? ''; + String get description => _data['description'] as String? ?? ''; + List get tiers => + (_data['tiers'] as List? ?? const []) + .cast>() + .map(ScoreTierData._) + .toList(); +} + +extension type ScoreTierData._(Map _data) { + String get id => _data['id'] as String? ?? ''; + String get score => _data['score'] as String? ?? ''; + String get name => _data['name'] as String? ?? ''; + String get heading => _data['heading'] as String? ?? ''; + String get badge => _data['badge'] as String? ?? 'Criteria & Triage'; + String get criteria => _data['criteria'] as String? ?? ''; + String get actionsLabel => + _data['actions_label'] as String? ?? 'Engineering Triage Actions:'; + List get actions => + (_data['actions'] as List? ?? const []) + .cast>() + .map(ScoreTierActionData._) + .toList(); +} + +extension type ScoreTierActionData._(Map _data) { + String get label => _data['label'] as String? ?? ''; + String get detail => _data['detail'] as String? ?? ''; +} diff --git a/sites/docs/lib/src/extensions/registry.dart b/sites/docs/lib/src/extensions/registry.dart index 51993e1576..9ca48f1dc5 100644 --- a/sites/docs/lib/src/extensions/registry.dart +++ b/sites/docs/lib/src/extensions/registry.dart @@ -6,6 +6,7 @@ import 'package:jaspr_content/jaspr_content.dart'; import 'package:site_shared/page_extensions.dart'; import 'glossary_link_processor.dart'; +import 'story_structure_processor.dart'; import 'tutorial_navigation.dart'; import 'tutorial_structure_processor.dart'; @@ -21,4 +22,5 @@ const List allNodeProcessingExtensions = [ GlossaryLinkProcessor(), TutorialNavigationExtension(), TutorialStructureExtension(), + StoryStructureExtension(), ]; diff --git a/sites/docs/lib/src/extensions/story_structure_processor.dart b/sites/docs/lib/src/extensions/story_structure_processor.dart new file mode 100644 index 0000000000..4b42e084b3 --- /dev/null +++ b/sites/docs/lib/src/extensions/story_structure_processor.dart @@ -0,0 +1,141 @@ +// 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_content/jaspr_content.dart'; + +/// A page extension for Jaspr Content that structures pages using the +/// `story` layout into distinct storytelling chapters. +final class StoryStructureExtension implements PageExtension { + const StoryStructureExtension(); + + @override + Future> apply(Page page, List nodes) async { + final layout = page.data.page['layout']; + if (layout != 'story') { + return nodes; + } + + final chapters = []; + var currentChapterNodes = []; + var chapterIndex = 0; + Node? currentH2Node; + ElementNode? headerNode; + + bool isMeaningfulNode(Node node) { + if (node is TextNode) { + final text = node.text.trim(); + return text.isNotEmpty && !text.startsWith('