diff --git a/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js b/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js index 80c516c43e..b222a6c4d2 100644 --- a/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js +++ b/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js @@ -36,6 +36,7 @@ import { ACTION_BURN } from '../burning/BurnProvider.js'; * @property {Record} favorites * @property {string[]} urls * @property {number} totalTrackers + * @property {Record} cookiePopUpBlocked */ /** @@ -52,6 +53,7 @@ export function normalizeData(prev, incoming) { trackingStatus: {}, urls: [], totalTrackers: incoming.totalTrackers, + cookiePopUpBlocked: {}, }; if (shallowDiffers(prev.urls, incoming.urls)) { @@ -64,6 +66,7 @@ export function normalizeData(prev, incoming) { const id = item.url; output.favorites[id] = item.favorite; + output.cookiePopUpBlocked[id] = item.cookiePopUpBlocked; /** @type {Item} */ const next = { @@ -73,6 +76,7 @@ export function normalizeData(prev, incoming) { faviconMax: item.favicon?.maxAvailableSize ?? DDG_DEFAULT_ICON_SIZE, favoriteSrc: item.favicon?.src, trackersFound: item.trackersFound, + // cookiePopUpBlocked: item.cookiePopUpBlocked, }; const differs = shallowDiffers(next, prev.items[id] || {}); output.items[id] = differs ? next : prev.items[id] || {}; @@ -187,6 +191,7 @@ export function SignalStateProvider({ children }) { favorites: {}, urls: [], totalTrackers: 0, + cookiePopUpBlocked: {}, }, { activity: state.data.activity, urls: state.data.urls, totalTrackers: state.data.totalTrackers }, ), diff --git a/special-pages/pages/new-tab/app/activity/activity.md b/special-pages/pages/new-tab/app/activity/activity.md index 366d6ddad1..70a6198759 100644 --- a/special-pages/pages/new-tab/app/activity/activity.md +++ b/special-pages/pages/new-tab/app/activity/activity.md @@ -48,7 +48,8 @@ title: Activity "url": "https://youtube.com/watch?v=abc", "relativeTime": "Just now" } - ] + ], + "cookiePopUpBlocked": true, } ] } @@ -178,4 +179,4 @@ example payload without id (for example, on history items) ``` ### `activity_burnAnimationComplete` -- Sent when the burn animation completes \ No newline at end of file +- Sent when the burn animation completes diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.examples.js b/special-pages/pages/new-tab/app/activity/components/Activity.examples.js index d2c4791524..323e782fdb 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.examples.js +++ b/special-pages/pages/new-tab/app/activity/components/Activity.examples.js @@ -16,7 +16,7 @@ export const activityExamples = { factory: () => ( - + ), @@ -25,7 +25,7 @@ export const activityExamples = { factory: () => ( - + ), @@ -34,7 +34,7 @@ export const activityExamples = { factory: () => ( - + ), @@ -58,6 +58,7 @@ function Mock({ children, size }) { favorites: {}, urls: [], totalTrackers: 0, + cookiePopUpBlocked: null, }, { activity: mocks, urls: mocks.map((x) => x.url), totalTrackers: 0 }, ); diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.js b/special-pages/pages/new-tab/app/activity/components/Activity.js index 060090dfd4..f346baa8d9 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.js +++ b/special-pages/pages/new-tab/app/activity/components/Activity.js @@ -1,5 +1,8 @@ import { h } from 'preact'; import styles from './Activity.module.css'; +// @todo legacyProtections: `stylesLegacy` can be removed once all platforms +// are ready for the new Protections Report +import stylesLegacy from './ActivityLegacy.module.css'; import { useContext, useEffect, useRef } from 'preact/hooks'; import { memo } from 'preact/compat'; import { ActivityContext, ActivityServiceContext } from '../ActivityProvider.js'; @@ -8,7 +11,7 @@ import { useOnMiddleClick } from '../../utils.js'; import { useAdBlocking, useBatchedActivityApi, usePlatformName } from '../../settings.provider.js'; import { CompanyIcon } from '../../components/CompanyIcon.js'; import { Trans } from '../../../../../shared/components/TranslationsProvider.js'; -import { ActivityItem } from './ActivityItem.js'; +import { ActivityItem, ActivityItemLegacy } from './ActivityItem.js'; import { ActivityBurningSignalContext, BurnProvider } from '../../burning/BurnProvider.js'; import { useEnv } from '../../../../../shared/components/EnvironmentProvider.js'; import { useComputed } from '@preact/signals'; @@ -18,6 +21,7 @@ import { HistoryItems } from './HistoryItems.js'; import { NormalizedDataContext, SignalStateProvider } from '../NormalizeDataProvider.js'; import { ActivityInteractionsContext } from '../../burning/ActivityInteractionsContext.js'; import { ProtectionsEmpty } from '../../protections/components/Protections.js'; +import { TickPill } from '../../components/TickPill/TickPill'; /** * @import enStrings from "../strings.json" @@ -55,8 +59,9 @@ export function ActivityEmptyState() { * @param {object} props * @param {boolean} props.canBurn * @param {DocumentVisibilityState} props.visibility + * @param {boolean} props.shouldDisplayLegacyActivity */ -export function ActivityBody({ canBurn, visibility }) { +export function ActivityBody({ canBurn, visibility, shouldDisplayLegacyActivity }) { const { isReducedMotion } = useEnv(); const { keys } = useContext(NormalizedDataContext); const { burning, exiting } = useContext(ActivityBurningSignalContext); @@ -71,8 +76,33 @@ export function ActivityBody({ canBurn, visibility }) { return ( ); @@ -111,16 +141,23 @@ const BurnableItem = memo( * @param {object} props * @param {string} props.id * @param {'visible' | 'hidden'} props.documentVisibility + * @param {boolean} props.shouldDisplayLegacyActivity */ - function BurnableItem({ id, documentVisibility }) { + function BurnableItem({ id, documentVisibility, shouldDisplayLegacyActivity }) { const { activity } = useContext(NormalizedDataContext); const item = useComputed(() => activity.value.items[id]); + if (!item.value) { return null; } + + // @todo legacyProtections: Once all platforms are ready for the new + // protections report we can use `ActivityItem` + const ActivityItemComponent = shouldDisplayLegacyActivity ? ActivityItemLegacy : ActivityItem; + return ( - - + {shouldDisplayLegacyActivity ? ( + // @todo legacyProtections: `TrackerStatusLegacy` and + // supporting prop can be removed once all platforms are + // ready for the new protections report + + ) : ( + + )} - + ); }, @@ -143,8 +187,9 @@ const RemovableItem = memo( * @param {string} props.id * @param {boolean} props.canBurn * @param {"visible" | "hidden"} props.documentVisibility + * @param {boolean} props.shouldDisplayLegacyActivity */ - function RemovableItem({ id, canBurn, documentVisibility }) { + function RemovableItem({ id, canBurn, documentVisibility, shouldDisplayLegacyActivity }) { const { activity } = useContext(NormalizedDataContext); const item = useComputed(() => activity.value.items[id]); if (!item.value) { @@ -154,8 +199,13 @@ const RemovableItem = memo(

); } + + // @todo legacyProtections: Once all platforms are ready for the new + // protections report we can use `ActivityItem` + const ActivityItemComponent = shouldDisplayLegacyActivity ? ActivityItemLegacy : ActivityItem; + return ( - - + {shouldDisplayLegacyActivity ? ( + // @todo legacyProtections: `TrackerStatusLegacy` and + // supporting prop can be removed once all platforms are + // ready for the new protections report + + ) : ( + + )} - + ); }, ); @@ -178,6 +235,40 @@ const DDG_MAX_TRACKER_ICONS = 3; * @param {boolean} props.trackersFound */ function TrackerStatus({ id, trackersFound }) { + const { t } = useTypedTranslationWith(/** @type {enStrings} */ ({})); + const { activity } = useContext(NormalizedDataContext); + const status = useComputed(() => activity.value.trackingStatus[id]); + const cookiePopUpBlocked = useComputed(() => activity.value.cookiePopUpBlocked?.[id]).value; + const { totalCount: totalTrackersBlocked } = status.value; + + const totalTrackersPillText = + totalTrackersBlocked === 0 + ? trackersFound + ? t('activity_no_trackers_blocked') + : t('activity_no_trackers') + : t(totalTrackersBlocked === 1 ? 'activity_countBlockedSingular' : 'activity_countBlockedPlural', { + count: String(totalTrackersBlocked), + }); + + return ( +
+
+ {totalTrackersBlocked > 0 && } + {cookiePopUpBlocked && } +
+
+ ); +} + +// @todo legacyProtections: `TrackerStatusLegacy` can be removed once all +// platforms are ready for the new protections report + +/** + * @param {object} props + * @param {string} props.id + * @param {boolean} props.trackersFound + */ +function TrackerStatusLegacy({ id, trackersFound }) { const { t } = useTypedTranslationWith(/** @type {enStrings} */ ({})); const { activity } = useContext(NormalizedDataContext); const status = useComputed(() => activity.value.trackingStatus[id]); @@ -193,7 +284,7 @@ function TrackerStatus({ id, trackersFound }) { if (other.length > 0) { const title = other.map((item) => item.displayName).join('\n'); otherIcon = ( - + +{other.length} ); @@ -207,23 +298,23 @@ function TrackerStatus({ id, trackersFound }) { text = adBlocking ? t('activity_no_adsAndTrackers') : t('activity_no_trackers'); } return ( -

