diff --git a/components/00-tokens/layout/_layout.scss b/components/00-tokens/layout/_layout.scss index d2b24b1a9..59fd360f6 100644 --- a/components/00-tokens/layout/_layout.scss +++ b/components/00-tokens/layout/_layout.scss @@ -92,7 +92,7 @@ $layout-widths: map.deep-get(tokens.$tokens, size, component-layout, width); margin-top: var(--font-spacing-paragraph-extra); } - &:not(.no-page-spacing) { + &:not(.no-page-spacing, .visually-hidden) { margin-bottom: var(--spacing-page-inner); } } diff --git a/components/01-atoms/images/image/_yds-image.scss b/components/01-atoms/images/image/_yds-image.scss index a4430f5c8..dd300787f 100644 --- a/components/01-atoms/images/image/_yds-image.scss +++ b/components/01-atoms/images/image/_yds-image.scss @@ -32,6 +32,10 @@ figure { color: var(--color-gray-800); } } + + [data-component-layout] & { + color: var(--color-layout-content); + } } @mixin clickable-component-image { diff --git a/components/02-molecules/accordion/_yds-accordion.scss b/components/02-molecules/accordion/_yds-accordion.scss index a21025eec..a5092c2e5 100644 --- a/components/02-molecules/accordion/_yds-accordion.scss +++ b/components/02-molecules/accordion/_yds-accordion.scss @@ -6,6 +6,12 @@ @include tokens.spacing-page-section; --color-text-shadow: var(--color-basic-white); + + // remove margin from the top and bottom of the accordion in a fifty-fifty layout + [data-component-layout='fifty-fifty'] & { + margin-block-start: 0; + margin-block-end: 0; + } } .accordion__heading { diff --git a/components/02-molecules/content-spotlight-portrait/content-spotlights.js b/components/02-molecules/content-spotlight-portrait/content-spotlights.js index 832b2221c..b10b4a729 100644 --- a/components/02-molecules/content-spotlight-portrait/content-spotlights.js +++ b/components/02-molecules/content-spotlight-portrait/content-spotlights.js @@ -2,7 +2,7 @@ Drupal.behaviors.contentSpotlights = { attach(context) { // Define the selectors to check and store them in a variable const selectorsToCheck = - '.text-with-image, .content-spotlight-portrait, .quote-callout, .facts-and-figures__group, .tiles'; + '.text-with-image, .content-spotlight-portrait, .quote-callout, .facts-and-figures__group, .tiles, .yds-layout'; // Select all elements with class "text-with-image" or "content-spotlight-portrait" const contentSpotlights = context.querySelectorAll(selectorsToCheck); diff --git a/components/02-molecules/link-grid/_yds-link-grid.scss b/components/02-molecules/link-grid/_yds-link-grid.scss index 85138d7f3..46b761b9d 100644 --- a/components/02-molecules/link-grid/_yds-link-grid.scss +++ b/components/02-molecules/link-grid/_yds-link-grid.scss @@ -65,6 +65,11 @@ $component-link-grid-themes: map.deep-get(tokens.$tokens, 'component-themes'); &[data-component-theme='five'] { --color-link-grid-action: var(--color-slot-two); } + + // if used in the layout component, remove the top margin + [data-component-layout] & { + margin-block-start: 0; + } } .link-grid__heading { @@ -77,6 +82,11 @@ $component-link-grid-themes: map.deep-get(tokens.$tokens, 'component-themes'); .link-grid__inner { --color-link-grid-border: var(--color-link-grid-action); + // set the border color to the color slot one so there is a default set + [data-component-theme='default'] & { + --color-link-grid-action: var(--color-slot-one); + } + display: flex; flex-flow: column nowrap; width: 100%; @@ -110,6 +120,14 @@ $component-link-grid-themes: map.deep-get(tokens.$tokens, 'component-themes'); &:first-of-type { border-width: var(--size-thickness-4); } + + // if used in fifty-fifty layout and min-width 2xl + // make the columns 50% width + [data-component-layout='fifty-fifty'] & { + @media (min-width: tokens.$break-2xl) { + flex: 1 1 50%; + } + } } .link-grid__list-item { diff --git a/components/03-organisms/layout/layout.stories.js b/components/03-organisms/layout/layout.stories.js index 5f01b7cb7..f21ef911f 100644 --- a/components/03-organisms/layout/layout.stories.js +++ b/components/03-organisms/layout/layout.stories.js @@ -1,17 +1,56 @@ // Markup. import twoColumnTwig from './two-column/_two-column--example.twig'; +import layoutTwig from './layout/_layout--example.twig'; // Data files import textData from '../../02-molecules/text/text-field.yml'; +import accordionData from '../../02-molecules/accordion/accordion.yml'; + +// Image atom component - generic images for demo +import imageData from '../../01-atoms/images/image/image.yml'; + +import '../../02-molecules/accordion/yds-accordion'; /** * Storybook Definition. */ export default { - title: 'Organisms/Layout/Two Column', + title: 'Organisms/Layouts', parameters: { layout: 'fullscreen', }, + argTypes: { + divider: { + name: 'Divider', + type: 'boolean', + }, + layoutOption: { + name: 'Layout', + type: 'select', + options: ['fifty-fifty', 'thirty-thirty-thirty', 'seventy-thirty'], + control: { type: 'select' }, + }, + theme: { + name: 'Component Theme', + type: 'select', + options: ['default', 'one', 'two', 'three', 'four'], + control: { type: 'select' }, + }, + }, + args: { + divider: false, + layoutOption: 'fifty-fifty', + theme: 'default', + }, }; export const TwoColumn = () => twoColumnTwig(textData); +export const layout = ({ divider, theme, layoutOption }) => + layoutTwig({ + ...textData, + ...accordionData, + ...imageData.responsive_images['4x3'], + layout__divider: divider ? 'true' : 'false', + component__theme: theme, + component__layout: layoutOption, + }); diff --git a/components/03-organisms/layout/layout/_layout--example.twig b/components/03-organisms/layout/layout/_layout--example.twig new file mode 100644 index 000000000..276899210 --- /dev/null +++ b/components/03-organisms/layout/layout/_layout--example.twig @@ -0,0 +1,101 @@ +{% extends "@organisms/layout/layout/yds-layout.twig" %} + {% block layout__primary %} + {% include "@molecules/text/yds-text-field.twig" with { + text_field__content: '

Primary Column

The Undergraduate Handbook

A comprehensive guide for prospective and current Chemistry Majors, with a complete description of requirements and opportunities.

View the handbook online

Course List

A list of courses and a description of the Chemistry undergraduate program, including information on placement exams, laboratory registration, premedical students, and major requirements

Yale College programs of study

', + } %} + {% endblock %} + {% block layout__secondary %} + {% if component__layout == 'fifty-fifty' %} + {% include "@molecules/link-grid/yds-link-grid.twig" with { + link_grid__heading: 'Quick Links', + link_grid__theme: 'inherit', + link_grid__links_one: [ + { + link_grid__link__content: 'Undergraduate Handbook', + link_grid__link__url: '#' + }, + { + link_grid__link__content: 'Course List', + link_grid__link__url: '#' + }, + { + link_grid__link__content: 'Placement Exams', + link_grid__link__url: '#' + }, + ], + link_grid__links_two: [ + { + link_grid__link__content: 'Laboratory Registration', + link_grid__link__url: '#' + }, + { + link_grid__link__content: 'Premedical Students', + link_grid__link__url: '#' + }, + { + link_grid__link__content: 'Major Requirements', + link_grid__link__url: '#' + }, + ], + link_grid__links_three: [ + { + link_grid__link__content: 'Yale College Programs of Study', + link_grid__link__url: '#' + }, + { + link_grid__link__content: 'Yale College Programs of Study', + link_grid__link__url: '#' + }, + { + link_grid__link__content: 'Yale College Programs of Study', + link_grid__link__url: '#' + }, + ], + link_grid__links_four: [ + { + link_grid__link__content: 'Yale College Programs of Study', + link_grid__link__url: '#' + }, + { + link_grid__link__content: 'Yale College Programs of Study', + link_grid__link__url: '#' + }, + { + link_grid__link__content: 'Yale College Programs of Study', + link_grid__link__url: '#' + }, + ], + }%} + + {% include "@molecules/accordion/yds-accordion.twig" with { + accordion__heading: 'Accordion Group', + accordion__width: 'site', + accordion__alignment: 'left', + accordion__items: [ + { + accordion__item__heading: accordion__item__heading, + accordion__item__content: accordion__item__content + }, + { + accordion__item__heading: 'Books', + accordion__item__content: accordion__item__content + }, + { + accordion__item__heading: 'Courses', + accordion__item__content: accordion__item__content + }, + { + accordion__item__heading: 'Events', + accordion__item__content: accordion__item__content + }, + ] + } %} + {% else %} + {% include "@molecules/text/yds-text-field.twig" with { + text_field__content: '

Secondary Column

The Undergraduate Handbook

A comprehensive guide for prospective and current Chemistry Majors, with a complete description of requirements and opportunities.

View the handbook online

Course List

A list of courses and a description of the Chemistry undergraduate program, including information on placement exams, laboratory registration, premedical students, and major requirements

Yale College programs of study

', + } %} + {% endif %} + {% endblock %} + {% block layout__tertiary %} + {% include "@atoms/images/image/_responsive-image.twig" %} + {% endblock %} diff --git a/components/03-organisms/layout/layout/_yds-layout.scss b/components/03-organisms/layout/layout/_yds-layout.scss new file mode 100644 index 000000000..79ade2a83 --- /dev/null +++ b/components/03-organisms/layout/layout/_yds-layout.scss @@ -0,0 +1,270 @@ +@use '../../../00-tokens/tokens'; +@use '../../../00-tokens/functions/map'; +@use '../../../01-atoms/atoms'; +@use '../../grid-mixins' as grid; + +$global-layout-themes: map.deep-get(tokens.$tokens, 'global-themes'); +$layout-component-themes: map.deep-get(tokens.$tokens, 'component-themes'); +$break-layout-layout: tokens.$break-2xl; +$break-layout-layout-max: $break-layout-layout - 0.05; + +.yds-layout { + --color-layout-border: var(--color-slot-one); + + // Component themes defaults: iterate over each component theme to establish + // default variables. + @each $theme, $value in $layout-component-themes { + &[data-component-theme='#{$theme}'] { + // prettier-ignore + --color-slot-one: var(--component-themes-#{$theme}-slot-one); + --color-slot-two: var(--component-themes-#{$theme}-slot-two); + --color-slot-three: var(--component-themes-#{$theme}-slot-three); + --color-slot-four: var(--component-themes-#{$theme}-slot-four); + --color-slot-five: var(--component-themes-#{$theme}-slot-five); + --color-slot-six: var(--component-themes-#{$theme}-slot-six); + --color-slot-seven: var(--component-themes-#{$theme}-slot-seven); + --color-slot-eight: var(--component-themes-#{$theme}-slot-eight); + --color-layout-theme: var(--color-slot-one); + --color-layout-content: var(--color-slot-eight); + --color-layout-border: var(--color-slot-four); + --color-link-base: var(--color-link-base); + --color-link-hover: var(--color-link-hover); + + background-color: var(--color-layout-theme); + color: var(--color-layout-content); + + // override text-shadow color for links in component themes + .link, + .text-field a, + .link-grid__link { + --color-text-shadow: var(--color-layout-theme); + --color-link-hover: var(--color-link-hover); + --color-link-base: var(--color-link-base); + } + } + } + + // Global themes: set color slots for each theme + // This establishes `--color-slot-` variables name-spaced to the selector + // in which it is used. We can map component-level variables to global-level + // `--color-slot-` variables. + @each $globalTheme, $value in $global-layout-themes { + [data-global-theme='#{$globalTheme}'] & { + --color-slot-one: var(--global-themes-#{$globalTheme}-colors-slot-one); + --color-slot-two: var(--global-themes-#{$globalTheme}-colors-slot-two); + --color-slot-three: var( + --global-themes-#{$globalTheme}-colors-slot-three + ); + --color-slot-four: var(--global-themes-#{$globalTheme}-colors-slot-four); + --color-slot-five: var(--global-themes-#{$globalTheme}-colors-slot-five); + --color-slot-six: var(--global-themes-#{$globalTheme}-colors-slot-six); + --color-slot-seven: var( + --global-themes-#{$globalTheme}-colors-slot-seven + ); + --color-slot-eight: var( + --global-themes-#{$globalTheme}-colors-slot-eight + ); + + @if $globalTheme == 'four' { + // Switch colors slot in order to have the selected background colors per component theme. + --color-slot-two: var(--global-themes-four-colors-slot-five); + --color-slot-five: var(--global-themes-four-colors-slot-two); + + // Set color slot for the text on light background. + --color-slot-seven: var(--global-themes-four-colors-slot-six); + } + } + } + + // Component theme overrides: set specific component themes overrides + /// define component name spaced variables and map them to global theme slots. + &[data-component-theme='one'] { + --color-layout-theme: var(--color-slot-one); + --color-layout-content: var(--color-slot-eight); + --color-link-visited-base: var(--color-link-visited-light); + --color-link-visited-hover: var(--color-link-visited-light-hover); + --color-link-base: var(--color-slot-eight); + --color-link-hover: var(--color-slot-eight); + --color-layout-border: var(--color-slot-four); + --color-heading: var(--color-slot-eight); + --color-link-grid-action: var(--color-slot-eight); + } + + &[data-component-theme='two'] { + --color-layout-theme: var(--color-slot-four); + --color-layout-content: var(--color-slot-seven); + --color-link-base: var(--color-slot-seven); + --color-link-hover: var(--color-slot-seven); + --color-heading: var(--color-slot-six); + --color-layout-border: var(--color-slot-six); + --color-link-grid-action: var(--color-slot-six); + } + + &[data-component-theme='three'] { + --color-layout-theme: var(--color-slot-five); + --color-layout-content: var(--color-slot-eight); + --color-link-visited-base: var(--color-link-visited-light); + --color-link-visited-hover: var(--color-link-visited-light-hover); + --color-link-base: var(--color-slot-eight); + --color-link-hover: var(--color-slot-eight); + --color-layout-border: var(--color-slot-four); + --color-heading: var(--color-slot-eight); + --color-link-grid-action: var(--color-slot-four); + } + + &[data-component-theme='four'] { + --color-layout-theme: var(--color-slot-two); + --color-layout-content: var(--color-slot-eight); + --color-link-visited-base: var(--color-link-visited-light); + --color-link-visited-hover: var(--color-link-visited-light-hover); + --color-link-base: var(--color-slot-eight); + --color-link-hover: var(--color-slot-eight); + --color-layout-border: var(--color-slot-four); + --color-heading: var(--color-slot-eight); + --color-link-grid-action: var(--color-slot-four); + } + + // if we're not using a component theme, add top and bottom margin + // Or if the spotlight is the only spotlight on the page + &[data-component-theme='default'], + &[data-spotlights-position='first-and-last'] { + @include tokens.spacing-page-section; + } + + // We're using JavaScript to evaluate the last and first spotlights in a group of spotlights + &[data-spotlights-position='first'] { + margin-top: var(--size-spacing-10); + } + + &[data-spotlights-position='last'] { + margin-bottom: var(--size-spacing-10); + } +} + +.yds-layout__inner { + padding-block-start: var(--size-spacing-10); + padding-block-end: var(--size-spacing-10); + + @media (min-width: $break-layout-layout) { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: var(--spacing-page-inner); + } + + // Flex sets `min-width` of children to `auto` by default... but this breaks + // some of our base layout styles (causes horizontal scrolling), so we need to + // reset it to `0`. + > * { + min-width: 0; + + // Then, some of our components that are intended to be "full-page-width" + // will expand outside of the layout columns, so we need to set a max-width + // on those to keep them contained within the section columns. + > * { + max-width: 100%; + } + } +} + +.yds-layout__divider { + display: flex; + align-self: stretch; + background-color: var(--color-layout-border); + width: var(--border-thickness-2); + opacity: 0.5; + + // gap affects the width of the divider, so we can increase the width of the + // divider in this instance. + [data-component-layout='seventy-thirty'] & { + width: calc(var(--border-thickness-2) + var(--border-thickness-1)); + } + + @media (max-width: $break-layout-layout-max) { + width: 100%; + height: var(--border-thickness-2); + margin-bottom: var(--spacing-page-inner); + } +} + +.yds-layout__primary { + flex-flow: column nowrap; + + @media (max-width: $break-layout-layout-max) { + margin-bottom: var(--spacing-page-inner); + } + + [data-component-layout='fifty-fifty'] & { + @media (min-width: $break-layout-layout) { + flex: 0 1 calc(var(--size-component-layout-width-site) / 2); + } + } + + [data-component-layout='thirty-thirty-thirty'] & { + @media (min-width: $break-layout-layout) { + flex: 0 1 calc(var(--size-component-layout-width-site) / 3); + } + } + + [data-component-layout='seventy-thirty'] & { + @media (min-width: $break-layout-layout) { + flex: 1 0 var(--size-component-layout-width-content); + } + } +} + +.yds-layout__secondary { + display: flex; + flex-flow: column nowrap; + + [data-component-layout='fifty-fifty'] & { + @media (min-width: $break-layout-layout) { + flex: 0 1 calc(var(--size-component-layout-width-site) / 2); + } + } + + [data-component-layout='thirty-thirty-thirty'] & { + @media (min-width: $break-layout-layout) { + flex: 0 1 calc(var(--size-component-layout-width-site) / 3); + } + } + + [data-component-layout='seventy-thirty'] & { + @media (min-width: $break-layout-layout) { + flex: 0 1 calc(37.5rem + var(--spacing-component-gutter)); + } + } +} + +.yds-layout__tertiary { + display: flex; + flex-flow: column nowrap; + + [data-component-layout='thirty-thirty-thirty'] & { + @media (min-width: $break-layout-layout) { + flex: 0 1 calc(var(--size-component-layout-width-site) / 3); + } + } +} + +// Update font-size for headings based on layout +h2 { + [data-component-layout='seventy-thirty'] &, + [data-component-layout='thirty-thirty-thirty'] & { + @include tokens.h3-yale-new; + } +} + +h3 { + [data-component-layout='seventy-thirty'] &, + [data-component-layout='thirty-thirty-thirty'] & { + @include tokens.h4-yale-new; + } +} + +h4 { + [data-component-layout='seventy-thirty'] &, + [data-component-layout='thirty-thirty-thirty'] & { + @include tokens.h5-yale-new; + } +} diff --git a/components/03-organisms/layout/layout/yds-layout.twig b/components/03-organisms/layout/layout/yds-layout.twig new file mode 100644 index 000000000..a38edbf05 --- /dev/null +++ b/components/03-organisms/layout/layout/yds-layout.twig @@ -0,0 +1,50 @@ +{# + # Available Variables: + # - component__theme (string): 'default' by default + # - layout__divider: (boolean) false by default + # - component__layout (string): 'fifty-fifty' by default + # - component__width (string): 'site' by default + + # Available Blocks: + # - layout_primary + # - layout_secondary + # - layout_tertiary + #} + +{% set layout__base_class = 'yds-layout' %} + +{% set layout__attributes = { + 'data-component-has-divider': layout__divider == 'true' ? 'true' : 'false', + 'data-component-theme': component__theme|default('default'), + 'data-component-layout': component__layout|default('fifty-fifty'), + 'data-component-width': component__width|default('site'), + 'class': bem(layout__base_class), +} %} + +
+
+
+ {% block layout__primary %} + {{ content.primary }} + {% endblock %} +
+ {% if layout__divider == 'true' %} +
+ {% endif %} +
+ {% block layout__secondary %} + {{ content.secondary }} + {% endblock %} +
+ {% if layout__divider == 'true' and component__layout == 'thirty-thirty-thirty' %} +
+ {% endif %} + {% if component__layout == 'thirty-thirty-thirty' %} +
+ {% block layout__tertiary %} + {{ content.tertiary }} + {% endblock %} +
+ {% endif %} +
+
diff --git a/components/03-organisms/organisms.scss b/components/03-organisms/organisms.scss index f0c080d1a..de12cb86c 100644 --- a/components/03-organisms/organisms.scss +++ b/components/03-organisms/organisms.scss @@ -13,3 +13,4 @@ @forward './site-header/yds-site-header'; @forward './facts-and-figures-group/yds-facts-and-figures-group'; @forward './tiles/yds-tiles'; +@forward './layout/layout/yds-layout'; diff --git a/components/04-page-layouts/page-layouts.scss b/components/04-page-layouts/page-layouts.scss index 511f23f8a..8bb86ae83 100644 --- a/components/04-page-layouts/page-layouts.scss +++ b/components/04-page-layouts/page-layouts.scss @@ -19,15 +19,23 @@ body[data-body-frozen] { } } +// The last item inside the `.main-content` area should have some space between +// it and the site footer (size-spacing-12 below) - unless it is designated as +// `$flush-bottom` above. Then it will have no bottom-margin separating it from +// the site footer. +.main-content > *:last-child { + margin-bottom: var(--main-content-bottom-margin, var(--size-spacing-13)); +} + // The first item inside the `.main-content` area should have some space between // it and the site header (size-spacing-6 and 9 below) - unless it is designated // as `$flush-top` above. Then it will have no top-margin separating it from the // site header. -.main-content > *:first-child { +.main-content > *:first-child:not(.page-meta, .layout--banner) { margin-top: var(--main-content-top-margin, var(--size-spacing-6)); } -.main-content .page-title { +.main-content .page-title:not(.visually-hidden) { margin-top: var(--size-spacing-10); // 4rem @media (max-width: tokens.$break-l) { @@ -56,14 +64,6 @@ body[data-body-frozen] { } } -// The last item inside the `.main-content` area should have some space between -// it and the site footer (size-spacing-12 below) - unless it is designated as -// `$flush-bottom` above. Then it will have no bottom-margin separating it from -// the site footer. -.main-content > *:last-child { - margin-bottom: var(--main-content-bottom-margin, var(--size-spacing-13)); -} - body { --site-header-height: 4.8125rem; // 77px / 16 }