From 33ddc5b21e958a5cd0feb2d900548123afb6a465 Mon Sep 17 00:00:00 2001 From: Eric Windmill Date: Thu, 20 Aug 2026 12:02:59 -0700 Subject: [PATCH 01/14] refactor: generalize filterable index layout and components Extract generic two-column filterable index layout styles and sidebar/search components from learning resources into reusable filterable_index.dart and _filterable-index.scss. --- sites/docs/lib/_sass/_site.scss | 1 + .../_sass/components/_filterable-index.scss | 224 +++++++++++++++++ .../pages/_learning-resources-index.scss | 233 +----------------- .../components/pages/filterable_index.dart | 153 ++++++++++++ .../learning_resource_filters_sidebar.dart | 130 +++++----- .../pages/learning_resource_index.dart | 4 +- 6 files changed, 441 insertions(+), 304 deletions(-) create mode 100644 sites/docs/lib/_sass/components/_filterable-index.scss create mode 100644 sites/docs/lib/src/components/pages/filterable_index.dart diff --git a/sites/docs/lib/_sass/_site.scss b/sites/docs/lib/_sass/_site.scss index 99092c6a676..a4d52a33ee6 100644 --- a/sites/docs/lib/_sass/_site.scss +++ b/sites/docs/lib/_sass/_site.scss @@ -14,6 +14,7 @@ @use 'components/content'; @use 'components/expansion-list'; @use 'components/filter-search'; +@use 'components/filterable-index'; @use 'components/footer'; @use 'components/header'; @use 'components/icons'; diff --git a/sites/docs/lib/_sass/components/_filterable-index.scss b/sites/docs/lib/_sass/components/_filterable-index.scss new file mode 100644 index 00000000000..1d881c23a3c --- /dev/null +++ b/sites/docs/lib/_sass/components/_filterable-index.scss @@ -0,0 +1,224 @@ +// Layout and controls shared by the filterable index pages: the learning +// resources index and the critical user journey index. +// +// The markup comes from the `FiltersSidebar` and `FilterSearchGroup` +// components in `filterable_index.dart`, so everything here is keyed off +// classes. Each page styles its own results list separately, keyed off the +// id of that list. + +$mobile-breakpoint: 839px; +$sidebar-width: 220px; + +.filterable-index { + display: flex; + flex-direction: row; + + .left-col { + margin-right: 1rem; + flex: 2; + } + + .right-col { + width: $sidebar-width; + } +} + +// The sidebar follows the page as it scrolls on wide screens, and becomes an +// off-canvas drawer on narrow ones. Everything within it, including any +// page-specific footer, slides in and out as a single unit. +.filter-sidebar { + position: sticky; + top: calc(var(--site-header-height) + 1rem); +} + +.filter-group-wrapper { + border: 1px solid var(--site-inset-borderColor); + background-color: var(--site-inset-bgColor); + border-radius: var(--site-radius); + overflow: hidden; +} + +// Only shown while the sidebar is a drawer, to close it again. +.filter-header { + display: none; + position: absolute; + top: 0; + right: 0; + padding: 0.75rem 1rem; + z-index: 10; +} + +.close-icon { + cursor: pointer; + padding: 0.25rem; + border-radius: 4px; + background: none; + border: none; + color: var(--site-base-fgColor-alt); + transition: all 0.2s ease; + position: absolute; + right: 1rem; + top: 75%; + transform: translateY(-50%); + + &:hover { + background-color: rgba(0, 0, 0, 0.1); + color: var(--site-primary-color); + } + + .material-symbols { + font-size: 20px; + } +} + +.filter-group { + .table-title { + text-align: center; + color: var(--site-base-fgColor-alt); + background-color: var(--site-raised-bgColor); + font-family: var(--site-ui-fontFamily); + font-weight: 600; + font-size: .925rem; + padding: .5rem; + border-bottom: 1px solid var(--site-inset-borderColor); + } + + .table-content { + padding: 1rem; + } + + ul { + padding-left: 0; + margin-bottom: .5rem; + + li { + list-style: none; + padding-left: 0; + padding-bottom: .25rem; + display: flex; + align-items: center; + + label { + padding-left: .35rem; + font-size: .9rem; + } + } + } + + h4 { + margin: 0 0 .5rem; + padding: 0; + } + + button { + color: var(--site-primary-color); + margin: 0 0 2rem; + padding: .5rem 0 1rem 1rem; + + &:hover { + color: var(--site-onPrimary-color); + } + } + + .hidden { + display: none; + } +} + +.filter-search-group { + display: flex; + flex-direction: column; + + button.show-filters-button { + @media (min-width: $mobile-breakpoint + 1) { + display: none; + } + } + + .label-row { + display: flex; + justify-content: space-between; + font-size: .925rem; + + label { + font-family: var(--site-ui-fontFamily); + color: var(--site-base-fgColor-lighter); + + padding: .25rem 1rem 0 0; + margin: 0; + text-align: end; + } + + button { + padding: .25rem; + color: var(--site-primary-color); + display: flex; + align-items: center; + + &:hover { + color: var(--site-onPrimary-color-light); + } + + &:disabled, + &[disabled] { + color: var(--site-inset-bgColor-translucent); + cursor: default; + } + } + } +} + +// On narrow screens the sidebar becomes a drawer that slides in from the +// right. Opening and closing is driven entirely by the hidden +// `#open-filter-toggle` checkbox that precedes it. +@media (max-width: $mobile-breakpoint) { + .filterable-index { + flex-direction: column; + } + + .left-col { + margin-right: 0; + } + + // The drawer itself is fixed-positioned, so this column no longer needs to + // reserve width for it in the flex layout. + .right-col { + width: auto; + } + + .filter-header { + display: block; + } + + .filter-sidebar { + position: fixed; + top: var(--site-header-height); + bottom: 0; + right: -$sidebar-width; + width: $sidebar-width; + display: flex; + flex-direction: column; + background-color: var(--site-inset-bgColor); + border-left: 1px solid var(--site-inset-borderColor); + transition: right 0.3s ease-in-out; + z-index: 1000; + } + + #open-filter-toggle:checked + .filter-sidebar { + right: 0; + } + + // Fill the drawer rather than floating within it as a card, + // and scroll on its own so any footer stays in view. + .filter-group-wrapper { + flex: 1; + min-height: 0; + overflow-y: auto; + border: none; + border-radius: 0; + } + + .filter-group .table-title { + background-color: var(--site-raised-bgColor-translucent); + } +} diff --git a/sites/docs/lib/_sass/pages/_learning-resources-index.scss b/sites/docs/lib/_sass/pages/_learning-resources-index.scss index 0e6581adbd5..5d316703d11 100644 --- a/sites/docs/lib/_sass/pages/_learning-resources-index.scss +++ b/sites/docs/lib/_sass/pages/_learning-resources-index.scss @@ -1,235 +1,8 @@ @use 'package:site_shared/_sass/base/mixins'; -#resource-filter-group-wrapper { - border: 1px solid var(--site-inset-borderColor); - background-color: var(--site-inset-bgColor); - border-radius: var(--site-radius); - overflow: hidden; - position: sticky; - top: calc(var(--site-header-height) + 1rem); -} - -.filter-header { - display: none; - position: absolute; - top: 0; - right: 0; - padding: 0.75rem 1rem; - z-index: 10; -} - -.close-icon { - cursor: pointer; - padding: 0.25rem; - border-radius: 4px; - background: none; - border: none; - color: var(--site-base-fgColor-alt); - transition: all 0.2s ease; - position: absolute; - right: 1rem; - top: 75%; - transform: translateY(-50%); - - &:hover { - background-color: rgba(0, 0, 0, 0.1); - color: var(--site-primary-color); - } - - .material-symbols { - font-size: 20px; - } -} - -// Mobile screen customizations. -@media (max-width: 839px) { - .filter-header { - display: block; - } - - #resource-filter-group-wrapper { - position: fixed; - top: var(--site-header-height); - bottom: 0; - right: -220px; - width: 220px; - border-bottom: none; - height: 100%; - border-radius: 0; - transition: right 0.3s ease-in-out; - z-index: 1000; - } - - #open-filter-toggle:not(:checked)+#resource-filter-group-wrapper { - right: -220px; - } - - #open-filter-toggle:checked+#resource-filter-group-wrapper { - right: 0; - } -} - -//Desktop screens -@media (min-width: 840px) { - #resource-filter-group { - position: static !important; - right: auto !important; - } - - .filter-header { - display: none !important; - } -} - -#resource-index-content { - display: flex; - flex-direction: row; - - .left-col { - margin-right: 1rem; - flex: 2; - } - - .right-col { - width: 220px; - - @media (max-width: 840px) { - position: fixed; - top: var(--site-header-height); - bottom: 0; - right: -15rem; - box-shadow: 0 6px 18px 0 rgba(0, 0, 0, 0.2); - border-radius: 0.4rem; - width: 220px; - - @keyframes slidein { - 0% { - right: -10rem; - } - - 100% { - right: 0; - } - } - - &.show { - animation-duration: 500ms; - animation-delay: 200ms; - animation-name: slidein; - animation-iteration-count: 1; - animation-timing-function: ease; - animation-fill-mode: forwards; - } - } - } -} - -#resource-filter-group { - .table-title { - text-align: center; - color: var(--site-base-fgColor-alt); - background-color: var(--site-raised-bgColor); - font-family: var(--site-ui-fontFamily); - font-weight: 600; - font-size: .925rem; - padding: .5rem; - border-bottom: 1px solid var(--site-inset-borderColor); - } - - .table-content { - padding: 1rem; - } - - ul { - padding-left: 0; - margin-bottom: .5rem; - - li { - list-style: none; - padding-left: 0; - padding-bottom: .25rem; - display: flex; - align-items: center; - - label { - padding-left: .35rem; - font-size: .9rem; - } - } - } - - h4 { - margin: 0 0 .5rem; - padding: 0; - } - - button { - color: var(--site-primary-color); - margin: 0 0 2rem; - padding: .5rem 0 1rem 1rem; - - &:hover { - color: var(--site-onPrimary-color); - } - } - - @media (max-width: 840px) { - border-bottom: none; - height: 100%; - border-radius: 0; - - .table-title { - background-color: var(--site-raised-bgColor-translucent); - } - } - - .hidden { - display: none; - } -} - -#resource-search-group { - display: flex; - flex-direction: column; - - button.show-filters-button { - @media (min-width: 840px) { - display: none; - } - } - - .label-row { - display: flex; - justify-content: space-between; - font-size: .925rem; - - label { - font-family: var(--site-ui-fontFamily); - color: var(--site-base-fgColor-lighter); - - padding: .25rem 1rem 0 0; - margin: 0; - text-align: end; - } - - button { - padding: .25rem; - color: var(--site-primary-color); - display: flex; - align-items: center; - - &:hover { - color: var(--site-onPrimary-color-light); - } - - &:disabled, - &[disabled] { - color: var(--site-inset-bgColor-translucent); - cursor: default; - } - } - } -} +// The learning resources index shares its two column layout, search field, +// and filter sidebar with the critical user journey index, so those styles +// live in `_filterable-index.scss`. #all-resources-grid { margin-block-start: 1rem; diff --git a/sites/docs/lib/src/components/pages/filterable_index.dart b/sites/docs/lib/src/components/pages/filterable_index.dart new file mode 100644 index 00000000000..9f6cc824959 --- /dev/null +++ b/sites/docs/lib/src/components/pages/filterable_index.dart @@ -0,0 +1,153 @@ +// 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. + +/// The shell components shared by the filterable index pages: +/// the learning resources index and the critical user journey index. +/// +/// The pages provide their own filter controls and results list, +/// but the surrounding layout, search field, and sidebar are the same, +/// and are styled by `_filterable-index.scss`. +library; + +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/components/common/search.dart'; +import 'package:site_shared/components/utils/global_event_listener.dart'; +import 'package:universal_web/web.dart' as web; + +/// The id of the hidden checkbox that opens and closes the filter sidebar +/// while it is a drawer on narrow screens. +/// +/// The drawer is CSS-only: `_filterable-index.scss` keys off the checkbox's +/// `:checked` state, and the close button within the sidebar is a `