+

{text}

); } return ( -
-
+
+
{icons} {otherIcon}
-
+
{adBlocking ? ( ) : ( - + )}
@@ -263,8 +354,9 @@ export function ActivityConfigured({ children }) { * ``` * @param {object} props * @param {boolean} props.showBurnAnimation + * @param {boolean} props.shouldDisplayLegacyActivity */ -export function ActivityConsumer({ showBurnAnimation }) { +export function ActivityConsumer({ showBurnAnimation, shouldDisplayLegacyActivity }) { const { state } = useContext(ActivityContext); const service = useContext(ActivityServiceContext); const platformName = usePlatformName(); @@ -274,7 +366,7 @@ export function ActivityConsumer({ showBurnAnimation }) { return ( - + ); @@ -283,7 +375,7 @@ export function ActivityConsumer({ showBurnAnimation }) { - + diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.module.css b/special-pages/pages/new-tab/app/activity/components/Activity.module.css index efe08990e0..ea2336cc26 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.module.css +++ b/special-pages/pages/new-tab/app/activity/components/Activity.module.css @@ -77,9 +77,7 @@ background: var(--color-black-at-12); transition: transform .2s; - border: 0.5px solid rgba(0, 0, 0, 0.09); background: rgba(255, 255, 255, 0.30); - box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.12), 0px 0px 1.5px 0px rgba(0, 0, 0, 0.16); [data-theme="dark"] & { border: 0.5px solid rgba(255, 255, 255, 0.09); @@ -198,18 +196,14 @@ padding-left: 1px; /* visual alignment */ } -.companiesIcons { - display: flex; - gap: 3px; - > * { - flex-shrink: 0; - min-width: 0; - } +.companiesText { + & div:first-of-type:not(:only-child) { + margin-bottom: 6px; + } } -.companiesText {} .history { - margin-top: 10px; + margin-top: 8px; } .historyItem { display: flex; @@ -293,4 +287,4 @@ transform: rotate(180deg) } } -} \ No newline at end of file +} diff --git a/special-pages/pages/new-tab/app/activity/components/ActivityItem.js b/special-pages/pages/new-tab/app/activity/components/ActivityItem.js index 34ba648d16..ec4a4f9840 100644 --- a/special-pages/pages/new-tab/app/activity/components/ActivityItem.js +++ b/special-pages/pages/new-tab/app/activity/components/ActivityItem.js @@ -2,11 +2,12 @@ import { h } from 'preact'; import { useTypedTranslationWith } from '../../types.js'; import cn from 'classnames'; import styles from './Activity.module.css'; +import stylesLegacy from './ActivityLegacy.module.css'; import { FaviconWithState } from '../../../../../shared/components/FaviconWithState.js'; import { ACTION_ADD_FAVORITE, ACTION_REMOVE, ACTION_REMOVE_FAVORITE } from '../constants.js'; import { Star, StarFilled } from '../../components/icons/Star.js'; import { Fire } from '../../components/icons/Fire.js'; -import { Cross } from '../../components/Icons.js'; +import { Cross, FireIcon } from '../../components/Icons.js'; import { useContext } from 'preact/hooks'; import { memo } from 'preact/compat'; import { useComputed } from '@preact/signals'; @@ -55,6 +56,47 @@ export const ActivityItem = memo( }, ); +export const ActivityItemLegacy = memo( + /** + * @param {object} props + * @param {boolean} props.canBurn + * @param {"visible"|"hidden"} props.documentVisibility + * @param {import("preact").ComponentChild} props.children + * @param {string} props.title + * @param {string} props.url + * @param {string|null|undefined} props.favoriteSrc + * @param {number} props.faviconMax + * @param {string} props.etldPlusOne + */ + function ActivityItem({ canBurn, documentVisibility, title, url, favoriteSrc, faviconMax, etldPlusOne, children }) { + return ( +
  • + +
    {children}
    +
  • + ); + }, +); + /** * Renders a set of control buttons that handle actions related to favorites and burn/removal features. * @@ -97,7 +139,7 @@ function Controls({ canBurn, url, title }) { value={url} type="button" > - {canBurn ? : } + {canBurn ? : }
    ); diff --git a/special-pages/pages/new-tab/app/activity/components/ActivityLegacy.module.css b/special-pages/pages/new-tab/app/activity/components/ActivityLegacy.module.css new file mode 100644 index 0000000000..efe08990e0 --- /dev/null +++ b/special-pages/pages/new-tab/app/activity/components/ActivityLegacy.module.css @@ -0,0 +1,296 @@ +.root { + display: grid; +} + +.activity { + --favicon-width: 32px; + --heading-gap: 8px; + + + overflow: hidden; + width: calc(100% + 12px); + margin-left: -6px; + + &:not(:empty) { + margin-top: 24px; + } +} + +.block { + margin-top: 24px; +} + +.loader { + height: 10px; + border: 1px dotted black; + border-radius: 5px; + opacity: 0; +} + +.anim { + position: relative; + overflow: hidden; + border-radius: var(--border-radius-lg); + + [data-lottie-player] { + width: 100%; + height: auto; + object-fit: cover; + position: absolute; + bottom: -50%; + left: 0; + } +} + +.item { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 6px; + padding-right: 6px; +} + +.burning { + > *:not([data-lottie-player]) { + transition: opacity .2s; + transition-delay: .3s; + opacity: 0; + } +} + +.heading { + display: flex; + gap: var(--heading-gap); + width: 100%; +} + +.favicon { + width: 32px; + height: 32px; + /* adding a margin to prevent needing an extra dom node for spacing */ + margin: 3px; + display: block; + backdrop-filter: blur(24px); + border-radius: var(--border-radius-sm); + flex-shrink: 0; + text-decoration: none; + position: relative; + background: var(--color-black-at-12); + transition: transform .2s; + + border: 0.5px solid rgba(0, 0, 0, 0.09); + background: rgba(255, 255, 255, 0.30); + box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.12), 0px 0px 1.5px 0px rgba(0, 0, 0, 0.16); + + [data-theme="dark"] & { + border: 0.5px solid rgba(255, 255, 255, 0.09); + background: rgba(0, 0, 0, 0.18); + box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.12), 0px 0px 1.5px 0px rgba(0, 0, 0, 0.16); + backdrop-filter: blur(24px); + } + + > *:first-child { + position: absolute; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + } +} + +.title { + font-size: var(--title-3-em-font-size); + font-weight: var(--title-3-em-font-weight); + text-decoration: none; + color: var(--ntp-text-normal); + height: 35px; + display: flex; + align-items: center; + line-height: 1; + + /* Note: This is not a 1:1 value from figma, I reduced it for perfect visual alignment */ + gap: 4px; + min-width: 0; + + &:hover, &:focus-visible { + color: var(--ntp-color-primary); + .favicon { + transform: scale(1.08) + } + } +} + +.controls { + display: flex; + margin-left: auto; + flex-shrink: 0; + position: relative; + gap: 4px; + top: 4px; +} + +.icon { + width: 24px; + height: 24px; + position: relative; + border: none; + background: transparent; + padding: 0; + margin: 0; + color: var(--ntp-text-normal); + svg { + position: absolute; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + } +} + +.controlIcon { + border-radius: 50%; + background-color: var(--color-black-at-3); + &:hover { + background-color: var(--color-black-at-6); + } + + [data-theme="dark"] & { + background-color: var(--color-white-at-6); + } + [data-theme="dark"] &:hover { + background-color: var(--color-white-at-9); + } + svg { + fill-opacity: 0.6; + } +} + +.disableWhenBusy { + [data-busy="true"] & { + cursor: not-allowed; + } +} + +.body { + padding-left: calc(var(--favicon-width) + var(--heading-gap)); + padding-right: calc(var(--favicon-width) + var(--heading-gap) * 2); + position: relative; +} + +.otherIcon { + width: 16px; + height: 16px; + border-radius: 50%; + font-weight: bold; + font-size: 0.5rem; + line-height: 16px; + color: var(--color-black-at-60); + background: var(--color-black-at-6); + text-align: center; + + [data-theme="dark"] & { + color: var(--color-white-at-50); + background: var(--color-white-at-9); + } +} + +.companiesIconRow { + display: flex; + align-items: center; + gap: 6px; + padding-left: 1px; /* visual alignment */ +} + +.companiesIcons { + display: flex; + gap: 3px; + > * { + flex-shrink: 0; + min-width: 0; + } +} +.companiesText {} + +.history { + margin-top: 10px; +} +.historyItem { + display: flex; + align-items: center; + width: 100%; + height: 16px; + + .historyItem { + margin-top: 5px; + } +} +.historyLink { + font-size: var(--small-label-font-size); + font-weight: var(--small-label-font-weight); + line-height: var(--small-label-line-height); + color: var(--ntp-text-normal); + text-decoration: none; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:hover, &:focus-visible { + color: var(--ntp-color-primary) + } + + &:hover .time { + text-decoration: none; + display: inline-block; + } +} + +.time { + flex-shrink: 0; + margin-left: 8px; + color: var(--ntp-text-muted); + opacity: 0.6; + font-size: var(--small-label-font-size); + font-weight: var(--small-label-font-weight); + line-height: var(--small-label-line-height); +} + +.historyBtn { + width: 16px; + height: 16px; + flex-shrink: 0; + border: 0; + border-radius: 4px; + position: relative; + text-align: center; + padding: 0; + margin: 0; + margin-left: 8px; + background: transparent; + display: flex; + align-items: center; + justify-content: center; + color: var(--color-black-at-60); + + &:hover { + background-color: var(--color-black-at-6); + } + + [data-theme="dark"] & { + color: var(--color-white-at-60); + &:hover { + background-color: var(--color-white-at-6); + } + } + + svg { + display: inline-block; + width: 16px; + height: 16px; + position: relative; + top: 1px; + transform: rotate(0); + } + + &[data-action="hide"] { + svg { + transform: rotate(180deg) + } + } +} \ No newline at end of file diff --git a/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js b/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js index 2daa6d595b..1442c0ba6e 100644 --- a/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js +++ b/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js @@ -441,4 +441,22 @@ export class ActivityPage { - paragraph: Past 7 days `); } + + /** + * Test that cookie popup blocked indicator is shown for items with cookiePopUpBlocked: true + */ + async showsCookiePopupBlockedIndicator() { + // First item in 'few' mock has cookiePopUpBlocked: true + const firstItem = this.context().getByTestId('ActivityItem').nth(0); + await expect(firstItem.getByText(/cookie pop-up/i)).toBeVisible(); + } + + /** + * Test that cookie popup blocked indicator is NOT shown for items with cookiePopUpBlocked: false + */ + async hidesCookiePopupIndicatorWhenNotBlocked() { + // Second item in 'few' mock (youtube) has cookiePopUpBlocked: false + const secondItem = this.context().getByTestId('ActivityItem').nth(1); + await expect(secondItem.getByText(/cookie pop-up/i)).not.toBeVisible(); + } } diff --git a/special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js b/special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js index f4aad82f5b..6faca16187 100644 --- a/special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js +++ b/special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js @@ -100,6 +100,22 @@ test.describe('activity widget', () => { await ap.didRender(); await ap.showsAdsAndTrackersTrackerStates(); }); + test('shows cookie popup blocked indicator', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + const ap = new ActivityPage(page, ntp); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { ...defaultPageParams } }); + await ap.didRender(); + await ap.showsCookiePopupBlockedIndicator(); + }); + test('hides cookie popup indicator when not blocked', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + const ap = new ActivityPage(page, ntp); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { ...defaultPageParams } }); + await ap.didRender(); + await ap.hidesCookiePopupIndicatorWhenNotBlocked(); + }); test('after rendering and navigating to a new tab, data is re-requested on return', async ({ page }, workerInfo) => { const ntp = NewtabPage.create(page, workerInfo); const ap = new ActivityPage(page, ntp); diff --git a/special-pages/pages/new-tab/app/activity/mocks/activity.mock-transport.js b/special-pages/pages/new-tab/app/activity/mocks/activity.mock-transport.js index 2bf90c018f..b09ee22f2c 100644 --- a/special-pages/pages/new-tab/app/activity/mocks/activity.mock-transport.js +++ b/special-pages/pages/new-tab/app/activity/mocks/activity.mock-transport.js @@ -87,6 +87,7 @@ export function activityMockTransport() { trackersFound: false, trackingStatus: { trackerCompanies: [], totalCount: 0 }, title: 'example.com', + cookiePopUpBlocked: true, }); count += 1; console.log('sent', dataset); @@ -339,6 +340,7 @@ export function generateSampleData(count) { trackerCompanies, totalCount: trackerCompanies.length === 0 ? 0 : Math.round(trackerCompanies.length * 1.5), }, + cookiePopUpBlocked: true, }); } return data; diff --git a/special-pages/pages/new-tab/app/activity/mocks/activity.mocks.js b/special-pages/pages/new-tab/app/activity/mocks/activity.mocks.js index 27f41cec49..f164be54ac 100644 --- a/special-pages/pages/new-tab/app/activity/mocks/activity.mocks.js +++ b/special-pages/pages/new-tab/app/activity/mocks/activity.mocks.js @@ -77,6 +77,7 @@ export const activityMocks = { totalCount: 56, }, history: [], + cookiePopUpBlocked: true, }, ], }, @@ -115,6 +116,7 @@ export const activityMocks = { relativeTime: '1 day ago', }, ], + cookiePopUpBlocked: true, }, { favicon: { src: 'youtube-icon.png' }, @@ -139,6 +141,7 @@ export const activityMocks = { relativeTime: '3 days ago', }, ], + cookiePopUpBlocked: false, }, { favicon: { src: 'amazon-icon.png' }, @@ -158,6 +161,7 @@ export const activityMocks = { relativeTime: '1 day ago', }, ], + cookiePopUpBlocked: true, }, { favicon: { src: 'twitter-icon.png' }, @@ -177,6 +181,7 @@ export const activityMocks = { relativeTime: '2 days ago', }, ], + cookiePopUpBlocked: true, }, { favicon: { src: 'linkedin-icon.png' }, @@ -196,6 +201,7 @@ export const activityMocks = { relativeTime: '2 hrs ago', }, ], + cookiePopUpBlocked: false, }, ], }, diff --git a/special-pages/pages/new-tab/app/activity/strings.json b/special-pages/pages/new-tab/app/activity/strings.json index c8a78a5d81..b789f047f1 100644 --- a/special-pages/pages/new-tab/app/activity/strings.json +++ b/special-pages/pages/new-tab/app/activity/strings.json @@ -16,9 +16,21 @@ "note": "Placeholder message indicating that no trackers are blocked" }, "activity_countBlockedPlural": { + "title": "{count} Tracking attempts blocked", + "note": "Pill text indicating that more than 1 attempt has been blocked. Eg: '2 Tracking attempts blocked'" + }, + "activity_countBlockedPluralLegacy": { "title": "{count} tracking attempts blocked", "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" }, + "activity_countBlockedSingular": { + "title": "{count} Tracking attempt blocked", + "note": "Pill text indicating that 1 attempt has been blocked. Eg: '1 Tracking attempt blocked'" + }, + "activity_cookiePopUpBlocked": { + "title": "Cookie pop-up blocked", + "note": "Pill text indicating that we have blocked cookie pop-ups" + }, "activity_noRecentAdsAndTrackers_subtitle": { "title": "Recently visited sites will appear here. Keep browsing to see how many ads and trackers we block.", "note": "Shown in the place a list of browsing history entries will be displayed." diff --git a/special-pages/pages/new-tab/app/components/Icons.js b/special-pages/pages/new-tab/app/components/Icons.js index 791d1bd889..e0aab7291a 100644 --- a/special-pages/pages/new-tab/app/components/Icons.js +++ b/special-pages/pages/new-tab/app/components/Icons.js @@ -601,3 +601,85 @@ export function CloseSmallIcon(props) { ); } + +/** + * @param {import('preact').JSX.SVGAttributes} props + */ +export function NewBadgeIcon(props) { + return ( + + + + + + + ); +} + +/** + * @param {import('preact').JSX.SVGAttributes} props + */ +export function InfoIcon(props) { + return ( + + + + + + ); +} + +/** + * From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Check-16.svg + * @param {import('preact').JSX.SVGAttributes} props + */ +export function Check(props) { + return ( + + + + ); +} + +/** + * From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Fire-Solid-16.svg + * @param {import('preact').JSX.SVGAttributes} props + */ +export function FireIcon(props) { + return ( + + + + ); +} diff --git a/special-pages/pages/new-tab/app/components/Icons.module.css b/special-pages/pages/new-tab/app/components/Icons.module.css index 1bbb2bb812..8989bac0ca 100644 --- a/special-pages/pages/new-tab/app/components/Icons.module.css +++ b/special-pages/pages/new-tab/app/components/Icons.module.css @@ -18,4 +18,18 @@ } } +/* InfoIcon styles */ +:global(.info-icon-fill) { + fill: black; + fill-opacity: 0.36; +} + +[data-theme=dark] :global(.info-icon-fill) { + fill: white; + fill-opacity: 0.24; +} +/* FireIcon styles */ +.fireIcon { + color: currentColor; +} diff --git a/special-pages/pages/new-tab/app/components/TickPill/TickPill.js b/special-pages/pages/new-tab/app/components/TickPill/TickPill.js new file mode 100644 index 0000000000..39e49e8d70 --- /dev/null +++ b/special-pages/pages/new-tab/app/components/TickPill/TickPill.js @@ -0,0 +1,24 @@ +import { h } from 'preact'; +import { Check } from '../Icons.js'; +import cn from 'classnames'; +import styles from './TickPill.module.css'; + +/** + * A pill-shaped component displaying a checkmark with text + * @param {Object} props + * @param {string} props.text - The text to display next to the checkmark + * @param {string} [props.className] - Additional CSS classes + * @param {boolean} [props.displayTick] - Display the tick or not + */ +export function TickPill({ text, className, displayTick = true }) { + return ( +
    + {displayTick && ( + + + + )} + {text} +
    + ); +} diff --git a/special-pages/pages/new-tab/app/components/TickPill/TickPill.module.css b/special-pages/pages/new-tab/app/components/TickPill/TickPill.module.css new file mode 100644 index 0000000000..1ef1dd7490 --- /dev/null +++ b/special-pages/pages/new-tab/app/components/TickPill/TickPill.module.css @@ -0,0 +1,45 @@ +.tickPill { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 10px; + border-radius: 100px; + background-color: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.12); + height: 20px; + width: fit-content; +} + +.iconWrapper { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.iconWrapper svg { + width: 12px; + height: 12px; +} + +.text { + font-size: 11px; + font-weight: 400; + line-height: 16px; + color: rgba(255, 255, 255, 0.84); + white-space: nowrap; +} + +/* Light mode styles */ +[data-theme="light"] .tickPill { + background-color: rgba(0, 0, 0, 0.04); + border: 1px solid rgba(0, 0, 0, 0.12); +} + +[data-theme="light"] .text { + color: rgba(0, 0, 0, 0.84); +} + +[data-theme="light"] .iconWrapper svg path { + fill: rgba(0, 0, 0, 0.84); +} diff --git a/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js new file mode 100644 index 0000000000..c6f684d11b --- /dev/null +++ b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js @@ -0,0 +1,26 @@ +import { h } from 'preact'; +import { useState } from 'preact/hooks'; +import styles from './Tooltip.module.css'; +import cn from 'classnames'; + +/** + * A tooltip component that appears on hover + * @param {Object} props + * @param {import('preact').ComponentChildren} props.children - The element that triggers the tooltip + * @param {string} props.content - The tooltip content text + * @param {string} [props.className] - Additional CSS classes for the trigger element + */ +export function Tooltip({ children, content, className }) { + const [isVisible, setIsVisible] = useState(false); + + return ( +
    setIsVisible(true)} + onMouseLeave={() => setIsVisible(false)} + > + {children} + {isVisible && + ); +} diff --git a/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.module.css b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.module.css new file mode 100644 index 0000000000..bb3c136461 --- /dev/null +++ b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.module.css @@ -0,0 +1,44 @@ +.tooltipContainer { + position: relative; + display: inline-flex; + align-items: center; +} + +.tooltip { + position: absolute; + bottom: -20px; + left: calc(100% + 8px); + padding: 8px 16px; + border-radius: 12px; + background-color: rgba(255, 255, 255, 0.98); + box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.20); + font-size: 12px; + line-height: 15px; + color: var(--color-black-at-96); + white-space: normal; + width: 236px; + z-index: 1000; + animation: tooltipFadeIn 300ms ease; + + & span { + display: block; + margin-top: 22px; + } +} + +/* Dark mode styles */ +[data-theme="dark"] .tooltip { + background-color: rgb(71, 71, 71); + color: var(--color-white-at-96); +} + +@keyframes tooltipFadeIn { + from { + opacity: 0; + transform: translateX(-10px); + } + to { + opacity: 1; + transform: translateX(0); + } +} diff --git a/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css index de482e1342..3626b1c34d 100644 --- a/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css +++ b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css @@ -2,9 +2,13 @@ display: flex; align-items: center; height: 24px; - margin-bottom: 16px; + margin-bottom: 36px; position: relative; - gap: 8px; + gap: 2px; + + &.noTrackers { + margin-bottom: 12px; + } } .headingIcon { @@ -13,7 +17,7 @@ position: relative; display: flex; align-items: center; - justify-content: center; + justify-content: left; padding-top: 0.5px; img { @@ -26,13 +30,25 @@ font-size: var(--title-3-em-font-size); font-weight: var(--title-3-em-font-weight); line-height: var(--title-3-em-line-height); - flex: 1; + flex: 0 0 auto; + margin-right: 6px; +} + +.infoIcon { + width: 16px; + height: 16px; + display: inline-block; + vertical-align: middle; } .widgetExpander { position: relative; + flex: 1; & [aria-controls] { + background-color: var(--ntp-widget-expander-bg); + width: 24px; + height: 24px; position: absolute; top: 50%; transform: translateY(-50%); @@ -46,17 +62,31 @@ } } +.counterContainer { + display: flex; + gap: 24px; +} + .counter { display: flex; flex-direction: column; gap: 4px; + padding-right: 38px; } .title { + color: var(--ntp-text-muted); grid-area: title; - font-size: var(--title-2-font-size); - font-weight: var(--title-2-font-weight); - line-height: var(--title-2-line-height); + font-size: var(--title-3-em-font-size); + font-weight: 400; + line-height: 28px; + + & span { + color: var(--ntp-text-primary); + display: block; + font-size: 40px; + padding-bottom: 6px; + } } .subtitle { diff --git a/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStatsLegacy.module.css b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStatsLegacy.module.css new file mode 100644 index 0000000000..de482e1342 --- /dev/null +++ b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStatsLegacy.module.css @@ -0,0 +1,168 @@ +.control { + display: flex; + align-items: center; + height: 24px; + margin-bottom: 16px; + position: relative; + gap: 8px; +} + +.headingIcon { + width: 24px; + height: 24px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding-top: 0.5px; + + img { + max-width: 1rem; + max-height: 1rem; + } +} + +.caption { + font-size: var(--title-3-em-font-size); + font-weight: var(--title-3-em-font-weight); + line-height: var(--title-3-em-line-height); + flex: 1; +} + +.widgetExpander { + position: relative; + + & [aria-controls] { + position: absolute; + top: 50%; + transform: translateY(-50%); + opacity: 1; + /** + * NOTE: This is just for visual alignment. The grid in which this sits is correct, + * but to preserve the larger tap-area for the button, we're opting to shift this over + * manually to solve this specific layout case. + */ + right: -4px + } +} + +.counter { + display: flex; + flex-direction: column; + gap: 4px; +} + +.title { + grid-area: title; + font-size: var(--title-2-font-size); + font-weight: var(--title-2-font-weight); + line-height: var(--title-2-line-height); +} + +.subtitle { + grid-area: label; + color: var(--ntp-text-muted); + line-height: var(--body-line-height); + text-transform: uppercase; + + &.indented { + padding-left: 2px; + } +} + +.body { + display: grid; + grid-row-gap: var(--sp-3); + +} + +.list { + display: grid; + grid-template-columns: auto; + grid-row-gap: calc(6 * var(--px-in-rem)); + transition: opacity ease-in-out 0.3s, visibility ease-in-out 0.3s; + + &:not(:empty) { + margin-top: 24px; + } +} + +.row { + min-height: 2rem; + display: grid; + grid-gap: var(--sp-2); + grid-template-columns: auto auto 40%; + grid-template-areas: 'company count bar'; + align-items: center; + + @media screen and (min-width: 500px) { + grid-template-columns: 35% 10% calc(55% - 1rem); /* - 1rem accounts for the grid gaps */ + } +} + +.listFooter { + display: flex; + .otherTrackersRow + .listExpander { + margin-left: auto; + } +} + +.otherTrackersRow { + padding-left: var(--sp-1); + color: var(--ntp-text-muted); + display: flex; + align-items: center; + +} + +.company { + grid-area: company; + display: flex; + align-items: center; + gap: var(--sp-2); + padding-left: var(--sp-1); + overflow: hidden; +} + +.name { + font-size: var(--title-3-em-font-size); + font-weight: var(--title-3-em-font-weight); + line-height: var(--title-3-em-line-height); + text-overflow: ellipsis; + display: block; + overflow: hidden; + white-space: nowrap; + position: relative; + top: -1px; +} + +.count { + grid-area: count; + text-align: right; + color: var(--ntp-text-normal); + line-height: 1; +} + +.bar { + grid-area: bar; + width: 100%; + height: 1rem; + border-radius: calc(20 * var(--px-in-rem)); + + background: var(--color-black-at-3); + + [data-theme=dark] & { + background: var(--color-white-at-6); + } +} + +.fill { + grid-area: bar; + height: 1rem; + border-radius: calc(20 * var(--px-in-rem)); + background: var(--color-black-at-6); + + [data-theme=dark] & { + background: var(--color-white-at-9); + } +} diff --git a/special-pages/pages/new-tab/app/privacy-stats/strings.json b/special-pages/pages/new-tab/app/privacy-stats/strings.json index 2b5ed4ba65..01dfb479e6 100644 --- a/special-pages/pages/new-tab/app/privacy-stats/strings.json +++ b/special-pages/pages/new-tab/app/privacy-stats/strings.json @@ -16,10 +16,18 @@ "note": "Placeholder to indicate that no tracking activity was blocked in the last 7 days" }, "stats_countBlockedSingular": { + "title": "Tracking attempt blocked", + "note": "The main headline indicating that a single tracker was blocked" + }, + "stats_countBlockedSingularLegacy": { "title": "1 tracking attempt blocked", "note": "The main headline indicating that a single tracker was blocked" }, "stats_countBlockedPlural": { + "title": "Tracking attempts blocked", + "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" + }, + "stats_countBlockedPluralLegacy": { "title": "{count} tracking attempts blocked", "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" }, @@ -32,13 +40,33 @@ "note": "Placeholder to indicate that no ads or tracking activity was blocked in the last 7 days" }, "stats_countBlockedAdsAndTrackersSingular": { + "title": "advertising & tracking attempt blocked", + "note": "The main headline indicating that a single ad or tracking attempt was blocked" + }, + "stats_countBlockedAdsAndTrackersSingularLegacy": { "title": "1 advertising & tracking attempt blocked", "note": "The main headline indicating that a single ad or tracking attempt was blocked" }, "stats_countBlockedAdsAndTrackersPlural": { + "title": "advertising & tracking attempts blocked", + "note": "The main headline indicating that more than 1 ad or tracking attempt has been blocked. Eg: '2 advertising & tracking attempts blocked" + }, + "stats_countBlockedAdsAndTrackersPluralLegacy": { "title": "{count} advertising & tracking attempts blocked", "note": "The main headline indicating that more than 1 ad or tracking attempt has been blocked. Eg: '2 advertising & tracking attempts blocked" }, + "stats_totalCookiePopUpsBlockedSingular": { + "title": "Cookie pop-up blocked", + "note": "The heading indicating that a single cookie pop-up was handled by the CPM" + }, + "stats_totalCookiePopUpsBlockedPlural": { + "title": "Cookie pop-ups blocked", + "note": "The heading indicating multiple cookie pop-ups were handled by the CPM" + }, + "stats_protectionsReportInfo": { + "title": "Displays tracking attempts blocked in the last 7 days and the number of cookie pop-ups blocked since you started using the browser. You can reset these stats using the Fire Button.", + "note": "Text explaining how to reset the protections stats" + }, "stats_feedCountBlockedSingular": { "title": "1 attempt blocked by DuckDuckGo in the last 7 days", "note": "A summary description of how many tracking attempts where blocked, when only one exists." diff --git a/special-pages/pages/new-tab/app/protections/components/Protections.js b/special-pages/pages/new-tab/app/protections/components/Protections.js index 32e5c584c6..038b40b804 100644 --- a/special-pages/pages/new-tab/app/protections/components/Protections.js +++ b/special-pages/pages/new-tab/app/protections/components/Protections.js @@ -4,6 +4,7 @@ import cn from 'classnames'; import styles from './Protections.module.css'; import { ProtectionsHeading } from './ProtectionsHeading.js'; import { useTypedTranslationWith } from '../../types.js'; +import { ProtectionsHeadingLegacy } from './ProtectionsHeadingLegacy'; /** * @import enStrings from "../strings.json" @@ -22,10 +23,20 @@ import { useTypedTranslationWith } from '../../types.js'; * @param {(feed: ProtectionsConfig['feed']) => void} props.setFeed * @param {import("preact").ComponentChild} [props.children] * @param {()=>void} props.toggle + * @param {import("@preact/signals").Signal} props.totalCookiePopUpsBlockedSignal */ -export function Protections({ expansion = 'expanded', children, blockedCountSignal, feed, toggle, setFeed }) { +export function Protections({ + expansion = 'expanded', + children, + blockedCountSignal, + feed, + toggle, + setFeed, + totalCookiePopUpsBlockedSignal, +}) { const WIDGET_ID = useId(); const TOGGLE_ID = useId(); + const totalCookiePopUpsBlocked = totalCookiePopUpsBlockedSignal.value; const attrs = useMemo(() => { return { @@ -36,13 +47,27 @@ export function Protections({ expansion = 'expanded', children, blockedCountSign return (
    - + {/* If `totalCookiePopUpsBlocked` is `undefined`, it means the + native side is not sending this property and we can assume it's not + yet been implemented */} + {totalCookiePopUpsBlocked === undefined ? ( + + ) : ( + + )} {children} diff --git a/special-pages/pages/new-tab/app/protections/components/Protections.module.css b/special-pages/pages/new-tab/app/protections/components/Protections.module.css index 6e6ff274d2..e7d90a4242 100644 --- a/special-pages/pages/new-tab/app/protections/components/Protections.module.css +++ b/special-pages/pages/new-tab/app/protections/components/Protections.module.css @@ -44,7 +44,7 @@ } .block { - margin-top: 24px; + margin-top: 32px; } .empty { color: var(--ntp-text-muted); diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsConsumer.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsConsumer.js index 07255bebdd..e7f0e16702 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsConsumer.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsConsumer.js @@ -1,5 +1,5 @@ import { useContext } from 'preact/hooks'; -import { ProtectionsContext, useBlockedCount } from './ProtectionsProvider.js'; +import { ProtectionsContext, useBlockedCount, useCookiePopUpsBlockedCount } from './ProtectionsProvider.js'; import { h } from 'preact'; import { Protections } from './Protections.js'; import { ActivityProvider } from '../../activity/ActivityProvider.js'; @@ -40,6 +40,8 @@ export function ProtectionsConsumer() { function ProtectionsReadyState({ data, config }) { const { toggle, setFeed } = useContext(ProtectionsContext); const blockedCountSignal = useBlockedCount(data.totalCount); + const totalCookiePopUpsBlockedSignal = useCookiePopUpsBlockedCount(data.totalCookiePopUpsBlocked); + return ( {config.feed === 'activity' && ( - + )} {config.feed === 'privacy-stats' && ( diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.examples.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.examples.js index d0cfb21218..93c0aeef71 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.examples.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.examples.js @@ -21,7 +21,16 @@ export const protectionsHeadingExamples = {
    - {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => { + {( + /** @type {Mock} */ { + expansion, + feed, + setFeed, + blockedCountSignal, + toggle, + totalCookiePopUpsBlockedSignal, + }, + ) => { return ( @@ -38,7 +48,16 @@ export const protectionsHeadingExamples = { }} - {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => { + {( + /** @type {Mock} */ { + expansion, + feed, + setFeed, + blockedCountSignal, + toggle, + totalCookiePopUpsBlockedSignal, + }, + ) => { return ( @@ -59,7 +79,16 @@ export const protectionsHeadingExamples = {
    - {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => { + {( + /** @type {Mock} */ { + expansion, + feed, + setFeed, + blockedCountSignal, + toggle, + totalCookiePopUpsBlockedSignal, + }, + ) => { return ( @@ -74,7 +104,16 @@ export const protectionsHeadingExamples = { }} - {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => { + {( + /** @type {Mock} */ { + expansion, + feed, + setFeed, + blockedCountSignal, + toggle, + totalCookiePopUpsBlockedSignal, + }, + ) => { return ( @@ -89,7 +129,16 @@ export const protectionsHeadingExamples = { }} - {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => { + {( + /** @type {Mock} */ { + expansion, + feed, + setFeed, + blockedCountSignal, + toggle, + totalCookiePopUpsBlockedSignal, + }, + ) => { return ( @@ -104,7 +154,16 @@ export const protectionsHeadingExamples = { }} - {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => { + {( + /** @type {Mock} */ { + expansion, + feed, + setFeed, + blockedCountSignal, + toggle, + totalCookiePopUpsBlockedSignal, + }, + ) => { return ( @@ -119,7 +179,16 @@ export const protectionsHeadingExamples = { }} - {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => { + {( + /** @type {Mock} */ { + expansion, + feed, + setFeed, + blockedCountSignal, + toggle, + totalCookiePopUpsBlockedSignal, + }, + ) => { return ( @@ -162,6 +232,7 @@ function PrintState(props) { * @property {() => void} toggle * @property {import('../../../types/new-tab.js').Expansion} expansion * @property {import('@preact/signals').Signal} blockedCountSignal + * @property {import('@preact/signals').Signal} totalCookiePopUpsBlockedSignal * @property {FeedType} feed * @property {(f: FeedType) => void} setFeed * @@ -175,6 +246,7 @@ const MockWithState = ({ children, initial = 0, feedType = 'privacy-stats', inte const [feed, setFeed] = useState(feedType); const [expansion, setExpansion] = useState(/** @type {import('../../../types/new-tab.js').Expansion} */ ('expanded')); const signal = useSignal(initial); + const totalCookiePopUpsBlockedSignal = useSignal(/** @type {number | null | undefined} */ (null)); useEffect(() => { if (interval === 0) return; const int = setInterval(() => (signal.value += 1), interval); @@ -183,5 +255,5 @@ const MockWithState = ({ children, initial = 0, feedType = 'privacy-stats', inte const toggle = () => { setExpansion((old) => (old === 'expanded' ? 'collapsed' : 'expanded')); }; - return children({ toggle, expansion, feed, setFeed, blockedCountSignal: signal }); + return children({ toggle, expansion, feed, setFeed, blockedCountSignal: signal, totalCookiePopUpsBlockedSignal }); }; diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js index f05f8ce8d5..7e033c968e 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js @@ -1,13 +1,10 @@ import { useTypedTranslationWith } from '../../types.js'; -import { useState } from 'preact/hooks'; import styles from '../../privacy-stats/components/PrivacyStats.module.css'; import { ShowHideButtonCircle } from '../../components/ShowHideButton.jsx'; import cn from 'classnames'; import { h } from 'preact'; -import { useAdBlocking } from '../../settings.provider.js'; -import { Trans } from '../../../../../shared/components/TranslationsProvider.js'; -import { getLocalizedNumberFormatter } from '../../../../../shared/utils.js'; -import { useLocale } from '../../../../../shared/components/EnvironmentProvider.js'; +import { InfoIcon, NewBadgeIcon } from '../../components/Icons.js'; +import { Tooltip } from '../../components/Tooltip/Tooltip.js'; /** * @import enStrings from "../strings.json" @@ -20,33 +17,43 @@ import { useLocale } from '../../../../../shared/components/EnvironmentProvider. * @param {boolean} props.canExpand * @param {() => void} props.onToggle * @param {import('preact').ComponentProps<'button'>} [props.buttonAttrs] + * @param {import("@preact/signals").Signal} props.totalCookiePopUpsBlockedSignal */ -export function ProtectionsHeading({ expansion, canExpand, blockedCountSignal, onToggle, buttonAttrs = {} }) { +export function ProtectionsHeading({ + expansion, + canExpand, + blockedCountSignal, + onToggle, + buttonAttrs = {}, + totalCookiePopUpsBlockedSignal, +}) { const { t } = useTypedTranslationWith(/** @type {Strings} */ ({})); - const locale = useLocale(); - const [formatter] = useState(() => getLocalizedNumberFormatter(locale)); - const adBlocking = useAdBlocking(); - const blockedCount = blockedCountSignal.value; - const none = blockedCount === 0; - const some = blockedCount > 0; - const alltime = formatter.format(blockedCount); + const totalTrackersBlocked = blockedCountSignal.value; + const totalCookiePopUpsBlocked = totalCookiePopUpsBlockedSignal.value ?? 0; - let alltimeTitle; - if (blockedCount === 1) { - alltimeTitle = adBlocking ? t('stats_countBlockedAdsAndTrackersSingular') : t('stats_countBlockedSingular'); - } else { - alltimeTitle = adBlocking - ? t('stats_countBlockedAdsAndTrackersPlural', { count: alltime }) - : t('stats_countBlockedPlural', { count: alltime }); - } + // Native does not tell the FE if cookie pop up protection is enabled but + // we can derive this from the value of `totalCookiePopUpsBlocked` in the + // `ProtectionsService` + const isCpmEnabled = totalCookiePopUpsBlockedSignal.value !== null; + + const trackersBlockedHeading = totalTrackersBlocked === 1 ? t('stats_countBlockedSingular') : t('stats_countBlockedPlural'); + + const cookiePopUpsBlockedHeading = + totalCookiePopUpsBlocked === 1 ? t('stats_totalCookiePopUpsBlockedSingular') : t('stats_totalCookiePopUpsBlockedPlural'); return (
    -
    +
    Privacy Shield

    {t('protections_menuTitle')}

    + + {/* @todo accessibility: move focus to content on hover? */} + + + + {canExpand && ( )}
    -
    - {none &&

    {t('protections_noRecent')}

    } - {some && ( -

    - {' '} - -

    +
    + {/* Total Trackers Blocked */} +
    + {totalTrackersBlocked === 0 &&

    {t('protections_noRecent')}

    } + {totalTrackersBlocked > 0 && ( +

    + {totalTrackersBlocked} + {trackersBlockedHeading} +

    + )} +
    + + {/* Total Cookie Pop-Ups Blocked */} + {/* Rules: Display CPM stats when Cookie Pop-Up Protection is + enabled AND both `totalTrackersBlocked` and + `totalCookiePopUpsBlocked` are at least 1 */} + {isCpmEnabled && totalTrackersBlocked > 0 && totalCookiePopUpsBlocked > 0 && ( +
    +

    + {totalCookiePopUpsBlocked} + {cookiePopUpsBlockedHeading} +

    + {/* @todo `NewBadgeIcon` will be manually removed in + a future iteration */} + +
    )} -

    {t('stats_feedCountBlockedPeriod')}

    ); diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeadingLegacy.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeadingLegacy.js new file mode 100644 index 0000000000..c615a68f35 --- /dev/null +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeadingLegacy.js @@ -0,0 +1,76 @@ +import { useTypedTranslationWith } from '../../types.js'; +import { useState } from 'preact/hooks'; +import styles from '../../privacy-stats/components/PrivacyStatsLegacy.module.css'; +import { ShowHideButtonCircle } from '../../components/ShowHideButton.jsx'; +import cn from 'classnames'; +import { h } from 'preact'; +import { useAdBlocking } from '../../settings.provider.js'; +import { Trans } from '../../../../../shared/components/TranslationsProvider.js'; +import { getLocalizedNumberFormatter } from '../../../../../shared/utils.js'; +import { useLocale } from '../../../../../shared/components/EnvironmentProvider.js'; + +/** + * @import enStrings from "../strings.json" + * @import statsStrings from "../../privacy-stats/strings.json" + * @import activityStrings from "../../activity/strings.json" + * @typedef {enStrings & statsStrings & activityStrings} Strings + * @param {object} props + * @param {import('../../../types/new-tab.ts').Expansion} props.expansion + * @param {import("@preact/signals").Signal} props.blockedCountSignal + * @param {boolean} props.canExpand + * @param {() => void} props.onToggle + * @param {import('preact').ComponentProps<'button'>} [props.buttonAttrs] + */ +export function ProtectionsHeadingLegacy({ expansion, canExpand, blockedCountSignal, onToggle, buttonAttrs = {} }) { + const { t } = useTypedTranslationWith(/** @type {Strings} */ ({})); + const locale = useLocale(); + const [formatter] = useState(() => getLocalizedNumberFormatter(locale)); + const adBlocking = useAdBlocking(); + const blockedCount = blockedCountSignal.value; + const none = blockedCount === 0; + const some = blockedCount > 0; + const alltime = formatter.format(blockedCount); + + let alltimeTitle; + if (blockedCount === 1) { + alltimeTitle = adBlocking ? t('stats_countBlockedAdsAndTrackersSingularLegacy') : t('stats_countBlockedSingularLegacy'); + } else { + alltimeTitle = adBlocking + ? t('stats_countBlockedAdsAndTrackersPluralLegacy', { count: alltime }) + : t('stats_countBlockedPlural', { count: alltime }); + } + + return ( +
    +
    + + Privacy Shield + +

    {t('protections_menuTitle')}

    + {canExpand && ( + + + + )} +
    +
    + {none &&

    {t('protections_noRecent')}

    } + {some && ( +

    + {' '} + +

    + )} +

    {t('stats_feedCountBlockedPeriod')}

    +
    +
    + ); +} diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js index 1569ebfec3..85e57edb8b 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js @@ -97,6 +97,7 @@ export function useService() { export function useBlockedCount(initial) { const service = useService(); const signal = useSignal(initial); + // @todo jingram possibly refactor to include full object useSignalEffect(() => { return service.current?.onData((evt) => { signal.value = evt.data.totalCount; @@ -104,3 +105,20 @@ export function useBlockedCount(initial) { }); return signal; } + +/** + * @param {number | null | undefined} initial + * @return {import("@preact/signals").Signal} + */ +export function useCookiePopUpsBlockedCount(initial) { + const service = useService(); + const signal = useSignal(initial); + + useSignalEffect(() => { + return service.current?.onData((evt) => { + signal.value = evt.data.totalCookiePopUpsBlocked; + }); + }); + + return signal; +} diff --git a/special-pages/pages/new-tab/app/protections/integrations-tests/protections.page.js b/special-pages/pages/new-tab/app/protections/integrations-tests/protections.page.js index f6f6958afe..510be68bcc 100644 --- a/special-pages/pages/new-tab/app/protections/integrations-tests/protections.page.js +++ b/special-pages/pages/new-tab/app/protections/integrations-tests/protections.page.js @@ -40,6 +40,7 @@ export class ProtectionsPage { /** @type {ProtectionsData} */ const data = { totalCount: count, + totalCookiePopUpsBlocked: null, // null means CPM is not enabled }; await this.ntp.mocks.simulateSubscriptionMessage(named.subscription('protections_onDataUpdate'), data); await expect(this.context().getByRole('heading', { level: 3 })).toContainText(`${count} tracking attempts blocked`); @@ -56,4 +57,56 @@ export class ProtectionsPage { - paragraph: Ostatnie 7 dni `); } + + /** + * Test that cookie popup blocking stats are displayed when both trackers and cookie popups are > 0 + */ + async displaysCookiePopupStats() { + /** @type {ProtectionsData} */ + const data = { + totalCount: 100, + totalCookiePopUpsBlocked: 25, + }; + await this.ntp.mocks.simulateSubscriptionMessage(named.subscription('protections_onDataUpdate'), data); + await expect(this.context().getByRole('heading', { level: 3 }).first()).toContainText('100 tracking attempts blocked'); + // Cookie popup stats should be visible + await expect(this.context().getByText(/cookie pop-ups?/i)).toBeVisible(); + } + + /** + * Test that cookie popup stats are NOT displayed when totalCookiePopUpsBlocked is null (CPM disabled) + */ + async hidesCookiePopupStatsWhenDisabled() { + /** @type {ProtectionsData} */ + const data = { + totalCount: 100, + totalCookiePopUpsBlocked: null, + }; + await this.ntp.mocks.simulateSubscriptionMessage(named.subscription('protections_onDataUpdate'), data); + // Cookie popup stats should not be visible + await expect(this.context().getByText(/cookie pop-ups?/i)).not.toBeVisible(); + } + + /** + * Test that cookie popup stats are NOT displayed when totalCookiePopUpsBlocked is 0 + */ + async hidesCookiePopupStatsWhenZero() { + /** @type {ProtectionsData} */ + const data = { + totalCount: 100, + totalCookiePopUpsBlocked: 0, + }; + await this.ntp.mocks.simulateSubscriptionMessage(named.subscription('protections_onDataUpdate'), data); + // Cookie popup stats should not be visible when count is 0 + await expect(this.context().getByText(/cookie pop-ups?/i)).not.toBeVisible(); + } + + /** + * Test that the info tooltip is displayed + */ + async hasInfoTooltip() { + const heading = this.context().getByTestId('ProtectionsHeading'); + // The InfoIcon should be present + await expect(heading.locator('[class*="infoIcon"]')).toBeVisible(); + } } diff --git a/special-pages/pages/new-tab/app/protections/integrations-tests/protections.spec.js b/special-pages/pages/new-tab/app/protections/integrations-tests/protections.spec.js index aa05b645bb..ba5e891e3a 100644 --- a/special-pages/pages/new-tab/app/protections/integrations-tests/protections.spec.js +++ b/special-pages/pages/new-tab/app/protections/integrations-tests/protections.spec.js @@ -56,4 +56,44 @@ test.describe('protections report', () => { await protections.ready(); await protections.hasPolishText(); }); + + test('displays cookie popup blocking stats when enabled and counts > 0', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { 'protections.feed': 'activity' } }); + + const protections = new ProtectionsPage(ntp); + await protections.ready(); + await protections.displaysCookiePopupStats(); + }); + + test('hides cookie popup stats when CPM is disabled (null)', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { 'protections.feed': 'activity' } }); + + const protections = new ProtectionsPage(ntp); + await protections.ready(); + await protections.hidesCookiePopupStatsWhenDisabled(); + }); + + test('hides cookie popup stats when count is 0', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { 'protections.feed': 'activity' } }); + + const protections = new ProtectionsPage(ntp); + await protections.ready(); + await protections.hidesCookiePopupStatsWhenZero(); + }); + + test('displays info tooltip', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { 'protections.feed': 'activity' } }); + + const protections = new ProtectionsPage(ntp); + await protections.ready(); + await protections.hasInfoTooltip(); + }); }); diff --git a/special-pages/pages/new-tab/app/protections/mocks/protections.mock-transport.js b/special-pages/pages/new-tab/app/protections/mocks/protections.mock-transport.js index c92573c58c..a9f52fe8c3 100644 --- a/special-pages/pages/new-tab/app/protections/mocks/protections.mock-transport.js +++ b/special-pages/pages/new-tab/app/protections/mocks/protections.mock-transport.js @@ -80,12 +80,31 @@ export function protectionsMockTransport() { const msg = /** @type {any} */ (_msg); switch (msg.method) { case 'protections_getData': + // No data. Setting `stats=none` (totalCount = 0) also + // hides CPM stats if (url.searchParams.get('stats') === 'none') { dataset.totalCount = 0; } if (url.searchParams.get('activity') === 'empty') { dataset.totalCount = 0; } + if (url.searchParams.get('cpm') === 'true') { + dataset.totalCookiePopUpsBlocked = 22; + } + // CPM = 0 state + if (url.searchParams.get('cpm') === 'none') { + dataset.totalCookiePopUpsBlocked = 0; + } + // CPM disabled state + if (url.searchParams.get('cpm') === 'null') { + dataset.totalCookiePopUpsBlocked = null; + } + // Setting cpm=undefined allows us to see the legacy + // protections report. Useful until all platforms adopt the + // new schema + if (url.searchParams.get('cpm') === 'undefined') { + dataset.totalCookiePopUpsBlocked = undefined; + } return Promise.resolve(dataset); case 'protections_getConfig': { if (url.searchParams.get('protections.feed') === 'activity') { diff --git a/special-pages/pages/new-tab/app/protections/mocks/protections.mocks.js b/special-pages/pages/new-tab/app/protections/mocks/protections.mocks.js index 19ebdfde9e..3480bedfb0 100644 --- a/special-pages/pages/new-tab/app/protections/mocks/protections.mocks.js +++ b/special-pages/pages/new-tab/app/protections/mocks/protections.mocks.js @@ -5,11 +5,14 @@ export const protectionsMocks = { empty: { totalCount: 0, + totalCookiePopUpsBlocked: 0, }, few: { totalCount: 86, + totalCookiePopUpsBlocked: 21, }, many: { totalCount: 1_000_020, + totalCookiePopUpsBlocked: 5_432, }, }; diff --git a/special-pages/pages/new-tab/app/protections/protections.md b/special-pages/pages/new-tab/app/protections/protections.md index d542032611..b1a5d70863 100644 --- a/special-pages/pages/new-tab/app/protections/protections.md +++ b/special-pages/pages/new-tab/app/protections/protections.md @@ -29,7 +29,8 @@ title: Protections Report - returns {@link "NewTab Messages".ProtectionsData} ```json { - "totalCount": 84 + "totalCount": 84, + "totalCookiePopUpsBlocked": 23 } ``` diff --git a/special-pages/pages/new-tab/app/styles/ntp-theme.css b/special-pages/pages/new-tab/app/styles/ntp-theme.css index 02b89807d4..42d98c5ec2 100644 --- a/special-pages/pages/new-tab/app/styles/ntp-theme.css +++ b/special-pages/pages/new-tab/app/styles/ntp-theme.css @@ -49,8 +49,10 @@ body { --ntp-surface-border-color: var(--color-black-at-9); --ntp-text-normal: var(--color-black-at-84); --ntp-text-muted: var(--color-black-at-60); + --ntp-protections-text-muted: var(--color-black-at-66); --ntp-text-on-primary: var(--color-white-at-84); --ntp-color-primary: var(--ddg-color-primary); + --ntp-widget-expander-bg: rgba(31, 31, 31, 0.09); --ntp-focus-outline-color: black; --focus-ring: 0px 0px 0px 1px var(--color-white), 0px 0px 0px 3px var(--ntp-focus-outline-color); --focus-ring-thin: 0px 0px 0px 1px var(--ntp-focus-outline-color), 0px 0px 0px 1px var(--color-white); @@ -86,8 +88,10 @@ body { --ntp-surface-border-color: var(--color-white-at-12); --ntp-text-normal: var(--color-white-at-84); --ntp-text-muted: var(--color-white-at-60); + --ntp-protections-text-muted: var(--color-white-at-66); --ntp-color-primary: var(--color-blue-30); --ntp-text-on-primary: var(--color-black-at-84); + --ntp-widget-expander-bg: rgba(249, 249, 249, 0.12); --ntp-focus-outline-color: white; --focus-ring: 0px 0px 0px 1px var(--ntp-focus-outline-color), 0px 0px 0px 3px var(--color-white); --focus-ring-thin: 0px 0px 0px 1px var(--color-white), 0px 0px 0px 1px var(--ntp-focus-outline-color); diff --git a/special-pages/pages/new-tab/messages/types/activity.json b/special-pages/pages/new-tab/messages/types/activity.json index 34382b3eb8..6b34a14aa2 100644 --- a/special-pages/pages/new-tab/messages/types/activity.json +++ b/special-pages/pages/new-tab/messages/types/activity.json @@ -45,6 +45,17 @@ }, "favorite": { "type": "boolean" + }, + "cookiePopUpBlocked": { + "description": "A cookie pop-up has been blocked for the specific domain", + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + } + ] } }, "required": ["etldPlusOne", "title", "url", "trackingStatus", "trackersFound", "history", "favorite", "favicon"] @@ -91,4 +102,4 @@ "required": ["title", "url", "relativeTime"] } } -} \ No newline at end of file +} diff --git a/special-pages/pages/new-tab/messages/types/protections-data.json b/special-pages/pages/new-tab/messages/types/protections-data.json index 171858df36..c1f7b4010d 100644 --- a/special-pages/pages/new-tab/messages/types/protections-data.json +++ b/special-pages/pages/new-tab/messages/types/protections-data.json @@ -10,6 +10,17 @@ "totalCount": { "description": "Total number of trackers or ads blocked since install", "type": "number" + }, + "totalCookiePopUpsBlocked": { + "description": "Total number of cookie pop-ups blocked since install", + "oneOf": [ + { + "type": "null" + }, + { + "type": "number" + } + ] } } } diff --git a/special-pages/pages/new-tab/public/locales/en/new-tab.json b/special-pages/pages/new-tab/public/locales/en/new-tab.json index 88ea78a70b..420cb0ae7e 100644 --- a/special-pages/pages/new-tab/public/locales/en/new-tab.json +++ b/special-pages/pages/new-tab/public/locales/en/new-tab.json @@ -78,10 +78,18 @@ "note": "Placeholder to indicate that no tracking activity was blocked in the last 7 days" }, "stats_countBlockedSingular": { + "title": "Tracking attempt blocked", + "note": "The main headline indicating that a single tracker was blocked" + }, + "stats_countBlockedSingularLegacy": { "title": "1 tracking attempt blocked", "note": "The main headline indicating that a single tracker was blocked" }, "stats_countBlockedPlural": { + "title": "Tracking attempts blocked", + "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" + }, + "stats_countBlockedPluralLegacy": { "title": "{count} tracking attempts blocked", "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" }, @@ -94,13 +102,33 @@ "note": "Placeholder to indicate that no ads or tracking activity was blocked in the last 7 days" }, "stats_countBlockedAdsAndTrackersSingular": { + "title": "advertising & tracking attempt blocked", + "note": "The main headline indicating that a single ad or tracking attempt was blocked" + }, + "stats_countBlockedAdsAndTrackersSingularLegacy": { "title": "1 advertising & tracking attempt blocked", "note": "The main headline indicating that a single ad or tracking attempt was blocked" }, "stats_countBlockedAdsAndTrackersPlural": { + "title": "advertising & tracking attempts blocked", + "note": "The main headline indicating that more than 1 ad or tracking attempt has been blocked. Eg: '2 advertising & tracking attempts blocked" + }, + "stats_countBlockedAdsAndTrackersPluralLegacy": { "title": "{count} advertising & tracking attempts blocked", "note": "The main headline indicating that more than 1 ad or tracking attempt has been blocked. Eg: '2 advertising & tracking attempts blocked" }, + "stats_totalCookiePopUpsBlockedSingular": { + "title": "Cookie pop-up blocked", + "note": "The heading indicating that a single cookie pop-up was handled by the CPM" + }, + "stats_totalCookiePopUpsBlockedPlural": { + "title": "Cookie pop-ups blocked", + "note": "The heading indicating multiple cookie pop-ups were handled by the CPM" + }, + "stats_protectionsReportInfo": { + "title": "Displays tracking attempts blocked in the last 7 days and the number of cookie pop-ups blocked since you started using the browser. You can reset these stats using the Fire Button.", + "note": "Text explaining how to reset the protections stats" + }, "stats_feedCountBlockedSingular": { "title": "1 attempt blocked by DuckDuckGo in the last 7 days", "note": "A summary description of how many tracking attempts where blocked, when only one exists." @@ -410,9 +438,21 @@ "note": "Placeholder message indicating that no trackers are blocked" }, "activity_countBlockedPlural": { + "title": "{count} Tracking attempts blocked", + "note": "Pill text indicating that more than 1 attempt has been blocked. Eg: '2 Tracking attempts blocked'" + }, + "activity_countBlockedPluralLegacy": { "title": "{count} tracking attempts blocked", "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" }, + "activity_countBlockedSingular": { + "title": "{count} Tracking attempt blocked", + "note": "Pill text indicating that 1 attempt has been blocked. Eg: '1 Tracking attempt blocked'" + }, + "activity_cookiePopUpBlocked": { + "title": "Cookie pop-up blocked", + "note": "Pill text indicating that we have blocked cookie pop-ups" + }, "activity_noRecentAdsAndTrackers_subtitle": { "title": "Recently visited sites will appear here. Keep browsing to see how many ads and trackers we block.", "note": "Shown in the place a list of browsing history entries will be displayed." diff --git a/special-pages/pages/new-tab/types/new-tab.ts b/special-pages/pages/new-tab/types/new-tab.ts index 560fc8bd02..1755ba711b 100644 --- a/special-pages/pages/new-tab/types/new-tab.ts +++ b/special-pages/pages/new-tab/types/new-tab.ts @@ -745,6 +745,10 @@ export interface DomainActivity { trackersFound: boolean; history: HistoryEntry[]; favorite: boolean; + /** + * A cookie pop-up has been blocked for the specific domain + */ + cookiePopUpBlocked?: null | boolean; } export interface TrackingStatus { trackerCompanies: { @@ -968,6 +972,10 @@ export interface ProtectionsData { * Total number of trackers or ads blocked since install */ totalCount: number; + /** + * Total number of cookie pop-ups blocked since install + */ + totalCookiePopUpsBlocked?: null | number; } /** * Generated from @see "../messages/rmf_getData.request.json" diff --git a/special-pages/shared/styles/variables.css b/special-pages/shared/styles/variables.css index da3a306280..62d5ebe2d4 100644 --- a/special-pages/shared/styles/variables.css +++ b/special-pages/shared/styles/variables.css @@ -90,6 +90,7 @@ --color-black-at-48: rgba(0, 0, 0, 0.48); --color-black-at-50: rgba(0, 0, 0, 0.5); --color-black-at-60: rgba(0, 0, 0, 0.6); + --color-black-at-66: rgba(0, 0, 0, 0.66); --color-black-at-72: rgba(0, 0, 0, 0.72); --color-black-at-80: rgba(0, 0, 0, 0.8); --color-black-at-84: rgba(0, 0, 0, 0.84); @@ -110,6 +111,7 @@ --color-white-at-42: rgba(255, 255, 255, 0.42); --color-white-at-50: rgba(255, 255, 255, 0.5); --color-white-at-60: rgba(255, 255, 255, 0.6); + --color-white-at-66: rgba(255, 255, 255, 0.66); --color-white-at-70: rgba(255, 255, 255, 0.7); --color-white-at-80: rgba(255, 255, 255, 0.8); --color-white-at-84: rgba(255, 255, 255, 0.84);