From 52a83f723e575001622b1e23fae5b68bf02ed47f Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 15 Oct 2025 17:22:33 -0400 Subject: [PATCH 01/15] Move utility functions to shared file and update unit tests Remove freemium dir from test-unit script --- special-pages/package.json | 2 +- .../components/FreemiumPIRBanner.js | 2 +- .../freemiumPIRBanner.utils.js | 30 ------------------- special-pages/shared/utils.js | 30 +++++++++++++++++++ .../unit-tests => unit-test}/utils.spec.mjs | 2 +- 5 files changed, 33 insertions(+), 33 deletions(-) delete mode 100644 special-pages/pages/new-tab/app/freemium-pir-banner/freemiumPIRBanner.utils.js rename special-pages/{pages/new-tab/app/freemium-pir-banner/unit-tests => unit-test}/utils.spec.mjs (96%) diff --git a/special-pages/package.json b/special-pages/package.json index e7860339f0..9efd08598f 100644 --- a/special-pages/package.json +++ b/special-pages/package.json @@ -10,7 +10,7 @@ "build": "node index.mjs", "build.dev": "npm run build -- --env development", "lint-fix": "cd ../ && npm run lint-fix", - "test-unit": "node --test \"unit-test/*\" \"pages/history/unit-tests/*\" \"pages/duckplayer/unit-tests/*\" \"pages/new-tab/app/freemium-pir-banner/unit-tests/*\" \"pages/new-tab/app/omnibar/unit-tests/*\"", + "test-unit": "node --test \"unit-test/*\" \"pages/history/unit-tests/*\" \"pages/duckplayer/unit-tests/*\" \"pages/new-tab/app/omnibar/unit-tests/*\"", "test-int": "playwright test --grep-invert '@screenshots'", "test-int-x": "npm run test-int", "test-int-snapshots": "playwright test --grep '@screenshots'", diff --git a/special-pages/pages/new-tab/app/freemium-pir-banner/components/FreemiumPIRBanner.js b/special-pages/pages/new-tab/app/freemium-pir-banner/components/FreemiumPIRBanner.js index 768ecce651..c57836816f 100644 --- a/special-pages/pages/new-tab/app/freemium-pir-banner/components/FreemiumPIRBanner.js +++ b/special-pages/pages/new-tab/app/freemium-pir-banner/components/FreemiumPIRBanner.js @@ -5,7 +5,7 @@ import { DismissButton } from '../../components/DismissButton'; import styles from './FreemiumPIRBanner.module.css'; import { FreemiumPIRBannerContext } from '../FreemiumPIRBannerProvider'; import { useContext } from 'preact/hooks'; -import { convertMarkdownToHTMLForStrongTags } from '../freemiumPIRBanner.utils'; +import { convertMarkdownToHTMLForStrongTags } from '../../../../../shared/utils'; /** * @typedef { import("../../../types/new-tab").FreemiumPIRBannerMessage} FreemiumPIRBannerMessage diff --git a/special-pages/pages/new-tab/app/freemium-pir-banner/freemiumPIRBanner.utils.js b/special-pages/pages/new-tab/app/freemium-pir-banner/freemiumPIRBanner.utils.js deleted file mode 100644 index 3900c6428e..0000000000 --- a/special-pages/pages/new-tab/app/freemium-pir-banner/freemiumPIRBanner.utils.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @type {(markdown: string) => string} convertMarkdownToHTMLForStrongTags - */ - -export function convertMarkdownToHTMLForStrongTags(markdown) { - // first, remove any HTML tags - markdown = escapeXML(markdown); - - // Use a regular expression to find all the words wrapped in ** - const regex = /\*\*(.*?)\*\*/g; - - // Replace the matched text with the HTML tags - const result = markdown.replace(regex, '$1'); - return result; -} - -/** - * Escapes any occurrences of &, ", <, > or / with XML entities. - */ -function escapeXML(str) { - const replacements = { - '&': '&', - '"': '"', - "'": ''', - '<': '<', - '>': '>', - '/': '/', - }; - return String(str).replace(/[&"'<>/]/g, (m) => replacements[m]); -} diff --git a/special-pages/shared/utils.js b/special-pages/shared/utils.js index e8627f0235..06f9537409 100644 --- a/special-pages/shared/utils.js +++ b/special-pages/shared/utils.js @@ -21,3 +21,33 @@ export const getLocalizedNumberFormatter = (locale) => { return new Intl.NumberFormat(localeToUse); }; + +/** + * @type {(markdown: string) => string} convertMarkdownToHTMLForStrongTags + */ +export function convertMarkdownToHTMLForStrongTags(markdown) { + // first, remove any HTML tags + markdown = escapeXML(markdown); + + // Use a regular expression to find all the words wrapped in ** + const regex = /\*\*(.*?)\*\*/g; + + // Replace the matched text with the HTML tags + const result = markdown.replace(regex, '$1'); + return result; +} + +/** + * Escapes any occurrences of &, ", <, > or / with XML entities. + */ +function escapeXML(str) { + const replacements = { + '&': '&', + '"': '"', + "'": ''', + '<': '<', + '>': '>', + '/': '/', + }; + return String(str).replace(/[&"'<>/]/g, (m) => replacements[m]); +} diff --git a/special-pages/pages/new-tab/app/freemium-pir-banner/unit-tests/utils.spec.mjs b/special-pages/unit-test/utils.spec.mjs similarity index 96% rename from special-pages/pages/new-tab/app/freemium-pir-banner/unit-tests/utils.spec.mjs rename to special-pages/unit-test/utils.spec.mjs index 08caf8e9e8..1d856bf091 100644 --- a/special-pages/pages/new-tab/app/freemium-pir-banner/unit-tests/utils.spec.mjs +++ b/special-pages/unit-test/utils.spec.mjs @@ -1,6 +1,6 @@ import { describe, it } from 'node:test'; import { equal } from 'node:assert/strict'; -import { convertMarkdownToHTMLForStrongTags } from '../freemiumPIRBanner.utils.js'; +import { convertMarkdownToHTMLForStrongTags } from '../shared/utils'; describe('convertMarkdownToHTMLForStrongTags', () => { it('with terms wrapped in "**" will return with tags wrapping that part of the string', () => { From c72ff3699ae9a21d58abf1523e38536ca72d0acb Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 15 Oct 2025 17:43:23 -0400 Subject: [PATCH 02/15] Messaging support for the subscription win-back banner --- .../examples/subscriptionWinBackBanner.js | 12 +++++ .../subscriptionWinBackBanner-message.json | 51 +++++++++++++++++++ .../messages/winBackOffer_action.notify.json | 13 +++++ .../messages/winBackOffer_dismiss.notify.json | 13 +++++ .../winBackOffer_getData.request.json | 3 ++ .../winBackOffer_getData.response.json | 8 +++ .../winBackOffer_onDataUpdate.subscribe.json | 8 +++ 7 files changed, 108 insertions(+) create mode 100644 special-pages/pages/new-tab/messages/examples/subscriptionWinBackBanner.js create mode 100644 special-pages/pages/new-tab/messages/types/subscriptionWinBackBanner-message.json create mode 100644 special-pages/pages/new-tab/messages/winBackOffer_action.notify.json create mode 100644 special-pages/pages/new-tab/messages/winBackOffer_dismiss.notify.json create mode 100644 special-pages/pages/new-tab/messages/winBackOffer_getData.request.json create mode 100644 special-pages/pages/new-tab/messages/winBackOffer_getData.response.json create mode 100644 special-pages/pages/new-tab/messages/winBackOffer_onDataUpdate.subscribe.json diff --git a/special-pages/pages/new-tab/messages/examples/subscriptionWinBackBanner.js b/special-pages/pages/new-tab/messages/examples/subscriptionWinBackBanner.js new file mode 100644 index 0000000000..2dfc725400 --- /dev/null +++ b/special-pages/pages/new-tab/messages/examples/subscriptionWinBackBanner.js @@ -0,0 +1,12 @@ +/** + * @type {import("../../types/new-tab.js").SubscriptionWinBackBannerData} + */ +const subscriptionWinBackBannerLastDay = { + content: { + messageType: 'big_single_action', + id: 'winback_last_day', + titleText: 'Last day to save 25%!', + descriptionText: 'Stay protected with our VPN, private AI, and more. Resubscribe today and save 25%. Limited time offer.', + actionText: 'See Offer', + }, +}; diff --git a/special-pages/pages/new-tab/messages/types/subscriptionWinBackBanner-message.json b/special-pages/pages/new-tab/messages/types/subscriptionWinBackBanner-message.json new file mode 100644 index 0000000000..5286bf056a --- /dev/null +++ b/special-pages/pages/new-tab/messages/types/subscriptionWinBackBanner-message.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Subscription Win-back Banner Data", + "type": "object", + "required": [ + "content" + ], + "properties": { + "content": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "object", + "title": "Subscription Win-back Banner Message", + "required": [ + "messageType", + "id", + "descriptionText", + "titleText", + "actionText" + ], + "properties": { + "messageType": { + "const": "big_single_action" + }, + "id": { + "type": "string", + "enum": [ + "winback_last_day" + ] + }, + "titleText": { + "type": [ + "string", + "null" + ] + }, + "descriptionText": { + "type": "string" + }, + "actionText": { + "type": "string" + } + } + } + ] + } + } +} diff --git a/special-pages/pages/new-tab/messages/winBackOffer_action.notify.json b/special-pages/pages/new-tab/messages/winBackOffer_action.notify.json new file mode 100644 index 0000000000..bb63871568 --- /dev/null +++ b/special-pages/pages/new-tab/messages/winBackOffer_action.notify.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Subscription Win-back Banner Action", + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + } + } +} diff --git a/special-pages/pages/new-tab/messages/winBackOffer_dismiss.notify.json b/special-pages/pages/new-tab/messages/winBackOffer_dismiss.notify.json new file mode 100644 index 0000000000..ad7c68b471 --- /dev/null +++ b/special-pages/pages/new-tab/messages/winBackOffer_dismiss.notify.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Subscription Win-back Banner Dismiss Action", + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + } + } +} diff --git a/special-pages/pages/new-tab/messages/winBackOffer_getData.request.json b/special-pages/pages/new-tab/messages/winBackOffer_getData.request.json new file mode 100644 index 0000000000..fbdeff52ec --- /dev/null +++ b/special-pages/pages/new-tab/messages/winBackOffer_getData.request.json @@ -0,0 +1,3 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#" +} diff --git a/special-pages/pages/new-tab/messages/winBackOffer_getData.response.json b/special-pages/pages/new-tab/messages/winBackOffer_getData.response.json new file mode 100644 index 0000000000..6f65c9e468 --- /dev/null +++ b/special-pages/pages/new-tab/messages/winBackOffer_getData.response.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": [ + { + "$ref": "types/subscriptionWinBackBanner-message.json" + } + ] +} diff --git a/special-pages/pages/new-tab/messages/winBackOffer_onDataUpdate.subscribe.json b/special-pages/pages/new-tab/messages/winBackOffer_onDataUpdate.subscribe.json new file mode 100644 index 0000000000..6f65c9e468 --- /dev/null +++ b/special-pages/pages/new-tab/messages/winBackOffer_onDataUpdate.subscribe.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": [ + { + "$ref": "types/subscriptionWinBackBanner-message.json" + } + ] +} From adb03e7aaaa8326e096c5a099a23e4bb17d99d20 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 15 Oct 2025 18:13:47 -0400 Subject: [PATCH 03/15] Create win-back banner component and supporting files Include supporting svg --- .../SubscriptionWinBackBannerProvider.js | 96 +++++++++++++++++++ .../SubscriptionWinBackBanner.examples.js | 18 ++++ .../components/SubscriptionWinBackBanner.js | 47 +++++++++ .../SubscriptionWinBackBanner.module.css | 41 ++++++++ .../subscription-winback-banner.spec.js | 31 ++++++ .../mocks/subscriptionWinBackBanner.data.js | 15 +++ .../subscription-winback-banner.md | 29 ++++++ .../subscriptionWinBackBanner.service.js | 61 ++++++++++++ .../public/icons/Subscription-Clock-96.svg | 42 ++++++++ 9 files changed, 380 insertions(+) create mode 100644 special-pages/pages/new-tab/app/subscription-winback-banner/SubscriptionWinBackBannerProvider.js create mode 100644 special-pages/pages/new-tab/app/subscription-winback-banner/components/SubscriptionWinBackBanner.examples.js create mode 100644 special-pages/pages/new-tab/app/subscription-winback-banner/components/SubscriptionWinBackBanner.js create mode 100644 special-pages/pages/new-tab/app/subscription-winback-banner/components/SubscriptionWinBackBanner.module.css create mode 100644 special-pages/pages/new-tab/app/subscription-winback-banner/integration-tests/subscription-winback-banner.spec.js create mode 100644 special-pages/pages/new-tab/app/subscription-winback-banner/mocks/subscriptionWinBackBanner.data.js create mode 100644 special-pages/pages/new-tab/app/subscription-winback-banner/subscription-winback-banner.md create mode 100644 special-pages/pages/new-tab/app/subscription-winback-banner/subscriptionWinBackBanner.service.js create mode 100644 special-pages/pages/new-tab/public/icons/Subscription-Clock-96.svg diff --git a/special-pages/pages/new-tab/app/subscription-winback-banner/SubscriptionWinBackBannerProvider.js b/special-pages/pages/new-tab/app/subscription-winback-banner/SubscriptionWinBackBannerProvider.js new file mode 100644 index 0000000000..78dde8167f --- /dev/null +++ b/special-pages/pages/new-tab/app/subscription-winback-banner/SubscriptionWinBackBannerProvider.js @@ -0,0 +1,96 @@ +import { createContext, h } from 'preact'; +import { useCallback, useEffect, useReducer, useRef } from 'preact/hooks'; +import { useMessaging } from '../types.js'; +import { SubscriptionWinBackBannerService } from './subscriptionWinBackBanner.service.js'; +import { reducer, useDataSubscription, useInitialData } from '../service.hooks.js'; + +/** + * @typedef {import('../../types/new-tab.js').SubscriptionWinBackBannerData} SubscriptionWinBackBannerData + * @typedef {import('../service.hooks.js').State} State + * @typedef {import('../service.hooks.js').Events} Events + */ + +/** + * These are the values exposed to consumers. + */ +export const SubscriptionWinBackBannerContext = createContext({ + /** @type {State} */ + state: { status: 'idle', data: null, config: null }, + /** @type {(id: string) => void} */ + dismiss: (id) => { + throw new Error('must implement dismiss' + id); + }, + /** @type {(id: string) => void} */ + action: (id) => { + throw new Error('must implement action' + id); + }, +}); + +export const SubscriptionWinBackBannerDispatchContext = createContext(/** @type {import("preact/hooks").Dispatch} */ ({})); + +/** + * A data provider that will use `SubscriptionWinBackBannerService` to fetch data, subscribe + * to updates and modify state. + * + * @param {Object} props + * @param {import("preact").ComponentChild} props.children + */ +export function SubscriptionWinBackBannerProvider(props) { + const initial = /** @type {State} */ ({ + status: 'idle', + data: null, + config: null, + }); + + // const [state, dispatch] = useReducer(withLog('SubscriptionWinBackBannerProvider', reducer), initial) + const [state, dispatch] = useReducer(reducer, initial); + + // create an instance of `SubscriptionWinBackBannerService` for the lifespan of this component. + const service = useService(); + + // get initial data + useInitialData({ dispatch, service }); + + // subscribe to data updates + useDataSubscription({ dispatch, service }); + + // todo(valerie): implement onDismiss in the service + const dismiss = useCallback( + (id) => { + console.log('onDismiss'); + service.current?.dismiss(id); + }, + [service], + ); + + const action = useCallback( + (id) => { + service.current?.action(id); + }, + [service], + ); + + return ( + + + {props.children} + + + ); +} + +/** + * @return {import("preact").RefObject} + */ +export function useService() { + const service = useRef(/** @type {SubscriptionWinBackBannerService|null} */ (null)); + const ntp = useMessaging(); + useEffect(() => { + const stats = new SubscriptionWinBackBannerService(ntp); + service.current = stats; + return () => { + stats.destroy(); + }; + }, [ntp]); + return service; +} diff --git a/special-pages/pages/new-tab/app/subscription-winback-banner/components/SubscriptionWinBackBanner.examples.js b/special-pages/pages/new-tab/app/subscription-winback-banner/components/SubscriptionWinBackBanner.examples.js new file mode 100644 index 0000000000..9deb1b57c7 --- /dev/null +++ b/special-pages/pages/new-tab/app/subscription-winback-banner/components/SubscriptionWinBackBanner.examples.js @@ -0,0 +1,18 @@ +import { h } from 'preact'; +import { noop } from '../../utils.js'; +import { SubscriptionWinBackBanner } from './SubscriptionWinBackBanner.js'; +import { subscriptionWinBackBannerDataExamples } from '../mocks/subscriptionWinBackBanner.data.js'; + +/** @type {Record import("preact").ComponentChild}>} */ + +export const subscriptionWinBackBannerExamples = { + 'subscriptionWinBackBanner.winback_last_day': { + factory: () => ( + + ), + }, +}; diff --git a/special-pages/pages/new-tab/app/subscription-winback-banner/components/SubscriptionWinBackBanner.js b/special-pages/pages/new-tab/app/subscription-winback-banner/components/SubscriptionWinBackBanner.js new file mode 100644 index 0000000000..4c635e5c5c --- /dev/null +++ b/special-pages/pages/new-tab/app/subscription-winback-banner/components/SubscriptionWinBackBanner.js @@ -0,0 +1,47 @@ +import cn from 'classnames'; +import { h } from 'preact'; +import { Button } from '../../../../../shared/components/Button/Button'; +import { DismissButton } from '../../components/DismissButton'; +import styles from './SubscriptionWinBackBanner.module.css'; +import { SubscriptionWinBackBannerContext } from '../SubscriptionWinBackBannerProvider'; +import { useContext } from 'preact/hooks'; +import { convertMarkdownToHTMLForStrongTags } from '../../../../../shared/utils'; + +/** + * @typedef { import("../../../types/new-tab").SubscriptionWinBackBannerMessage} SubscriptionWinBackBannerMessage + * @param {object} props + * @param {SubscriptionWinBackBannerMessage} props.message + * @param {(id: string) => void} props.dismiss + * @param {(id: string) => void} props.action + */ +export function SubscriptionWinBackBanner({ message, action, dismiss }) { + const processedMessageDescription = convertMarkdownToHTMLForStrongTags(message.descriptionText); + return ( +
+ + + +
+ {message.titleText &&

{message.titleText}

} +

+

+ {message.messageType === 'big_single_action' && message?.actionText && action && ( +
+ +
+ )} + {message.id && dismiss && dismiss(message.id)} />} +
+ ); +} + +export function SubscriptionWinBackBannerConsumer() { + const { state, action, dismiss } = useContext(SubscriptionWinBackBannerContext); + + if (state.status === 'ready' && state.data.content) { + return ; + } + return null; +} diff --git a/special-pages/pages/new-tab/app/subscription-winback-banner/components/SubscriptionWinBackBanner.module.css b/special-pages/pages/new-tab/app/subscription-winback-banner/components/SubscriptionWinBackBanner.module.css new file mode 100644 index 0000000000..75dd3ead8e --- /dev/null +++ b/special-pages/pages/new-tab/app/subscription-winback-banner/components/SubscriptionWinBackBanner.module.css @@ -0,0 +1,41 @@ +/** + * Using CSS Modules 'composes' to import styles from FreemiumPIRBanner. + * This avoids code duplication and keeps both components in sync. + * See: https://github.com/css-modules/css-modules/blob/master/docs/composition.md + */ + +.root { + composes: root from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css'; +} + +.iconBlock { + composes: iconBlock from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css'; +} + +.content { + composes: content from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css'; +} + +.title { + composes: title from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css'; +} + +.description { + composes: description from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css'; +} + +.btnBlock { + composes: btnBlock from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css'; +} + +.btnRow { + composes: btnRow from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css'; +} + +.dismissBtn { + composes: dismissBtn from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css'; +} + +.icon { + composes: icon from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css'; +} diff --git a/special-pages/pages/new-tab/app/subscription-winback-banner/integration-tests/subscription-winback-banner.spec.js b/special-pages/pages/new-tab/app/subscription-winback-banner/integration-tests/subscription-winback-banner.spec.js new file mode 100644 index 0000000000..e871345562 --- /dev/null +++ b/special-pages/pages/new-tab/app/subscription-winback-banner/integration-tests/subscription-winback-banner.spec.js @@ -0,0 +1,31 @@ +import { test, expect } from '@playwright/test'; +import { NewtabPage } from '../../../integration-tests/new-tab.page.js'; + +test.describe('newtab remote messaging framework subscriptionWinBackBanner', () => { + test('fetches config + data', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + await ntp.reducedMotion(); + await ntp.openPage({ winback: 'true' }); + + const calls1 = await ntp.mocks.waitForCallCount({ method: 'initialSetup', count: 1 }); + const calls2 = await ntp.mocks.waitForCallCount({ method: 'winBackOffer_getData', count: 1 }); + + expect(calls1.length).toBe(1); + expect(calls2.length).toBe(1); + }); + + test('renders a title, descriptionText an action button, and dismiss button', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + await ntp.reducedMotion(); + await ntp.openPage({ winback: 'true' }); + + await page.getByRole('heading', { name: 'Last day to save 25%!' }).waitFor(); + await page.getByText('Stay protected with our').waitFor(); + await page.locator('strong').waitFor(); + + await page.getByRole('button', { name: 'See Offer' }).click(); + await ntp.mocks.waitForCallCount({ method: 'winBackOffer_action', count: 1 }); + await page.getByTestId('dismissBtn').click(); + await ntp.mocks.waitForCallCount({ method: 'winBackOffer_dismiss', count: 1 }); + }); +}); diff --git a/special-pages/pages/new-tab/app/subscription-winback-banner/mocks/subscriptionWinBackBanner.data.js b/special-pages/pages/new-tab/app/subscription-winback-banner/mocks/subscriptionWinBackBanner.data.js new file mode 100644 index 0000000000..201596b1e5 --- /dev/null +++ b/special-pages/pages/new-tab/app/subscription-winback-banner/mocks/subscriptionWinBackBanner.data.js @@ -0,0 +1,15 @@ +/** + * @type {Record}>} + + */ +export const subscriptionWinBackBannerDataExamples = { + winback_last_day: { + content: { + messageType: 'big_single_action', + id: 'winback_last_day', + titleText: 'Last day to save 25%!', + descriptionText: 'Stay protected with our VPN, private AI, and more. Resubscribe today and save 25%. Limited time offer.', + actionText: 'See Offer', + }, + }, +}; diff --git a/special-pages/pages/new-tab/app/subscription-winback-banner/subscription-winback-banner.md b/special-pages/pages/new-tab/app/subscription-winback-banner/subscription-winback-banner.md new file mode 100644 index 0000000000..2067d2f1c3 --- /dev/null +++ b/special-pages/pages/new-tab/app/subscription-winback-banner/subscription-winback-banner.md @@ -0,0 +1,29 @@ +--- +title: Subscription Win-back Banner +--- + +## Requests: +- {@link "NewTab Messages".SubscriptionWinBackBannerGetDataRequest `winBackOffer_getData`} + - Used to fetch the initial data (during the first render) + - returns {@link "NewTab Messages".SubscriptionWinBackBannerData} + +## Subscriptions: +- {@link "NewTab Messages".SubscriptionWinBackBannerOnDataUpdateSubscription `winBackOffer_onDataUpdate`}. + - The messages available for the platform + - returns {@link "NewTab Messages".SubscriptionWinBackBannerData} + +## Notifications: +- {@link "NewTab Messages".SubscriptionWinBackBannerActionNotification `winBackOffer_action`} + - Sent when the user clicks the action button + - sends {@link "NewTab Messages".SubscriptionWinBackBannerAction} + - example payload: + ```json + { + "id": "winback_last_day" + } + ``` + +## Examples: + +The following examples show the data types in JSON format: +[messages/new-tab/examples/stats.js](../../messages/examples/subscriptionWinBackBanner.js) diff --git a/special-pages/pages/new-tab/app/subscription-winback-banner/subscriptionWinBackBanner.service.js b/special-pages/pages/new-tab/app/subscription-winback-banner/subscriptionWinBackBanner.service.js new file mode 100644 index 0000000000..5895c4e8f0 --- /dev/null +++ b/special-pages/pages/new-tab/app/subscription-winback-banner/subscriptionWinBackBanner.service.js @@ -0,0 +1,61 @@ +/** + * @typedef {import("../../types/new-tab.js").SubscriptionWinBackBannerData} SubscriptionWinBackBannerData + */ +import { Service } from '../service.js'; + +export class SubscriptionWinBackBannerService { + /** + * @param {import("../../src/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method. + * @internal + */ + constructor(ntp) { + this.ntp = ntp; + /** @type {Service} */ + this.dataService = new Service({ + initial: () => ntp.messaging.request('winBackOffer_getData'), + subscribe: (cb) => ntp.messaging.subscribe('winBackOffer_onDataUpdate', cb), + }); + } + + name() { + return 'SubscriptionWinBackBannerService'; + } + + /** + * @returns {Promise} + * @internal + */ + async getInitial() { + return await this.dataService.fetchInitial(); + } + + /** + * @internal + */ + destroy() { + this.dataService.destroy(); + } + + /** + * @param {(evt: {data: SubscriptionWinBackBannerData, source: 'manual' | 'subscription'}) => void} cb + * @internal + */ + onData(cb) { + return this.dataService.onData(cb); + } + + /** + * @param {string} id + * @internal + */ + dismiss(id) { + return this.ntp.messaging.notify('winBackOffer_dismiss', { id }); + } + + /** + * @param {string} id + */ + action(id) { + this.ntp.messaging.notify('winBackOffer_action', { id }); + } +} diff --git a/special-pages/pages/new-tab/public/icons/Subscription-Clock-96.svg b/special-pages/pages/new-tab/public/icons/Subscription-Clock-96.svg new file mode 100644 index 0000000000..6e1895dd25 --- /dev/null +++ b/special-pages/pages/new-tab/public/icons/Subscription-Clock-96.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 4c1752959f5b5aa7181a7348937aa1d57b1a54a1 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 15 Oct 2025 18:19:19 -0400 Subject: [PATCH 04/15] Wire up mock transport --- .../pages/new-tab/app/components/Examples.jsx | 2 + .../pages/new-tab/app/mock-transport.js | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/special-pages/pages/new-tab/app/components/Examples.jsx b/special-pages/pages/new-tab/app/components/Examples.jsx index c23339718d..724a125cb5 100644 --- a/special-pages/pages/new-tab/app/components/Examples.jsx +++ b/special-pages/pages/new-tab/app/components/Examples.jsx @@ -7,6 +7,7 @@ import { otherRMFExamples, RMFExamples } from '../remote-messaging-framework/com import { updateNotificationExamples } from '../update-notification/components/UpdateNotification.examples.js'; import { activityExamples } from '../activity/components/Activity.examples.js'; import { protectionsHeadingExamples } from '../protections/components/ProtectionsHeading.examples.js'; +import { subscriptionWinBackBannerExamples } from '../subscription-winback-banner/components/SubscriptionWinBackBanner.examples.js'; /** @type {Record import("preact").ComponentChild}>} */ export const mainExamples = { @@ -15,6 +16,7 @@ export const mainExamples = { ...nextStepsExamples, ...privacyStatsExamples, ...RMFExamples, + ...subscriptionWinBackBannerExamples, }; export const otherExamples = { diff --git a/special-pages/pages/new-tab/app/mock-transport.js b/special-pages/pages/new-tab/app/mock-transport.js index 9c04d5fc36..3c4f88a8d5 100644 --- a/special-pages/pages/new-tab/app/mock-transport.js +++ b/special-pages/pages/new-tab/app/mock-transport.js @@ -7,6 +7,7 @@ import { updateNotificationExamples } from './update-notification/mocks/update-n import { variants as nextSteps } from './next-steps/nextsteps.data.js'; import { customizerData, customizerMockTransport } from './customizer/mocks.js'; import { freemiumPIRDataExamples } from './freemium-pir-banner/mocks/freemiumPIRBanner.data.js'; +import { subscriptionWinBackBannerDataExamples } from './subscription-winback-banner/mocks/subscriptionWinBackBanner.data.js'; import { activityMockTransport } from './activity/mocks/activity.mock-transport.js'; import { protectionsMockTransport } from './protections/mocks/protections.mock-transport.js'; import { omnibarMockTransport } from './omnibar/mocks/omnibar.mock-transport.js'; @@ -95,6 +96,7 @@ export function mockTransport() { const rmfSubscriptions = new Map(); const freemiumPIRBannerSubscriptions = new Map(); const nextStepsSubscriptions = new Map(); + const subscriptionWinBackBannerSubscriptions = new Map(); function clearRmf() { const listeners = rmfSubscriptions.get('rmf_onDataUpdate') || []; @@ -163,6 +165,14 @@ export function mockTransport() { console.log('ignoring freemiumPIRBanner_dismiss', msg.params); return; } + case 'winBackOffer_action': { + console.log('ignoring winBackOffer_action', msg.params); + return; + } + case 'winBackOffer_dismiss': { + console.log('ignoring winBackOffer_dismiss', msg.params); + return; + } case 'favorites_setConfig': { if (!msg.params) throw new Error('unreachable'); @@ -256,6 +266,24 @@ export function mockTransport() { } return () => {}; } + case 'winBackOffer_onDataUpdate': { + // store the callback for later (eg: dismiss) + const prev = subscriptionWinBackBannerSubscriptions.get('winBackOffer_onDataUpdate') || []; + const next = [...prev]; + next.push(cb); + subscriptionWinBackBannerSubscriptions.set('winBackOffer_onDataUpdate', next); + + const subscriptionWinBackBannerParam = url.searchParams.get('winback'); + + if ( + subscriptionWinBackBannerParam !== null && + subscriptionWinBackBannerParam in subscriptionWinBackBannerDataExamples + ) { + const message = subscriptionWinBackBannerDataExamples[subscriptionWinBackBannerParam]; + cb(message); + } + return () => {}; + } case 'nextSteps_onDataUpdate': { const prev = nextStepsSubscriptions.get('nextSteps_onDataUpdate') || []; const next = [...prev]; @@ -471,6 +499,18 @@ export function mockTransport() { return Promise.resolve(freemiumPIRBannerMessage); } + case 'winBackOffer_getData': { + /** @type {import('../types/new-tab.ts').SubscriptionWinBackBannerData} */ + let subscriptionWinBackBannerMessage = { content: null }; + + const subscriptionWinBackBannerParam = url.searchParams.get('winback'); + + if (subscriptionWinBackBannerParam && subscriptionWinBackBannerParam in subscriptionWinBackBannerDataExamples) { + subscriptionWinBackBannerMessage = subscriptionWinBackBannerDataExamples[subscriptionWinBackBannerParam]; + } + + return Promise.resolve(subscriptionWinBackBannerMessage); + } case 'favorites_getData': { const param = url.searchParams.get('favorites'); let data; @@ -514,6 +554,7 @@ export function initialSetup(url) { { id: 'updateNotification' }, { id: 'rmf' }, { id: 'freemiumPIRBanner' }, + { id: 'subscriptionWinBackBanner' }, { id: 'nextSteps' }, { id: 'favorites' }, ]; From aa0d03aaa5606526ad1ac9f34b8c91a28e027979 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 15 Oct 2025 18:36:52 -0400 Subject: [PATCH 05/15] Generate types --- special-pages/pages/new-tab/types/new-tab.ts | 54 ++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/special-pages/pages/new-tab/types/new-tab.ts b/special-pages/pages/new-tab/types/new-tab.ts index a84be137d9..41205f9521 100644 --- a/special-pages/pages/new-tab/types/new-tab.ts +++ b/special-pages/pages/new-tab/types/new-tab.ts @@ -140,7 +140,9 @@ export interface NewTabMessages { | StatsShowMoreNotification | TelemetryEventNotification | UpdateNotificationDismissNotification - | WidgetsSetConfigNotification; + | WidgetsSetConfigNotification + | WinBackOfferActionNotification + | WinBackOfferDismissNotification; requests: | ActivityConfirmBurnRequest | ActivityGetDataRequest @@ -157,7 +159,8 @@ export interface NewTabMessages { | ProtectionsGetConfigRequest | ProtectionsGetDataRequest | RmfGetDataRequest - | StatsGetDataRequest; + | StatsGetDataRequest + | WinBackOfferGetDataRequest; subscriptions: | ActivityOnBurnCompleteSubscription | ActivityOnDataPatchSubscription @@ -180,7 +183,8 @@ export interface NewTabMessages { | StatsOnDataUpdateSubscription | TabsOnDataUpdateSubscription | UpdateNotificationOnDataUpdateSubscription - | WidgetsOnConfigUpdatedSubscription; + | WidgetsOnConfigUpdatedSubscription + | WinBackOfferOnDataUpdateSubscription; } /** * Generated from @see "../messages/activity_addFavorite.notify.json" @@ -672,6 +676,26 @@ export interface WidgetConfigItem { id: string; visibility: WidgetVisibility; } +/** + * Generated from @see "../messages/winBackOffer_action.notify.json" + */ +export interface WinBackOfferActionNotification { + method: "winBackOffer_action"; + params: SubscriptionWinBackBannerAction; +} +export interface SubscriptionWinBackBannerAction { + id: string; +} +/** + * Generated from @see "../messages/winBackOffer_dismiss.notify.json" + */ +export interface WinBackOfferDismissNotification { + method: "winBackOffer_dismiss"; + params: SubscriptionWinBackBannerDismissAction; +} +export interface SubscriptionWinBackBannerDismissAction { + id: string; +} /** * Generated from @see "../messages/activity_confirmBurn.request.json" */ @@ -1001,6 +1025,23 @@ export interface TrackerCompany { displayName: string; count: number; } +/** + * Generated from @see "../messages/winBackOffer_getData.request.json" + */ +export interface WinBackOfferGetDataRequest { + method: "winBackOffer_getData"; + result: SubscriptionWinBackBannerData; +} +export interface SubscriptionWinBackBannerData { + content: null | SubscriptionWinBackBannerMessage; +} +export interface SubscriptionWinBackBannerMessage { + messageType: "big_single_action"; + id: "winback_last_day"; + titleText: string | null; + descriptionText: string; + actionText: string; +} /** * Generated from @see "../messages/activity_onBurnComplete.subscribe.json" */ @@ -1174,6 +1215,13 @@ export interface WidgetsOnConfigUpdatedSubscription { subscriptionEvent: "widgets_onConfigUpdated"; params: WidgetConfigs; } +/** + * Generated from @see "../messages/winBackOffer_onDataUpdate.subscribe.json" + */ +export interface WinBackOfferOnDataUpdateSubscription { + subscriptionEvent: "winBackOffer_onDataUpdate"; + params: SubscriptionWinBackBannerData; +} declare module "../src/index.js" { export interface NewTabPage { From 6a5ddf0182241ddaf99d8c3cd1a16b97b42b86ce Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 15 Oct 2025 18:37:38 -0400 Subject: [PATCH 06/15] Wire up integration test for win-back banner on NTP --- .../pages/new-tab/integration-tests/new-tab.page.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/special-pages/pages/new-tab/integration-tests/new-tab.page.js b/special-pages/pages/new-tab/integration-tests/new-tab.page.js index 7cb99dd2bb..6cd6bdb389 100644 --- a/special-pages/pages/new-tab/integration-tests/new-tab.page.js +++ b/special-pages/pages/new-tab/integration-tests/new-tab.page.js @@ -71,6 +71,7 @@ export class NewtabPage { * @param {string} [params.updateNotification] - Optional flag to point to display=components view with certain rmf example visible * @param {string} [params.pir] - Optional flag to add certain Freemium PIR Banner example * @param {string} [params.platformName] - Optional parameters for opening the page. + * @param {string} [params.winback] - Optional parameters for Subscription Win-back Banner. */ async openPage({ mode = 'debug', @@ -82,6 +83,7 @@ export class NewtabPage { rmf, pir, updateNotification, + winback, } = {}) { await this.mocks.install(); const searchParams = new URLSearchParams({ mode, willThrow: String(willThrow) }); @@ -108,6 +110,10 @@ export class NewtabPage { searchParams.set('pir', pir); } + if (winback !== undefined) { + searchParams.set('winback', winback); + } + if (platformName !== undefined) { searchParams.set('platform', platformName); } From b3f48cf216e2ce3116be68ea368e2835f8ca2758 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 15 Oct 2025 18:41:05 -0400 Subject: [PATCH 07/15] Update button styles --- .../components/Button/Button.module.css | 650 ++++++++++-------- 1 file changed, 370 insertions(+), 280 deletions(-) diff --git a/special-pages/shared/components/Button/Button.module.css b/special-pages/shared/components/Button/Button.module.css index 2e48704942..9267400c5c 100644 --- a/special-pages/shared/components/Button/Button.module.css +++ b/special-pages/shared/components/Button/Button.module.css @@ -1,355 +1,445 @@ .button { - appearance: none; - background: var(--button-bg); - color: var(--button-text); - cursor: pointer; - position: relative; + appearance: none; + background: var(--button-bg); + color: var(--button-text); + cursor: pointer; + position: relative; } /* macOS Base Style */ [data-platform-name="macos"] .button { - border: 0; - border-radius: calc(5 * var(--px-in-rem)); - box-shadow: var(--button-shadow); - font-size: calc(13 * var(--px-in-rem)); + border: 0; + border-radius: calc(5 * var(--px-in-rem)); + box-shadow: var(--button-shadow); + font-size: calc(13 * var(--px-in-rem)); + height: var(--sp-5); + opacity: var(--button-opacity); + padding: 0 var(--sp-3); + + &.md { height: var(--sp-5); - opacity: var(--button-opacity); - padding: 0 var(--sp-3); - - &.lg { - height: var(--sp-7); - border-radius: calc(6 * var(--px-in-rem)); - } - - &.xl { - height: var(--sp-8); - border-radius: calc(6 * var(--px-in-rem)); - } + border-radius: calc(6 * var(--px-in-rem)); + } - &:disabled { - background: var(--button-bg--disabled, var(--button-bg)); - box-shadow: var(--button-shadow--disabled, var(--button-shadow)); - color: var(--button-text--disabled, var(--button-text)); - opacity: var(--button-opacity--disabled, var(--button-opacity)); + &.lg { + height: var(--sp-7); + border-radius: calc(6 * var(--px-in-rem)); + } - &:hover { - background: var(--button-bg--disabled, var(--button-bg)); - box-shadow: var(--button-shadow--disabled, var(--button-shadow)); - color: var(--button-text--disabled, var(--button-text)); - opacity: var(--button-opacity--disabled, var(--button-opacity)); - } - } + &.xl { + height: var(--sp-8); + border-radius: calc(6 * var(--px-in-rem)); + } - &:focus, - &:focus-visible { - background: var(--button-bg--focus, var(--button-bg)); - box-shadow: var(--button-shadow--focus, var(--button-shadow)); - color: var(--button-text--focus, var(--button-text)); - opacity: var(--button-opacity--focus, var(--button-opacity)); - } + &:disabled { + background: var(--button-bg--disabled, var(--button-bg)); + box-shadow: var(--button-shadow--disabled, var(--button-shadow)); + color: var(--button-text--disabled, var(--button-text)); + opacity: var(--button-opacity--disabled, var(--button-opacity)); &:hover { - background: var(--button-bg); - box-shadow: var(--button-shadow--hover, var(--button-shadow)); - color: var(--button-text--hover, var(--button-text)); - opacity: var(--button-opacity--hover, var(--button-opacity)); + background: var(--button-bg--disabled, var(--button-bg)); + box-shadow: var(--button-shadow--disabled, var(--button-shadow)); + color: var(--button-text--disabled, var(--button-text)); + opacity: var(--button-opacity--disabled, var(--button-opacity)); } + } - &:active { - background: var(--button-bg--active, var(--button-bg)); - box-shadow: var(--button-shadow--active, var(--button-shadow)); - color: var(--button-text--active, var(--button-text)); - opacity: var(--button-opacity--active, var(--button-opacity)); - } + &:focus, + &:focus-visible { + background: var(--button-bg--focus, var(--button-bg)); + box-shadow: var(--button-shadow--focus, var(--button-shadow)); + color: var(--button-text--focus, var(--button-text)); + opacity: var(--button-opacity--focus, var(--button-opacity)); + } + + &:hover { + background: var(--button-bg); + box-shadow: var(--button-shadow--hover, var(--button-shadow)); + color: var(--button-text--hover, var(--button-text)); + opacity: var(--button-opacity--hover, var(--button-opacity)); + } + + &:active { + background: var(--button-bg--active, var(--button-bg)); + box-shadow: var(--button-shadow--active, var(--button-shadow)); + color: var(--button-text--active, var(--button-text)); + opacity: var(--button-opacity--active, var(--button-opacity)); + } } /* iOS Base Style */ [data-platform-name="ios"] .button { - border-radius: var(--sp-2); - border: 0; - font-size: calc(15 * var(--px-in-rem)); - font-weight: 600; - height: calc(50 * var(--px-in-rem)); - letter-spacing: calc(-0.23 * var(--px-in-rem)); - padding: 0 var(--sp-6); - text-align: center; - - &:active { - background: var(--button-bg--active, var(--button-bg)); - color: var(--button-text--active, var(--button-text)); - } - - &:disabled { - background: var(--button-bg--disabled, var(--button-bg)); - color: var(--button-text--disabled, var(--button-text)); - } + border-radius: var(--sp-2); + border: 0; + font-size: calc(15 * var(--px-in-rem)); + font-weight: 600; + height: calc(50 * var(--px-in-rem)); + letter-spacing: calc(-0.23 * var(--px-in-rem)); + padding: 0 var(--sp-6); + text-align: center; + + &:active { + background: var(--button-bg--active, var(--button-bg)); + color: var(--button-text--active, var(--button-text)); + } + + &:disabled { + background: var(--button-bg--disabled, var(--button-bg)); + color: var(--button-text--disabled, var(--button-text)); + } } /* Backward-compatible styles for use when no platform info available */ body:not([data-platform-name]) { - & .button { - background-blend-mode: normal, color-burn, normal; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0) 100%), linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.06) 100%), #007aff; - - border-radius: var(--sp-2); - border: 1px solid rgba(40, 145, 255, 0.05); - box-shadow: 0 0 1px 0 rgba(40, 145, 255, 0.05), 0 1px 1px 0 rgba(40, 145, 255, 0.1); - color: white; - font-size: calc(13 * var(--px-in-rem)); - font-weight: 600; - line-height: var(--sp-8); - padding: 0 var(--sp-4); + & .button { + background-blend-mode: normal, color-burn, normal; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0) 100%), linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.06) 100%), #007aff; - &:hover { - background: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2749db; - } + border-radius: var(--sp-2); + border: 1px solid rgba(40, 145, 255, 0.05); + box-shadow: 0 0 1px 0 rgba(40, 145, 255, 0.05), 0 1px 1px 0 rgba(40, 145, 255, 0.1); + color: white; + font-size: calc(13 * var(--px-in-rem)); + font-weight: 600; + line-height: var(--sp-8); + padding: 0 var(--sp-4); - &:active { - background: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #1743d1; - } + &:hover { + background: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2749db; } + + &:active { + background: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #1743d1; + } + } } /* macOs Design System Button Tokens */ [data-platform-name="macos"] { - /* Shared among all variants */ - --macos-control-focused-shadow: 0 0 0 3px rgba(57, 105, 239, 0.55), 0 0 0 1px rgba(57, 105, 239, 0.55) inset, 0 0 1px 0 rgba(0, 0, 0, 0.05), 0 1px 1px 0 rgba(0, 0, 0, 0.1); - - /* Standard button variant */ - --macos-control-standard-background-rest: var(--color-white); - --macos-control-standard-background-rest--dark: rgba(255, 255, 255, 0.28); - --macos-control-standard-background-pressed: #e7e7e7; - --macos-control-standard-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 1px 0 0 rgba(255, 255, 255, 0.05) inset, 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 1px 0 rgba(0, 0, 0, 0.05), 0 1px 1px 0 rgba(0, 0, 0, 0.2); - - /* Accent Branded variant */ - --macos-control-accent-branded-background-rest: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2749DB; - --macos-control-accent-branded-background-pressed: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2140C0; - --macos-control-accent-branded-background-hover: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2749DB; - --macos-control-accent-branded-background-disabled: var(--color-white); - --macos-control-accent-branded-background-focus: linear-gradient(0deg, rgba(255, 255, 255, 0.00) 0%, rgba(255, 255, 255, 0.16) 100%), #2749DB; - - --macos-control-accent-branded-shadow-rest: 0 1px 0 0 rgba(255, 255, 255, 0) inset, 0 1px 0 0 rgba(255, 255, 255, 0) inset, 0 0 0 1px rgba(0, 122, 255, 0.05), 0 0 1px 0 rgba(0, 122, 255, 0.05), 0 1px 1px 0 rgba(0, 122, 255, 0.1); - --macos-control-accent-branded-shadow-pressed: 0px 0.5px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 0px 0px 0.5px rgba(0, 122, 255, 0.05), 0px 0px 1px 0px rgba(0, 122, 255, 0.05), 0px 1px 1px 0px rgba(0, 122, 255, 0.10); - --macos-control-accent-branded-shadow-hover: 0px 0.5px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 0px 0px 0.5px rgba(0, 122, 255, 0.05), 0px 0px 1px 0px rgba(0, 122, 255, 0.05), 0px 1px 1px 0px rgba(0, 122, 255, 0.10); - --macos-control-accent-branded-shadow-disabled: 0px 0.5px 0px 0px rgba(255, 255, 255, 0.20) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.05) inset, 0px 0px 0px 0.5px rgba(0, 0, 0, 0.10), 0px 0px 1px 0px rgba(0, 0, 0, 0.05), 0px 1px 1px 0px rgba(0, 0, 0, 0.20); + /* Shared among all variants */ + --macos-control-focused-shadow: 0 0 0 3px rgba(57, 105, 239, 0.55), 0 0 0 1px rgba(57, 105, 239, 0.55) inset, 0 0 1px 0 rgba(0, 0, 0, 0.05), 0 1px 1px 0 rgba(0, 0, 0, 0.1); + + /* Standard button variant */ + --macos-control-standard-background-rest: var(--color-white); + --macos-control-standard-background-rest--dark: rgba(255, 255, 255, 0.28); + --macos-control-standard-background-pressed: #e7e7e7; + --macos-control-standard-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 1px 0 0 rgba(255, 255, 255, 0.05) inset, 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 1px 0 rgba(0, 0, 0, 0.05), 0 1px 1px 0 rgba(0, 0, 0, 0.2); + + /* Accent variant */ + --macos-control-accent-background-rest: + linear-gradient(0deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.08) 100%), + linear-gradient(0deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.08) 100%), + linear-gradient(0deg, + rgba(255, 255, 255, 0.01) 0%, + rgba(255, 255, 255, 0.08) 100%), + #3478f6; + --macos-control-accent-background-hover: + linear-gradient(0deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.08) 100%), + linear-gradient(0deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.08) 100%), + linear-gradient(0deg, + rgba(255, 255, 255, 0.01) 0%, + rgba(255, 255, 255, 0.08) 100%), + #3478f6; + --macos-control-accent-background-pressed: + linear-gradient(0deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.2) 100%), + #0167eb; + --macos-control-accent-background-focus: + linear-gradient(0deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.08) 100%), + linear-gradient(0deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.08) 100%), + linear-gradient(0deg, + rgba(255, 255, 255, 0.01) 0%, + rgba(255, 255, 255, 0.08) 100%), + #3478f6; + --macos-control-accent-shadow-rest: + 0 1px 0 0 rgba(255, 255, 255, 0) inset, + 0 1px 0 0 rgba(255, 255, 255, 0) inset, 0 0 0 1px rgba(0, 122, 255, 0.05), + 0 0 1px 0 rgba(0, 122, 255, 0.05), 0 1px 1px 0 rgba(0, 122, 255, 0.1); + --macos-control-accent-shadow-hover: + 0px 0.5px 0px 0px rgba(255, 255, 255, 0) inset, + 0px 1px 0px 0px rgba(255, 255, 255, 0) inset, + 0px 0px 0px 0.5px rgba(0, 122, 255, 0.05), + 0px 0px 1px 0px rgba(0, 122, 255, 0.05), + 0px 1px 1px 0px rgba(0, 122, 255, 0.1); + --macos-control-accent-shadow-pressed: + 0px 0.5px 0px 0px rgba(255, 255, 255, 0) inset, + 0px 1px 0px 0px rgba(255, 255, 255, 0) inset, + 0px 0px 0px 0.5px rgba(0, 122, 255, 0.05), + 0px 0px 1px 0px rgba(0, 122, 255, 0.05), + 0px 1px 1px 0px rgba(0, 122, 255, 0.1); + + /* Accent Branded variant */ + --macos-control-accent-branded-background-rest: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2749DB; + --macos-control-accent-branded-background-pressed: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2140C0; + --macos-control-accent-branded-background-hover: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2749DB; + --macos-control-accent-branded-background-disabled: var(--color-white); + --macos-control-accent-branded-background-focus: linear-gradient(0deg, rgba(255, 255, 255, 0.00) 0%, rgba(255, 255, 255, 0.16) 100%), #2749DB; + + --macos-control-accent-branded-shadow-rest: 0 1px 0 0 rgba(255, 255, 255, 0) inset, 0 1px 0 0 rgba(255, 255, 255, 0) inset, 0 0 0 1px rgba(0, 122, 255, 0.05), 0 0 1px 0 rgba(0, 122, 255, 0.05), 0 1px 1px 0 rgba(0, 122, 255, 0.1); + --macos-control-accent-branded-shadow-pressed: 0px 0.5px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 0px 0px 0.5px rgba(0, 122, 255, 0.05), 0px 0px 1px 0px rgba(0, 122, 255, 0.05), 0px 1px 1px 0px rgba(0, 122, 255, 0.10); + --macos-control-accent-branded-shadow-hover: 0px 0.5px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 0px 0px 0.5px rgba(0, 122, 255, 0.05), 0px 0px 1px 0px rgba(0, 122, 255, 0.05), 0px 1px 1px 0px rgba(0, 122, 255, 0.10); + --macos-control-accent-branded-shadow-disabled: 0px 0.5px 0px 0px rgba(255, 255, 255, 0.20) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.05) inset, 0px 0px 0px 0.5px rgba(0, 0, 0, 0.10), 0px 0px 1px 0px rgba(0, 0, 0, 0.05), 0px 1px 1px 0px rgba(0, 0, 0, 0.20); } /* macOS Variables */ [data-platform-name="macos"] .button { + &.standard { + --button-bg: var(--macos-control-standard-background-rest); + --button-text: var(--macos-text-primary); + --button-shadow: var(--macos-control-standard-shadow); + --button-opacity: 1; + + /* Active */ + --button-bg--active: var(--macos-control-standard-background-pressed); + + /* Disabled */ + --button-bg--disabled: var(--macos-control-standard-background-rest); + --button-text--disabled: var(--macos-text-primary); + --button-shadow--disabled: var(--macos-control-standard-shadow); + --button-opacity--disabled: 0.4; + + /* Focus */ + --button-shadow--focus: var(--macos-control-focused-shadow); + + /* Hover */ + /* TODO: No difference on hover according to Figma(?) */ + } + + &.accent { + --button-bg: var(--macos-control-accent-background-rest); + --button-text: var(--color-white); + --button-shadow: var(--macos-control-accent-shadow-rest); + --button-opacity: 1; + + /* Active */ + --button-bg--active: var(--macos-control-accent-background-pressed); + --button-text--active: var(--color-white-at-80); + --button-shadow--active: var(--macos-control-accent-shadow-pressed); + + /* Disabled */ + --button-bg--disabled: var(--macos-control-standard-background-rest); + --button-text--disabled: var(--macos-text-primary); + --button-shadow--disabled: var(--macos-control-standard-shadow); + --button-opacity--disabled: 0.4; + + /* Focus */ + --button-bg--focus: var(--macos-control-accent-background-focus); + --button-text--focus: var(--color-white); + --button-shadow--focus: var(--macos-control-focused-shadow); + + /* Hover */ + --button-bg--hover: var(--macos-control-accent-background-hover); + --button-text--hover: var(--color-white); + --button-shadow--hover: var(--macos-control-accent-shadow-hover); + } + + &.accentBrand { + --button-bg: var(--macos-control-accent-branded-background-rest); + --button-text: var(--color-white); + --button-shadow: var(--macos-control-accent-branded-shadow-rest); + --button-opacity: 1; + + /* Active */ + --button-bg--active: var(--macos-control-accent-branded-background-pressed); + --button-text--active: var(--color-white-at-80); + --button-shadow--active: var(--macos-control-accent-branded-shadow-pressed); + + /* Disabled */ + --button-bg--disabled: var(--macos-control-standard-background-rest); + --button-text--disabled: var(--macos-text-primary); + --button-shadow--disabled: var(--macos-control-standard-shadow); + --button-opacity--disabled: 0.4; + + /* Focus */ + --button-bg--focus: var(--macos-control-accent-branded-background-focus); + --button-text--focus: var(--color-white); + --button-shadow--focus: var(--macos-control-focused-shadow); + + /* Hover */ + --button-bg--hover: var(--macos-control-accent-branded-background-hover); + --button-text--hover: var(--color-white); + --button-shadow--hover: var(--macos-control-accent-branded-shadow-hover); + } + + [data-theme=dark] & { &.standard { - --button-bg: var(--macos-control-standard-background-rest); - --button-text: var(--macos-text-primary); - --button-shadow: var(--macos-control-standard-shadow); - --button-opacity: 1; - - /* Active */ - --button-bg--active: var(--macos-control-standard-background-pressed); - - /* Disabled */ - --button-bg--disabled: var(--macos-control-standard-background-rest); - --button-text--disabled: var(--macos-text-primary); - --button-shadow--disabled: var(--macos-control-standard-shadow); - --button-opacity--disabled: 0.4; - - /* Focus */ - --button-shadow--focus: var(--macos-control-focused-shadow); - - /* Hover */ - /* TODO: No difference on hover according to Figma(?) */ - } - - &.accent, - &.accentBrand { - --button-bg: var(--macos-control-accent-branded-background-rest); - --button-text: var(--color-white); - --button-shadow: var(--macos-control-accent-branded-shadow-rest); - --button-opacity: 1; - - /* Active */ - --button-bg--active: var(--macos-control-accent-branded-background-pressed); - --button-text--active: var(--color-white-at-80); - --button-shadow--active: var(--macos-control-accent-branded-shadow-pressed); - - /* Disabled */ - --button-bg--disabled: var(--macos-control-standard-background-rest); - --button-text--disabled: var(--macos-text-primary); - --button-shadow--disabled: var(--macos-control-standard-shadow); - --button-opacity--disabled: 0.4; - - /* Focus */ - --button-bg--focus: var(--macos-control-accent-branded-background-focus); - --button-text--focus: var(--color-white); - --button-shadow--focus: var(--macos-control-focused-shadow); - - /* Hover */ - --button-bg--hover: var(--macos-control-accent-branded-background-hover); - --button-text--hover: var(--color-white); - --button-shadow--hover: var(--macos-control-accent-branded-shadow-hover); + --button-bg: var(--macos-control-standard-background-rest--dark); + --button-text: var(--color-white-at-84); } - [data-theme=dark] & { - &.standard { - --button-bg: var(--macos-control-standard-background-rest--dark); - --button-text: var(--color-white-at-84); - } - - /** TODO: Confirm that colors don't change in Dark Mode for accented macOS button */ - } + /** TODO: Confirm that colors don't change in Dark Mode for accented macOS button */ + } } /* iOS Variables */ [data-platform-name="ios"] .button { - &.primary { - --button-bg: var(--color-blue-50); - --button-text: var(--color-white); + &.primary { + --button-bg: var(--color-blue-50); + --button-text: var(--color-white); - /* Active */ - --button-bg--active: var(--color-blue-70); - --button-text--active: var(--color-white); + /* Active */ + --button-bg--active: var(--color-blue-70); + --button-text--active: var(--color-white); - /* Disabled */ - --button-bg--disabled: var(--color-black-at-6); - --button-text--disabled: var(--color-black-at-36); - } + /* Disabled */ + --button-bg--disabled: var(--color-black-at-6); + --button-text--disabled: var(--color-black-at-36); + } - &.ghost { - --button-bg: transparent; - --button-text: var(--color-blue-50); + &.ghost { + --button-bg: transparent; + --button-text: var(--color-blue-50); - /* Active */ - --button-bg--active: rgba(57, 105, 239, 0.12); - --button-text--active: var(--color-blue-70); + /* Active */ + --button-bg--active: rgba(57, 105, 239, 0.12); + --button-text--active: var(--color-blue-70); - /* Disabled */ - --button-bg--disabled: transparent; - --button-text--disabled: var(--color-black-at-36); - } + /* Disabled */ + --button-bg--disabled: transparent; + --button-text--disabled: var(--color-black-at-36); + } - [data-theme=dark] & { - &.primary { - --button-bg: var(--color-blue-30); - --button-text: var(--color-black-at-84); + [data-theme=dark] & { + &.primary { + --button-bg: var(--color-blue-30); + --button-text: var(--color-black-at-84); - /* Active */ - --button-bg--active: var(--color-blue-50); - --button-text--active: var(--color-black-at-84); + /* Active */ + --button-bg--active: var(--color-blue-50); + --button-text--active: var(--color-black-at-84); - /* Disabled */ - --button-bg--disabled: var(--color-black-at-6); - --button-text--disabled: var(--color-black-at-36); - } + /* Disabled */ + --button-bg--disabled: var(--color-black-at-6); + --button-text--disabled: var(--color-black-at-36); + } - &.ghost { - --button-bg: transparent; - --button-text: var(--color-blue-30); + &.ghost { + --button-bg: transparent; + --button-text: var(--color-blue-30); - /* Active */ - --button-bg--active: rgba(114, 149, 246, 0.2); - --button-text--active: var(--color-blue-20); + /* Active */ + --button-bg--active: rgba(114, 149, 246, 0.2); + --button-text--active: var(--color-blue-20); - /* Disabled */ - --button-bg--disabled: transparent; - --button-text--disabled: var(--color-black-at-36); - } + /* Disabled */ + --button-bg--disabled: transparent; + --button-text--disabled: var(--color-black-at-36); } + } } /* Windows Implementation */ [data-platform-name="windows"] { - .button { - /* this is the focus ring used on NTP widgets */ - --focus-ring: 0px 0px 0px 1px var(--color-white), 0px 0px 0px 3px var(--color-black); - - border-radius: var(--border-radius-sm); - height: var(--sp-8); - border-width: 0; - padding-left: var(--sp-3); - padding-right: var(--sp-3); - - &:focus-visible { - outline: none; - box-shadow: var(--focus-ring); - } - - &.standard { - background-color: var(--color-black-at-6); - border-width: 0; + .button { + /* this is the focus ring used on NTP widgets */ + --focus-ring: 0px 0px 0px 1px var(--color-white), 0px 0px 0px 3px var(--color-black); - &:hover { - background-color: var(--color-black-at-9); - cursor: pointer; - } + border-radius: var(--border-radius-sm); + height: var(--sp-8); + border-width: 0; + padding-left: var(--sp-3); + padding-right: var(--sp-3); - &:active { - background-color: var(--color-black-at-12); - } - - &:disabled { - color: var(--color-black-at-84); - } + &:focus-visible { + outline: none; + box-shadow: var(--focus-ring); + } - &:disabled:hover { - cursor: not-allowed; - background-color: var(--color-white-at-6); - } - } + &.standard { + background-color: var(--color-black-at-6); + border-width: 0; + + &:hover { + background-color: var(--color-black-at-9); + cursor: pointer; + } + + &:active { + background-color: var(--color-black-at-12); + } + + &:disabled { + color: var(--color-black-at-84); + } + + &:disabled:hover { + cursor: not-allowed; + background-color: var(--color-white-at-6); + } + } - &.accentBrand { - background-color: var(--ddg-color-primary); - color: var(--color-white); + &.accent, + &.accentBrand { + background-color: var(--ddg-color-primary); + color: var(--color-white); - &:hover { - background-color: var(--color-blue-60); - } + &:hover { + background-color: var(--color-blue-60); + } - &:active { - background-color: var(--color-blue-70); - } - } + &:active { + background-color: var(--color-blue-70); + } + } - [data-theme=dark] & { - --focus-ring: 0px 0px 0px 1px var(--color-black), 0px 0px 0px 3px var(--color-white); + [data-theme=dark] & { + --focus-ring: 0px 0px 0px 1px var(--color-black), 0px 0px 0px 3px var(--color-white); - &.standard { - color: var(--color-white-at-84); - background-color: var(--color-white-at-12); + &.standard { + color: var(--color-white-at-84); + background-color: var(--color-white-at-12); - &:hover { - background-color: var(--color-white-at-18); - } + &:hover { + background-color: var(--color-white-at-18); + } - &:active { - background-color: var(--color-white-at-24); - } + &:active { + background-color: var(--color-white-at-24); + } - &:disabled { - color: var(--color-white-at-12); - opacity: 0.8; - } + &:disabled { + color: var(--color-white-at-12); + opacity: 0.8; + } - &:disabled:hover { - background-color: var(--color-white-at-12); - } - } + &:disabled:hover { + background-color: var(--color-white-at-12); + } + } - &.accentBrand { - color: var(--color-black-at-84); - background-color: var(--color-blue-20); + &.accent, + &.accentBrand { + color: var(--color-black-at-84); + background-color: var(--color-blue-20); - &:hover { - background-color: var(--color-blue-30); - } + &:hover { + background-color: var(--color-blue-30); + } - &:active { - background-color: var(--color-blue-40); - } + &:active { + background-color: var(--color-blue-40); + } - &:disabled { - background-color: var(--color-white-at-36); - color: var(--color-black-at-84); - } - } + &:disabled { + background-color: var(--color-white-at-36); + color: var(--color-black-at-84); } + } } + } } From 4088c1e383f3dfc68261850488749ab22a67aa69 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 15 Oct 2025 18:58:37 -0400 Subject: [PATCH 08/15] Update new-tab readme --- special-pages/pages/new-tab/readme.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/special-pages/pages/new-tab/readme.md b/special-pages/pages/new-tab/readme.md index e4a0f90619..35d116338f 100644 --- a/special-pages/pages/new-tab/readme.md +++ b/special-pages/pages/new-tab/readme.md @@ -180,6 +180,12 @@ - `onboarding` - Shows onboarding PIR banner - `scan_results` - Shows scan results PIR banner +### Subscription Win-back Banner + - **Purpose**: Tests different win-back banner states + - **Parameter**: `winback` + - **Example**: `?winback=true` + - **Options**: + ## Combining Parameters From deba1e0dc1bb47f909ba97c80db86b92fda45300 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 15 Oct 2025 18:59:08 -0400 Subject: [PATCH 09/15] Add entry point for Subscription Win-back banner --- .../app/entry-points/subscriptionWinBackBanner.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 special-pages/pages/new-tab/app/entry-points/subscriptionWinBackBanner.js diff --git a/special-pages/pages/new-tab/app/entry-points/subscriptionWinBackBanner.js b/special-pages/pages/new-tab/app/entry-points/subscriptionWinBackBanner.js new file mode 100644 index 0000000000..d26c6b7713 --- /dev/null +++ b/special-pages/pages/new-tab/app/entry-points/subscriptionWinBackBanner.js @@ -0,0 +1,14 @@ +import { h } from 'preact'; +import { Centered } from '../components/Layout.js'; +import { SubscriptionWinBackBannerConsumer } from '../subscription-winback-banner/components/SubscriptionWinBackBanner.js'; +import { SubscriptionWinBackBannerProvider } from '../subscription-winback-banner/SubscriptionWinBackBannerProvider.js'; + +export function factory() { + return ( + + + + + + ); +} From bfe66234c492028e5a823a0c3924ad33375a1f6e Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 15 Oct 2025 19:12:04 -0400 Subject: [PATCH 10/15] Resolve Unknown property `composes` --- .stylelintrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stylelintrc.json b/.stylelintrc.json index 8b092df8e4..5aa6717403 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -4,7 +4,7 @@ "ignoreFiles": ["build/**/*.css", "Sources/**/*.css", "docs/**/*.css", "special-pages/pages/**/*/dist/*.css"], "rules": { "csstree/validator": { - "ignoreProperties": ["text-wrap", "view-transition-name"] + "ignoreProperties": ["text-wrap", "view-transition-name", "composes"] }, "alpha-value-notation": null, "at-rule-empty-line-before": null, From b474e6b328cba357e8c7b67a7b7c62349ddae5e4 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 15 Oct 2025 19:14:45 -0400 Subject: [PATCH 11/15] Resolve Unexpected unknown property "composes" property-no-unknown --- .stylelintrc.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.stylelintrc.json b/.stylelintrc.json index 5aa6717403..a825e898d0 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -6,6 +6,12 @@ "csstree/validator": { "ignoreProperties": ["text-wrap", "view-transition-name", "composes"] }, + "property-no-unknown": [ + true, + { + "ignoreProperties": ["composes"] + } + ], "alpha-value-notation": null, "at-rule-empty-line-before": null, "color-function-notation": null, From 8841f6058348795d3df05fe192d5b68bc95bd243 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Thu, 16 Oct 2025 12:12:40 -0400 Subject: [PATCH 12/15] Fix unit test issue --- special-pages/unit-test/utils.spec.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/special-pages/unit-test/utils.spec.mjs b/special-pages/unit-test/utils.spec.mjs index 1d856bf091..089e6b1139 100644 --- a/special-pages/unit-test/utils.spec.mjs +++ b/special-pages/unit-test/utils.spec.mjs @@ -1,6 +1,6 @@ import { describe, it } from 'node:test'; import { equal } from 'node:assert/strict'; -import { convertMarkdownToHTMLForStrongTags } from '../shared/utils'; +import { convertMarkdownToHTMLForStrongTags } from '../shared/utils.js'; describe('convertMarkdownToHTMLForStrongTags', () => { it('with terms wrapped in "**" will return with tags wrapping that part of the string', () => { From 44cdc7f613fbda9c2645d423a1bc1a15a336282d Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Thu, 16 Oct 2025 16:10:18 -0400 Subject: [PATCH 13/15] Patch: Update test-int-snapshots-update in injected/package.json --- injected/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/injected/package.json b/injected/package.json index 5f01338935..9ce0ca5d7b 100644 --- a/injected/package.json +++ b/injected/package.json @@ -15,7 +15,7 @@ "test-int": "playwright test --grep-invert '@screenshots'", "test-int-x": "xvfb-run --server-args='-screen 0 1024x768x24' npm run test-int", "test-int-snapshots": "playwright test --grep '@screenshots'", - "test-int-snapshots-update": "playwright test --grep '@screenshots' --update-snapshots --last-failed", + "test-int-snapshots-update": "playwright test --grep '@screenshots' --update-snapshots --last-failed --pass-with-no-tests", "test": "npm run test-unit && npm run test-int && npm run playwright", "serve": "http-server -c-1 --port 3220 integration-test/test-pages", "playwright": "playwright test --grep-invert '@screenshots'", From 20160657ab7c7eaff840a0df72c3a33d4a870abf Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Fri, 17 Oct 2025 01:00:17 -0400 Subject: [PATCH 14/15] Use accentBrand colors for MacOS button --- .../components/Button/Button.module.css | 656 ++++++++---------- 1 file changed, 284 insertions(+), 372 deletions(-) diff --git a/special-pages/shared/components/Button/Button.module.css b/special-pages/shared/components/Button/Button.module.css index 9267400c5c..cd43eccca4 100644 --- a/special-pages/shared/components/Button/Button.module.css +++ b/special-pages/shared/components/Button/Button.module.css @@ -1,445 +1,357 @@ .button { - appearance: none; - background: var(--button-bg); - color: var(--button-text); - cursor: pointer; - position: relative; + appearance: none; + background: var(--button-bg); + color: var(--button-text); + cursor: pointer; + position: relative; } /* macOS Base Style */ [data-platform-name="macos"] .button { - border: 0; - border-radius: calc(5 * var(--px-in-rem)); - box-shadow: var(--button-shadow); - font-size: calc(13 * var(--px-in-rem)); - height: var(--sp-5); - opacity: var(--button-opacity); - padding: 0 var(--sp-3); - - &.md { + border: 0; + border-radius: calc(5 * var(--px-in-rem)); + box-shadow: var(--button-shadow); + font-size: calc(13 * var(--px-in-rem)); height: var(--sp-5); - border-radius: calc(6 * var(--px-in-rem)); - } + opacity: var(--button-opacity); + padding: 0 var(--sp-3); - &.lg { - height: var(--sp-7); - border-radius: calc(6 * var(--px-in-rem)); - } + &.lg { + height: var(--sp-7); + border-radius: calc(6 * var(--px-in-rem)); + } - &.xl { - height: var(--sp-8); - border-radius: calc(6 * var(--px-in-rem)); - } + &.xl { + height: var(--sp-8); + border-radius: calc(6 * var(--px-in-rem)); + } - &:disabled { - background: var(--button-bg--disabled, var(--button-bg)); - box-shadow: var(--button-shadow--disabled, var(--button-shadow)); - color: var(--button-text--disabled, var(--button-text)); - opacity: var(--button-opacity--disabled, var(--button-opacity)); + &:disabled { + background: var(--button-bg--disabled, var(--button-bg)); + box-shadow: var(--button-shadow--disabled, var(--button-shadow)); + color: var(--button-text--disabled, var(--button-text)); + opacity: var(--button-opacity--disabled, var(--button-opacity)); - &:hover { - background: var(--button-bg--disabled, var(--button-bg)); - box-shadow: var(--button-shadow--disabled, var(--button-shadow)); - color: var(--button-text--disabled, var(--button-text)); - opacity: var(--button-opacity--disabled, var(--button-opacity)); + &:hover { + background: var(--button-bg--disabled, var(--button-bg)); + box-shadow: var(--button-shadow--disabled, var(--button-shadow)); + color: var(--button-text--disabled, var(--button-text)); + opacity: var(--button-opacity--disabled, var(--button-opacity)); + } } - } - &:focus, - &:focus-visible { - background: var(--button-bg--focus, var(--button-bg)); - box-shadow: var(--button-shadow--focus, var(--button-shadow)); - color: var(--button-text--focus, var(--button-text)); - opacity: var(--button-opacity--focus, var(--button-opacity)); - } + &:focus, + &:focus-visible { + background: var(--button-bg--focus, var(--button-bg)); + box-shadow: var(--button-shadow--focus, var(--button-shadow)); + color: var(--button-text--focus, var(--button-text)); + opacity: var(--button-opacity--focus, var(--button-opacity)); + } - &:hover { - background: var(--button-bg); - box-shadow: var(--button-shadow--hover, var(--button-shadow)); - color: var(--button-text--hover, var(--button-text)); - opacity: var(--button-opacity--hover, var(--button-opacity)); - } - - &:active { - background: var(--button-bg--active, var(--button-bg)); - box-shadow: var(--button-shadow--active, var(--button-shadow)); - color: var(--button-text--active, var(--button-text)); - opacity: var(--button-opacity--active, var(--button-opacity)); - } + &:hover { + background: var(--button-bg); + box-shadow: var(--button-shadow--hover, var(--button-shadow)); + color: var(--button-text--hover, var(--button-text)); + opacity: var(--button-opacity--hover, var(--button-opacity)); + } + + &:active { + background: var(--button-bg--active, var(--button-bg)); + box-shadow: var(--button-shadow--active, var(--button-shadow)); + color: var(--button-text--active, var(--button-text)); + opacity: var(--button-opacity--active, var(--button-opacity)); + } } /* iOS Base Style */ [data-platform-name="ios"] .button { - border-radius: var(--sp-2); - border: 0; - font-size: calc(15 * var(--px-in-rem)); - font-weight: 600; - height: calc(50 * var(--px-in-rem)); - letter-spacing: calc(-0.23 * var(--px-in-rem)); - padding: 0 var(--sp-6); - text-align: center; - - &:active { - background: var(--button-bg--active, var(--button-bg)); - color: var(--button-text--active, var(--button-text)); - } - - &:disabled { - background: var(--button-bg--disabled, var(--button-bg)); - color: var(--button-text--disabled, var(--button-text)); - } + border-radius: var(--sp-2); + border: 0; + font-size: calc(15 * var(--px-in-rem)); + font-weight: 600; + height: calc(50 * var(--px-in-rem)); + letter-spacing: calc(-0.23 * var(--px-in-rem)); + padding: 0 var(--sp-6); + text-align: center; + + &:active { + background: var(--button-bg--active, var(--button-bg)); + color: var(--button-text--active, var(--button-text)); + } + + &:disabled { + background: var(--button-bg--disabled, var(--button-bg)); + color: var(--button-text--disabled, var(--button-text)); + } } /* Backward-compatible styles for use when no platform info available */ body:not([data-platform-name]) { - & .button { - background-blend-mode: normal, color-burn, normal; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0) 100%), linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.06) 100%), #007aff; + & .button { + background-blend-mode: normal, color-burn, normal; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0) 100%), linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.06) 100%), #007aff; + + border-radius: var(--sp-2); + border: 1px solid rgba(40, 145, 255, 0.05); + box-shadow: 0 0 1px 0 rgba(40, 145, 255, 0.05), 0 1px 1px 0 rgba(40, 145, 255, 0.1); + color: white; + font-size: calc(13 * var(--px-in-rem)); + font-weight: 600; + line-height: var(--sp-8); + padding: 0 var(--sp-4); - border-radius: var(--sp-2); - border: 1px solid rgba(40, 145, 255, 0.05); - box-shadow: 0 0 1px 0 rgba(40, 145, 255, 0.05), 0 1px 1px 0 rgba(40, 145, 255, 0.1); - color: white; - font-size: calc(13 * var(--px-in-rem)); - font-weight: 600; - line-height: var(--sp-8); - padding: 0 var(--sp-4); - - &:hover { - background: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2749db; - } + &:hover { + background: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2749db; + } - &:active { - background: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #1743d1; + &:active { + background: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #1743d1; + } } - } } /* macOs Design System Button Tokens */ [data-platform-name="macos"] { - /* Shared among all variants */ - --macos-control-focused-shadow: 0 0 0 3px rgba(57, 105, 239, 0.55), 0 0 0 1px rgba(57, 105, 239, 0.55) inset, 0 0 1px 0 rgba(0, 0, 0, 0.05), 0 1px 1px 0 rgba(0, 0, 0, 0.1); - - /* Standard button variant */ - --macos-control-standard-background-rest: var(--color-white); - --macos-control-standard-background-rest--dark: rgba(255, 255, 255, 0.28); - --macos-control-standard-background-pressed: #e7e7e7; - --macos-control-standard-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 1px 0 0 rgba(255, 255, 255, 0.05) inset, 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 1px 0 rgba(0, 0, 0, 0.05), 0 1px 1px 0 rgba(0, 0, 0, 0.2); - - /* Accent variant */ - --macos-control-accent-background-rest: - linear-gradient(0deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0.08) 100%), - linear-gradient(0deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0.08) 100%), - linear-gradient(0deg, - rgba(255, 255, 255, 0.01) 0%, - rgba(255, 255, 255, 0.08) 100%), - #3478f6; - --macos-control-accent-background-hover: - linear-gradient(0deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0.08) 100%), - linear-gradient(0deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0.08) 100%), - linear-gradient(0deg, - rgba(255, 255, 255, 0.01) 0%, - rgba(255, 255, 255, 0.08) 100%), - #3478f6; - --macos-control-accent-background-pressed: - linear-gradient(0deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0.2) 100%), - #0167eb; - --macos-control-accent-background-focus: - linear-gradient(0deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0.08) 100%), - linear-gradient(0deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0.08) 100%), - linear-gradient(0deg, - rgba(255, 255, 255, 0.01) 0%, - rgba(255, 255, 255, 0.08) 100%), - #3478f6; - --macos-control-accent-shadow-rest: - 0 1px 0 0 rgba(255, 255, 255, 0) inset, - 0 1px 0 0 rgba(255, 255, 255, 0) inset, 0 0 0 1px rgba(0, 122, 255, 0.05), - 0 0 1px 0 rgba(0, 122, 255, 0.05), 0 1px 1px 0 rgba(0, 122, 255, 0.1); - --macos-control-accent-shadow-hover: - 0px 0.5px 0px 0px rgba(255, 255, 255, 0) inset, - 0px 1px 0px 0px rgba(255, 255, 255, 0) inset, - 0px 0px 0px 0.5px rgba(0, 122, 255, 0.05), - 0px 0px 1px 0px rgba(0, 122, 255, 0.05), - 0px 1px 1px 0px rgba(0, 122, 255, 0.1); - --macos-control-accent-shadow-pressed: - 0px 0.5px 0px 0px rgba(255, 255, 255, 0) inset, - 0px 1px 0px 0px rgba(255, 255, 255, 0) inset, - 0px 0px 0px 0.5px rgba(0, 122, 255, 0.05), - 0px 0px 1px 0px rgba(0, 122, 255, 0.05), - 0px 1px 1px 0px rgba(0, 122, 255, 0.1); - - /* Accent Branded variant */ - --macos-control-accent-branded-background-rest: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2749DB; - --macos-control-accent-branded-background-pressed: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2140C0; - --macos-control-accent-branded-background-hover: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2749DB; - --macos-control-accent-branded-background-disabled: var(--color-white); - --macos-control-accent-branded-background-focus: linear-gradient(0deg, rgba(255, 255, 255, 0.00) 0%, rgba(255, 255, 255, 0.16) 100%), #2749DB; - - --macos-control-accent-branded-shadow-rest: 0 1px 0 0 rgba(255, 255, 255, 0) inset, 0 1px 0 0 rgba(255, 255, 255, 0) inset, 0 0 0 1px rgba(0, 122, 255, 0.05), 0 0 1px 0 rgba(0, 122, 255, 0.05), 0 1px 1px 0 rgba(0, 122, 255, 0.1); - --macos-control-accent-branded-shadow-pressed: 0px 0.5px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 0px 0px 0.5px rgba(0, 122, 255, 0.05), 0px 0px 1px 0px rgba(0, 122, 255, 0.05), 0px 1px 1px 0px rgba(0, 122, 255, 0.10); - --macos-control-accent-branded-shadow-hover: 0px 0.5px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 0px 0px 0.5px rgba(0, 122, 255, 0.05), 0px 0px 1px 0px rgba(0, 122, 255, 0.05), 0px 1px 1px 0px rgba(0, 122, 255, 0.10); - --macos-control-accent-branded-shadow-disabled: 0px 0.5px 0px 0px rgba(255, 255, 255, 0.20) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.05) inset, 0px 0px 0px 0.5px rgba(0, 0, 0, 0.10), 0px 0px 1px 0px rgba(0, 0, 0, 0.05), 0px 1px 1px 0px rgba(0, 0, 0, 0.20); + /* Shared among all variants */ + --macos-control-focused-shadow: 0 0 0 3px rgba(57, 105, 239, 0.55), 0 0 0 1px rgba(57, 105, 239, 0.55) inset, 0 0 1px 0 rgba(0, 0, 0, 0.05), 0 1px 1px 0 rgba(0, 0, 0, 0.1); + + /* Standard button variant */ + --macos-control-standard-background-rest: var(--color-white); + --macos-control-standard-background-rest--dark: rgba(255, 255, 255, 0.28); + --macos-control-standard-background-pressed: #e7e7e7; + --macos-control-standard-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 1px 0 0 rgba(255, 255, 255, 0.05) inset, 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 1px 0 rgba(0, 0, 0, 0.05), 0 1px 1px 0 rgba(0, 0, 0, 0.2); + + /* Accent Branded variant */ + --macos-control-accent-branded-background-rest: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2749DB; + --macos-control-accent-branded-background-pressed: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2140C0; + --macos-control-accent-branded-background-hover: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.16) 100%), #2749DB; + --macos-control-accent-branded-background-disabled: var(--color-white); + --macos-control-accent-branded-background-focus: linear-gradient(0deg, rgba(255, 255, 255, 0.00) 0%, rgba(255, 255, 255, 0.16) 100%), #2749DB; + + --macos-control-accent-branded-shadow-rest: 0 1px 0 0 rgba(255, 255, 255, 0) inset, 0 1px 0 0 rgba(255, 255, 255, 0) inset, 0 0 0 1px rgba(0, 122, 255, 0.05), 0 0 1px 0 rgba(0, 122, 255, 0.05), 0 1px 1px 0 rgba(0, 122, 255, 0.1); + --macos-control-accent-branded-shadow-pressed: 0px 0.5px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 0px 0px 0.5px rgba(0, 122, 255, 0.05), 0px 0px 1px 0px rgba(0, 122, 255, 0.05), 0px 1px 1px 0px rgba(0, 122, 255, 0.10); + --macos-control-accent-branded-shadow-hover: 0px 0.5px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.00) inset, 0px 0px 0px 0.5px rgba(0, 122, 255, 0.05), 0px 0px 1px 0px rgba(0, 122, 255, 0.05), 0px 1px 1px 0px rgba(0, 122, 255, 0.10); + --macos-control-accent-branded-shadow-disabled: 0px 0.5px 0px 0px rgba(255, 255, 255, 0.20) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.05) inset, 0px 0px 0px 0.5px rgba(0, 0, 0, 0.10), 0px 0px 1px 0px rgba(0, 0, 0, 0.05), 0px 1px 1px 0px rgba(0, 0, 0, 0.20); } /* macOS Variables */ [data-platform-name="macos"] .button { - &.standard { - --button-bg: var(--macos-control-standard-background-rest); - --button-text: var(--macos-text-primary); - --button-shadow: var(--macos-control-standard-shadow); - --button-opacity: 1; - - /* Active */ - --button-bg--active: var(--macos-control-standard-background-pressed); - - /* Disabled */ - --button-bg--disabled: var(--macos-control-standard-background-rest); - --button-text--disabled: var(--macos-text-primary); - --button-shadow--disabled: var(--macos-control-standard-shadow); - --button-opacity--disabled: 0.4; - - /* Focus */ - --button-shadow--focus: var(--macos-control-focused-shadow); - - /* Hover */ - /* TODO: No difference on hover according to Figma(?) */ - } - - &.accent { - --button-bg: var(--macos-control-accent-background-rest); - --button-text: var(--color-white); - --button-shadow: var(--macos-control-accent-shadow-rest); - --button-opacity: 1; - - /* Active */ - --button-bg--active: var(--macos-control-accent-background-pressed); - --button-text--active: var(--color-white-at-80); - --button-shadow--active: var(--macos-control-accent-shadow-pressed); - - /* Disabled */ - --button-bg--disabled: var(--macos-control-standard-background-rest); - --button-text--disabled: var(--macos-text-primary); - --button-shadow--disabled: var(--macos-control-standard-shadow); - --button-opacity--disabled: 0.4; - - /* Focus */ - --button-bg--focus: var(--macos-control-accent-background-focus); - --button-text--focus: var(--color-white); - --button-shadow--focus: var(--macos-control-focused-shadow); - - /* Hover */ - --button-bg--hover: var(--macos-control-accent-background-hover); - --button-text--hover: var(--color-white); - --button-shadow--hover: var(--macos-control-accent-shadow-hover); - } - - &.accentBrand { - --button-bg: var(--macos-control-accent-branded-background-rest); - --button-text: var(--color-white); - --button-shadow: var(--macos-control-accent-branded-shadow-rest); - --button-opacity: 1; - - /* Active */ - --button-bg--active: var(--macos-control-accent-branded-background-pressed); - --button-text--active: var(--color-white-at-80); - --button-shadow--active: var(--macos-control-accent-branded-shadow-pressed); - - /* Disabled */ - --button-bg--disabled: var(--macos-control-standard-background-rest); - --button-text--disabled: var(--macos-text-primary); - --button-shadow--disabled: var(--macos-control-standard-shadow); - --button-opacity--disabled: 0.4; - - /* Focus */ - --button-bg--focus: var(--macos-control-accent-branded-background-focus); - --button-text--focus: var(--color-white); - --button-shadow--focus: var(--macos-control-focused-shadow); - - /* Hover */ - --button-bg--hover: var(--macos-control-accent-branded-background-hover); - --button-text--hover: var(--color-white); - --button-shadow--hover: var(--macos-control-accent-branded-shadow-hover); - } - - [data-theme=dark] & { &.standard { - --button-bg: var(--macos-control-standard-background-rest--dark); - --button-text: var(--color-white-at-84); - } + --button-bg: var(--macos-control-standard-background-rest); + --button-text: var(--macos-text-primary); + --button-shadow: var(--macos-control-standard-shadow); + --button-opacity: 1; - /** TODO: Confirm that colors don't change in Dark Mode for accented macOS button */ - } -} + /* Active */ + --button-bg--active: var(--macos-control-standard-background-pressed); -/* iOS Variables */ -[data-platform-name="ios"] .button { - &.primary { - --button-bg: var(--color-blue-50); - --button-text: var(--color-white); + /* Disabled */ + --button-bg--disabled: var(--macos-control-standard-background-rest); + --button-text--disabled: var(--macos-text-primary); + --button-shadow--disabled: var(--macos-control-standard-shadow); + --button-opacity--disabled: 0.4; - /* Active */ - --button-bg--active: var(--color-blue-70); - --button-text--active: var(--color-white); + /* Focus */ + --button-shadow--focus: var(--macos-control-focused-shadow); - /* Disabled */ - --button-bg--disabled: var(--color-black-at-6); - --button-text--disabled: var(--color-black-at-36); - } - - &.ghost { - --button-bg: transparent; - --button-text: var(--color-blue-50); + /* Hover */ + /* TODO: No difference on hover according to Figma(?) */ + } - /* Active */ - --button-bg--active: rgba(57, 105, 239, 0.12); - --button-text--active: var(--color-blue-70); + &.accent, + &.accentBrand { + --button-bg: var(--macos-control-accent-branded-background-rest); + --button-text: var(--color-white); + --button-shadow: var(--macos-control-accent-branded-shadow-rest); + --button-opacity: 1; + + /* Active */ + --button-bg--active: var(--macos-control-accent-branded-background-pressed); + --button-text--active: var(--color-white-at-80); + --button-shadow--active: var(--macos-control-accent-branded-shadow-pressed); + + /* Disabled */ + --button-bg--disabled: var(--macos-control-standard-background-rest); + --button-text--disabled: var(--macos-text-primary); + --button-shadow--disabled: var(--macos-control-standard-shadow); + --button-opacity--disabled: 0.4; + + /* Focus */ + --button-bg--focus: var(--macos-control-accent-branded-background-focus); + --button-text--focus: var(--color-white); + --button-shadow--focus: var(--macos-control-focused-shadow); + + /* Hover */ + --button-bg--hover: var(--macos-control-accent-branded-background-hover); + --button-text--hover: var(--color-white); + --button-shadow--hover: var(--macos-control-accent-branded-shadow-hover); + } - /* Disabled */ - --button-bg--disabled: transparent; - --button-text--disabled: var(--color-black-at-36); - } + [data-theme=dark] & { + &.standard { + --button-bg: var(--macos-control-standard-background-rest--dark); + --button-text: var(--color-white-at-84); + } + /** TODO: Confirm that colors don't change in Dark Mode for accented macOS button */ + } +} - [data-theme=dark] & { +/* iOS Variables */ +[data-platform-name="ios"] .button { &.primary { - --button-bg: var(--color-blue-30); - --button-text: var(--color-black-at-84); + --button-bg: var(--color-blue-50); + --button-text: var(--color-white); - /* Active */ - --button-bg--active: var(--color-blue-50); - --button-text--active: var(--color-black-at-84); + /* Active */ + --button-bg--active: var(--color-blue-70); + --button-text--active: var(--color-white); - /* Disabled */ - --button-bg--disabled: var(--color-black-at-6); - --button-text--disabled: var(--color-black-at-36); + /* Disabled */ + --button-bg--disabled: var(--color-black-at-6); + --button-text--disabled: var(--color-black-at-36); } &.ghost { - --button-bg: transparent; - --button-text: var(--color-blue-30); + --button-bg: transparent; + --button-text: var(--color-blue-50); - /* Active */ - --button-bg--active: rgba(114, 149, 246, 0.2); - --button-text--active: var(--color-blue-20); + /* Active */ + --button-bg--active: rgba(57, 105, 239, 0.12); + --button-text--active: var(--color-blue-70); - /* Disabled */ - --button-bg--disabled: transparent; - --button-text--disabled: var(--color-black-at-36); + /* Disabled */ + --button-bg--disabled: transparent; + --button-text--disabled: var(--color-black-at-36); } - } -} -/* Windows Implementation */ -[data-platform-name="windows"] { - .button { - /* this is the focus ring used on NTP widgets */ - --focus-ring: 0px 0px 0px 1px var(--color-white), 0px 0px 0px 3px var(--color-black); - border-radius: var(--border-radius-sm); - height: var(--sp-8); - border-width: 0; - padding-left: var(--sp-3); - padding-right: var(--sp-3); + [data-theme=dark] & { + &.primary { + --button-bg: var(--color-blue-30); + --button-text: var(--color-black-at-84); - &:focus-visible { - outline: none; - box-shadow: var(--focus-ring); - } + /* Active */ + --button-bg--active: var(--color-blue-50); + --button-text--active: var(--color-black-at-84); - &.standard { - background-color: var(--color-black-at-6); - border-width: 0; - - &:hover { - background-color: var(--color-black-at-9); - cursor: pointer; - } - - &:active { - background-color: var(--color-black-at-12); - } - - &:disabled { - color: var(--color-black-at-84); - } - - &:disabled:hover { - cursor: not-allowed; - background-color: var(--color-white-at-6); - } - } + /* Disabled */ + --button-bg--disabled: var(--color-black-at-6); + --button-text--disabled: var(--color-black-at-36); + } - &.accent, - &.accentBrand { - background-color: var(--ddg-color-primary); - color: var(--color-white); + &.ghost { + --button-bg: transparent; + --button-text: var(--color-blue-30); - &:hover { - background-color: var(--color-blue-60); - } + /* Active */ + --button-bg--active: rgba(114, 149, 246, 0.2); + --button-text--active: var(--color-blue-20); - &:active { - background-color: var(--color-blue-70); - } + /* Disabled */ + --button-bg--disabled: transparent; + --button-text--disabled: var(--color-black-at-36); + } } +} - [data-theme=dark] & { - --focus-ring: 0px 0px 0px 1px var(--color-black), 0px 0px 0px 3px var(--color-white); +/* Windows Implementation */ +[data-platform-name="windows"] { + .button { + /* this is the focus ring used on NTP widgets */ + --focus-ring: 0px 0px 0px 1px var(--color-white), 0px 0px 0px 3px var(--color-black); + + border-radius: var(--border-radius-sm); + height: var(--sp-8); + border-width: 0; + padding-left: var(--sp-3); + padding-right: var(--sp-3); + + &:focus-visible { + outline: none; + box-shadow: var(--focus-ring); + } - &.standard { - color: var(--color-white-at-84); - background-color: var(--color-white-at-12); + &.standard { + background-color: var(--color-black-at-6); + border-width: 0; - &:hover { - background-color: var(--color-white-at-18); - } + &:hover { + background-color: var(--color-black-at-9); + cursor: pointer; + } - &:active { - background-color: var(--color-white-at-24); - } + &:active { + background-color: var(--color-black-at-12); + } - &:disabled { - color: var(--color-white-at-12); - opacity: 0.8; - } + &:disabled { + color: var(--color-black-at-84); + } - &:disabled:hover { - background-color: var(--color-white-at-12); + &:disabled:hover { + cursor: not-allowed; + background-color: var(--color-white-at-6); + } } - } - &.accent, - &.accentBrand { - color: var(--color-black-at-84); - background-color: var(--color-blue-20); + &.accent, + &.accentBrand { + background-color: var(--ddg-color-primary); + color: var(--color-white); - &:hover { - background-color: var(--color-blue-30); - } + &:hover { + background-color: var(--color-blue-60); + } - &:active { - background-color: var(--color-blue-40); + &:active { + background-color: var(--color-blue-70); + } } - &:disabled { - background-color: var(--color-white-at-36); - color: var(--color-black-at-84); + [data-theme=dark] & { + --focus-ring: 0px 0px 0px 1px var(--color-black), 0px 0px 0px 3px var(--color-white); + + &.standard { + color: var(--color-white-at-84); + background-color: var(--color-white-at-12); + + &:hover { + background-color: var(--color-white-at-18); + } + + &:active { + background-color: var(--color-white-at-24); + } + + &:disabled { + color: var(--color-white-at-12); + opacity: 0.8; + } + + &:disabled:hover { + background-color: var(--color-white-at-12); + } + } + + &.accent, + &.accentBrand { + color: var(--color-black-at-84); + background-color: var(--color-blue-20); + + &:hover { + background-color: var(--color-blue-30); + } + + &:active { + background-color: var(--color-blue-40); + } + + &:disabled { + background-color: var(--color-white-at-36); + color: var(--color-black-at-84); + } + } } - } } - } } From ef8193c980dba68dd1d55d0a603cd13d9bd7edb9 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Fri, 17 Oct 2025 13:03:25 -0400 Subject: [PATCH 15/15] Update test to pass the correct query param value --- .../integration-tests/subscription-winback-banner.spec.js | 5 ++--- special-pages/playwright.config.js | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/special-pages/pages/new-tab/app/subscription-winback-banner/integration-tests/subscription-winback-banner.spec.js b/special-pages/pages/new-tab/app/subscription-winback-banner/integration-tests/subscription-winback-banner.spec.js index e871345562..03b72716f1 100644 --- a/special-pages/pages/new-tab/app/subscription-winback-banner/integration-tests/subscription-winback-banner.spec.js +++ b/special-pages/pages/new-tab/app/subscription-winback-banner/integration-tests/subscription-winback-banner.spec.js @@ -5,7 +5,7 @@ test.describe('newtab remote messaging framework subscriptionWinBackBanner', () test('fetches config + data', async ({ page }, workerInfo) => { const ntp = NewtabPage.create(page, workerInfo); await ntp.reducedMotion(); - await ntp.openPage({ winback: 'true' }); + await ntp.openPage({ winback: 'winback_last_day' }); const calls1 = await ntp.mocks.waitForCallCount({ method: 'initialSetup', count: 1 }); const calls2 = await ntp.mocks.waitForCallCount({ method: 'winBackOffer_getData', count: 1 }); @@ -17,11 +17,10 @@ test.describe('newtab remote messaging framework subscriptionWinBackBanner', () test('renders a title, descriptionText an action button, and dismiss button', async ({ page }, workerInfo) => { const ntp = NewtabPage.create(page, workerInfo); await ntp.reducedMotion(); - await ntp.openPage({ winback: 'true' }); + await ntp.openPage({ winback: 'winback_last_day' }); await page.getByRole('heading', { name: 'Last day to save 25%!' }).waitFor(); await page.getByText('Stay protected with our').waitFor(); - await page.locator('strong').waitFor(); await page.getByRole('button', { name: 'See Offer' }).click(); await ntp.mocks.waitForCallCount({ method: 'winBackOffer_action', count: 1 }); diff --git a/special-pages/playwright.config.js b/special-pages/playwright.config.js index 71d42de25a..5da7537260 100644 --- a/special-pages/playwright.config.js +++ b/special-pages/playwright.config.js @@ -24,6 +24,7 @@ export default defineConfig({ testMatch: [ 'favorites.spec.js', 'freemium-pir-banner.spec.js', + 'subscription-winback-banner.spec.js', 'new-tab.spec.js', 'new-tab.screenshots.spec.js', 'next-steps.spec.js',