/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
.discovery-stream.ds-layout.ds-layout-widgets {
margin-inline: 0;
}
.widgets-wrapper {
margin-inline: auto;
padding-block-end: var(--space-large);
// Classic layout
// @nova-cleanup(remove-conditional): Remove classic max-width after Nova ships
.classic-enabled & {
max-width: max-content;
}
// Nova layout
// @nova-cleanup(remove-conditional): Make Nova grid default, remove .nova-enabled selector
.nova-enabled & {
display: contents;
}
}
.widgets-section-container {
// Classic layout
// @nova-cleanup(remove-conditional): Remove classic padding/background styles after Nova ships
.classic-enabled & {
padding-block: var(--space-xsmall) var(--space-large);
padding-inline: var(--space-large);
background-color: var(--button-background-color);
border-radius: var(--border-radius-large);
// Bug 1908010 - This overwrites the design system color because of a
// known transparency issue with color-mix syntax when a wallpaper is set
.lightWallpaper &,
.darkWallpaper & {
background-color: var(--newtab-weather-background-color); // stylelint-disable-line stylelint-plugin-mozilla/use-design-tokens
@media (prefers-contrast) {
background-color: var(--background-color-box);
}
}
}
// Nova layout
// @nova-cleanup(remove-conditional): Make Nova grid default, remove .nova-enabled selector
.nova-enabled & {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(
auto-fill,
calc(4 * var(--col-width) + 3 * var(--space-medium))
);
gap: var(--space-medium);
justify-content: center;
}
.widgets-title-container {
// @nova-cleanup(remove-conditional): Make grid-column default, remove .nova-enabled selector
.nova-enabled & {
grid-column: 1 / -1;
}
display: flex;
justify-content: flex-end;
align-items: center;
gap: var(--space-medium);
// Helper class around the
element to position the FeatureHighlight message
.widgets-title-container-text {
display: inline-block;
margin-inline-end: auto;
position: relative;
}
.widgets-title-heading {
display: flex;
align-items: center;
gap: var(--space-medium);
}
.widgets-title-actions {
display: flex;
align-items: center;
gap: var(--space-medium);
position: relative;
}
}
h1 {
font-size: var(--font-size-large);
font-weight: var(--font-weight-bold);
// @nova-cleanup(remove-conditional): Make default, remove .nova-enabled selector
.nova-enabled & {
font-size: var(--font-size-heading-large);
font-weight: var(--font-weight-heading);
color: var(--newtab-contextual-text-primary-color);
@include at-content-cols(12) {
font-size: var(--font-size-heading-xlarge);
}
@include wallpaper-contrast-fix;
}
}
// @nova-cleanup(remove-conditional): Make default, remove .nova-enabled selector
#hide-all-widgets-button,
#toggle-widgets-size-button {
.nova-enabled & {
color: var(--newtab-contextual-text-primary-color);
@include wallpaper-contrast-fix;
}
}
}
.widgets-expand-button {
appearance: none;
align-items: center;
background-color: transparent;
border: 1px solid var(--newtab-primary-action-background);
border-radius: var(--border-radius-circle);
color: var(--newtab-primary-action-background);
cursor: pointer;
display: inline-flex;
height: var(--size-item-large);
justify-content: center;
margin: 0;
padding: 0;
width: var(--size-item-large);
&::before {
background-color: currentColor;
content: '';
height: var(--size-item-small);
mask: url('chrome://global/skin/icons/arrow-down.svg') center / contain no-repeat;
transition: transform 160ms ease;
width: var(--size-item-small);
}
&.is-maximized::before {
transform: rotate(180deg);
}
&:hover {
background-color: var(--newtab-element-secondary-hover-color);
}
&:active {
background-color: var(--newtab-element-secondary-active-color);
}
&:focus {
outline: none;
}
&:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-offset);
}
}
@media (prefers-reduced-motion: reduce) {
.widgets-expand-button::before {
transition: none;
}
}
.widgets-header-context-menu {
position: relative;
@include wallpaper-contrast-fix;
.widgets-header-context-menu-button {
&::part(button) {
background-color: transparent;
}
&::part(button):hover {
background-color: var(--button-background-color-hover);
}
&::part(button):active {
background-color: var(--button-background-color-active);
}
}
panel-list {
min-width: 256px;
}
panel-item::part(button) {
min-block-size: 30px;
}
}
// Classic layout padding (when Nova is disabled)
// @nova-cleanup(remove-conditional): Remove classic layout styles after Nova ships
.classic-enabled .widgets-section-container {
// Mirrors the grid-gap spacing used on
// .ds-outer-wrapper-breakpoint-override .ds-card-grid
@media(min-width: $break-point-widest) {
padding-block-end: var(--space-xlarge);
padding-inline: var(--space-xlarge);
}
// Sections Layout Override
.has-sections-grid & {
@media(min-width: $break-point-widest) {
padding-block-end: var(--space-large);
padding-inline: var(--space-large);
}
}
}
// Bug 1996308: This layout is somewhat brittle as the design dictates the parent container (.widgets-wrapper)
// be only as wide as the width of this container (.widgets-container) while still being responsive.
// Once we have four widgets, we can revert most of this logic as the container will be 100%.
.widgets-container {
// Nova layout
// @nova-cleanup(remove-conditional): Make Nova grid default, remove .nova-enabled selector
.nova-enabled & {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(auto-fill, var(--col-width));
grid-auto-rows: var(--row-height);
gap: var(--space-medium);
}
// Classic layout (when Nova is disabled)
// @nova-cleanup(remove-conditional): Remove classic fixed-width card layout after Nova ships
.classic-enabled & {
--widgets-card-width: var(--newtab-card-grid-layout-width);
// This is the maximum number of widgets the Widgets row can support
--widgets-max-cols: 3;
--widgets-grid-gap: var(--space-large);
--widgets-small-card-height: var(--newtab-card-height-small);
&:has(.lists),
&:has(.focus-timer),
&:has(.sports) {
display: grid;
grid-template-columns: repeat(auto-fit, var(--widgets-card-width));
gap: var(--widgets-grid-gap);
// The max-width logic is only necessary when displaying more than one column
@media(min-width: $break-point-medium) {
max-width: calc(var(--widgets-max-cols) * var(--widgets-card-width) + (var(--widgets-max-cols) - 1) * var(--widgets-grid-gap));
justify-content: center;
}
// Mirrors the grid-gap spacing used on
// .ds-outer-wrapper-breakpoint-override .ds-card-grid
@media(min-width: $break-point-widest) {
--widgets-grid-gap: var(--space-xlarge);
}
}
.is-small {
height: var(--widgets-small-card-height);
}
}
}
// Classic layout: Sections grid overrides
// @nova-cleanup(remove-conditional): Remove after Nova ships
.classic-enabled .has-sections-grid .widgets-container {
--widgets-card-width: var(--newtab-card-width-medium);
&:has(.lists),
&:has(.focus-timer),
&:has(.sports) {
@media(min-width: $break-point-widest) {
--widgets-grid-gap: var(--space-large);
}
}
}
// DnD visuals on the wrapper.
.nova-enabled .widget-draggable {
&.is-dragging {
opacity: 0.5;
}
}
.widgets-feedback-link {
display: inline-block;
margin-block-start: var(--space-medium);
font-size: var(--font-size-small);
}
.nova-enabled .widgets-row-toggle {
display: none;
grid-column: 1 / -1;
justify-self: center;
margin-block-start: var(--space-medium);
@include wallpaper-contrast-fix;
}
// Reveal the toggle when the layout would overflow at the current
// card-column count, and remove individual widgets from layout when
// they'd be hidden by collapse. JS publishes:
// - `data-overflow-N` on .widgets-section-container — drives toggle
// visibility.
// - `data-hidden-N` on each .widget-wrapper that would be clipped at
// that viewport — combined with `data-row-collapsed`, drives
// `display: none` so the container shrinks naturally (no clip
// needed) and the widget tabs out of focus / a11y order.
// One IAB card spans 4 grid columns, so the ranges below mirror 1-, 2-,
// 3-, and 4-card viewports.
.nova-enabled {
@include at-content-cols-range(0, 8) {
.widgets-section-container[data-overflow-1] .widgets-row-toggle {
display: block;
}
.widgets-container[data-row-collapsed] .widget-wrapper[data-hidden-1] {
display: none;
}
}
@include at-content-cols-range(8, 12) {
.widgets-section-container[data-overflow-2] .widgets-row-toggle {
display: block;
}
.widgets-container[data-row-collapsed] .widget-wrapper[data-hidden-2] {
display: none;
}
}
@include at-content-cols-range(12, 16) {
.widgets-section-container[data-overflow-3] .widgets-row-toggle {
display: block;
}
.widgets-container[data-row-collapsed] .widget-wrapper[data-hidden-3] {
display: none;
}
}
@include at-content-cols(16) {
.widgets-section-container[data-overflow-4] .widgets-row-toggle {
display: block;
}
.widgets-container[data-row-collapsed] .widget-wrapper[data-hidden-4] {
display: none;
}
}
}
.nova-enabled {
.widgets-container {
// Only reserve a grid slot for the placeholder when it can fit on row 1
// alongside the existing widgets (no wrap to a new row). Each widget is
// col-4 (4 grid cols), so N widgets + placeholder need 4*(N+1) cols of
// width. Child counts include the placeholder itself (rendered last in
// the DOM), so 2 children = 1 widget, 3 = 2 widgets, etc. Restricting to
// row 1 avoids edge cases with mixed-height widgets (e.g. a large widget
// occupying multiple medium row-slots in one column).
@include at-content-cols(8) {
&:has(> :nth-child(2)):not(:has(> :nth-child(3))) > .widgets-add-button {
display: flex;
}
}
@include at-content-cols(12) {
&:has(> :nth-child(3)):not(:has(> :nth-child(4))) > .widgets-add-button {
display: flex;
}
}
@include at-content-cols(16) {
&:has(> :nth-child(4)):not(:has(> :nth-child(5))) > .widgets-add-button {
display: flex;
}
}
}
.widgets-add-button {
display: none;
align-items: center;
justify-content: center;
padding: 0;
opacity: 0;
transition: opacity 0.2s ease;
background: var(--newtab-background-card);
border: 1px dashed var(--border-color);
border-radius: var(--border-radius-large);
color: var(
--icon-color-accent-primary-desaturated,
light-dark(var(--color-violet-desaturated-60), var(--color-violet-desaturated-50))
);
@media (prefers-reduced-motion: reduce) {
transition: none;
}
&.medium-widget {
grid-row: span 2;
}
&.large-widget {
grid-row: span 4;
}
&:hover,
&:focus-within {
opacity: 1;
background: var(--newtab-background-color-secondary);
outline: 1px solid var(--border-color-deemphasized);
box-shadow: var(--card-box-shadow-hover);
}
&:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-offset);
}
.widgets-add-button-icon {
display: inline-block;
width: var(--icon-size-large);
height: var(--icon-size-large);
background-color: currentColor;
mask: url('chrome://global/skin/icons/plus.svg') center / contain no-repeat;
}
}
}
.feature-highlight .feature-highlight-modal.widget-highlight-wrapper {
width: auto;
max-width: min-content;
border-radius: var(--border-radius-medium);
// Custom Positioning
&.inset-block-end {
margin-block-start: var(--space-large);
}
.content-wrapper {
margin: 0;
padding: var(--space-medium);
}
.widget-highlight {
img {
border-radius: var(--border-radius-medium);
margin-block-end: var(--space-small);
}
h3 {
margin-block: 0 var(--space-xsmall);
font-size: var(--font-size-root);
}
p {
margin-block: 0;
font-size: var(--font-size-root);
}
}
.message-icon {
display: none;
}
// The "X" dismiss button overlays the image in the message
moz-button {
position: absolute;
inset-inline-end: var(--space-medium);
inset-block-start: var(--space-medium);
// Custom contrast override
--button-icon-fill: var(--color-gray-0);
}
// If not image is not included, use the default icon color to maintain proper viz contrast
&.no-image {
min-width: 370px;
padding-block-start: var(--space-small);
moz-button {
--button-icon-fill: var(--icon-color);
}
}
}