diff --git a/special-pages/pages/new-tab/app/components/App.js b/special-pages/pages/new-tab/app/components/App.js index fa41764f8..5fc03da43 100644 --- a/special-pages/pages/new-tab/app/components/App.js +++ b/special-pages/pages/new-tab/app/components/App.js @@ -1,4 +1,4 @@ -import { h } from 'preact'; +import { Fragment, h } from 'preact'; import cn from 'classnames'; import styles from './App.module.css'; import { useCustomizerDrawerSettings, usePlatformName } from '../settings.provider.js'; @@ -7,6 +7,7 @@ import { useGlobalDropzone } from '../dropzone.js'; import { Customizer, CustomizerButton, CustomizerMenuPositionedFixed, useContextMenu } from '../customizer/components/Customizer.js'; import { useDrawer, useDrawerControls } from './Drawer.js'; import { CustomizerDrawer } from '../customizer/components/CustomizerDrawer.js'; +import { BackgroundConsumer, BackgroundProvider } from './BackgroundProvider.js'; /** * Renders the App component. @@ -24,35 +25,42 @@ export function App({ children }) { useContextMenu(); const { buttonRef, wrapperRef, visibility, displayChildren, hidden, buttonId, drawerId } = useDrawer(); - const { toggle, close } = useDrawerControls(); + const { toggle } = useDrawerControls(); return ( -
-
-
- - - {customizerKind === 'menu' && } - {customizerKind === 'drawer' && ( - - )} - - {children} -
-
+ {customizerKind === 'drawer' && ( - + + + )} -
+
+
+
+ + + {customizerKind === 'menu' && } + {customizerKind === 'drawer' && ( + + )} + + {children} +
+
+ {customizerKind === 'drawer' && ( + + )} +
+ ); } diff --git a/special-pages/pages/new-tab/app/components/App.module.css b/special-pages/pages/new-tab/app/components/App.module.css index 0569e5b0a..ad52a3d07 100644 --- a/special-pages/pages/new-tab/app/components/App.module.css +++ b/special-pages/pages/new-tab/app/components/App.module.css @@ -50,6 +50,7 @@ body:has([data-reset-layout="true"]) .tube { } } + .active {} .aside { @@ -69,5 +70,4 @@ body:has([data-reset-layout="true"]) .tube { box-sizing: border-box; height: 100vh; width: var(--ntp-drawer-width); - padding: var(--sp-2); } diff --git a/special-pages/pages/new-tab/app/components/BackgroundProvider.js b/special-pages/pages/new-tab/app/components/BackgroundProvider.js new file mode 100644 index 000000000..fbf6f1b5c --- /dev/null +++ b/special-pages/pages/new-tab/app/components/BackgroundProvider.js @@ -0,0 +1,100 @@ +import { createContext, Fragment, h } from 'preact'; +import styles from './BackgroundReceiver.module.css'; +import { values } from '../customizer/values.js'; +import { computed, signal } from '@preact/signals'; +import { useContext } from 'preact/hooks'; +import { CustomizerContext } from '../customizer/CustomizerProvider.js'; + +/** + * @import { BackgroundVariant } from "../../types/new-tab" + */ + +const BackgroundContext = createContext({ + /** @type {import("@preact/signals").Signal} */ + current: signal({ kind: 'default' }), +}); + +/** + * @param {object} props + * @param {import("preact").ComponentChild} props.children + */ +export function BackgroundProvider({ children }) { + const { data } = useContext(CustomizerContext); + const bg = computed(() => data.value.background); + return {children}; +} + +/** + * + */ +export function BackgroundConsumer() { + const { current } = useContext(BackgroundContext); + const background = current.value; + if (background === null) { + return
; + } + switch (background.kind) { + case 'hex': { + return ( +
+ ); + } + case 'color': { + const color = values.colors[background.value]; + return ( +
+ ); + } + case 'gradient': { + const gradient = values.gradients[background.value]; + return ( + +
+
+ + ); + } + case 'userImage': { + const img = background.value; + return ( +
+ ); + } + default: { + throw new Error('Unreachable!'); + } + } +} diff --git a/special-pages/pages/new-tab/app/components/BackgroundReceiver.module.css b/special-pages/pages/new-tab/app/components/BackgroundReceiver.module.css new file mode 100644 index 000000000..9d018328f --- /dev/null +++ b/special-pages/pages/new-tab/app/components/BackgroundReceiver.module.css @@ -0,0 +1,8 @@ +.root { + position: fixed; + z-index: 0; + inset: 0; + width: 100vw; + height: 100vh; + transition: all .3s; +} diff --git a/special-pages/pages/new-tab/app/components/DismissButton.jsx b/special-pages/pages/new-tab/app/components/DismissButton.jsx index 820d000af..6c746cd84 100644 --- a/special-pages/pages/new-tab/app/components/DismissButton.jsx +++ b/special-pages/pages/new-tab/app/components/DismissButton.jsx @@ -4,10 +4,10 @@ import { Cross } from './Icons'; import { useTypedTranslation } from '../types'; import styles from './DismissButton.module.css'; -/* +/** * @param {object} props * @param {string} [props.className] - * @param {() => void} props.onClick + * @param {() => void} [props.onClick] */ export function DismissButton({ className, onClick }) { const { t } = useTypedTranslation(); diff --git a/special-pages/pages/new-tab/app/components/Icons.js b/special-pages/pages/new-tab/app/components/Icons.js index dd0198ecd..8f2942c2d 100644 --- a/special-pages/pages/new-tab/app/components/Icons.js +++ b/special-pages/pages/new-tab/app/components/Icons.js @@ -82,3 +82,38 @@ export function Cross() { ); } + +export function CircleCheck() { + return ( + + + + + + + + + + + ); +} + +export function Picker() { + return ( + + + + ); +} diff --git a/special-pages/pages/new-tab/app/customizer/CustomizerProvider.js b/special-pages/pages/new-tab/app/customizer/CustomizerProvider.js new file mode 100644 index 000000000..c626edf53 --- /dev/null +++ b/special-pages/pages/new-tab/app/customizer/CustomizerProvider.js @@ -0,0 +1,92 @@ +import { createContext, h } from 'preact'; +import { useCallback } from 'preact/hooks'; +import { effect, signal, useSignal } from '@preact/signals'; + +/** + * @typedef {import('../../types/new-tab.js').CustomizerData} CustomizerData + * @typedef {import('../service.hooks.js').State} State + * @typedef {import('../service.hooks.js').Events} Events + */ + +/** + * These are the values exposed to consumers. + */ +export const CustomizerContext = createContext({ + /** @type {import("@preact/signals").Signal} */ + data: signal({ + background: { kind: 'default' }, + userImages: [], + theme: 'system', + }), + /** @type {(bg: CustomizerData['background']) => void} */ + select: (bg) => {}, + upload: () => {}, + /** + * @type {(theme: import('../../types/new-tab').CustomizerData['theme']) => void} + */ + setTheme: (theme) => {}, + /** + * @type {(id: string) => void} + */ + deleteImage: (id) => {}, +}); + +/** + * A data provider that will use `RMFService` to fetch data, subscribe + * to updates and modify state. + * + * @param {Object} props + * @param {import("./customizer.service.js").CustomizerService} props.service + * @param {CustomizerData} props.initialData + * @param {import("preact").ComponentChild} props.children + */ +export function CustomizerProvider({ service, initialData, children }) { + // const [state, dispatch] = useReducer(withLog('RMFProvider', reducer), initial) + const data = useSignal(initialData); + + effect(() => { + const unsub = service.onBackground((evt) => { + data.value = { ...data.value, background: evt.data }; + }); + const unsub1 = service.onTheme((evt) => { + data.value = { ...data.value, theme: evt.data }; + }); + const unsub2 = service.onImages((evt) => { + data.value = { ...data.value, userImages: evt.data }; + }); + + return () => { + unsub(); + unsub1(); + unsub2(); + }; + }); + + /** @type {(bg: CustomizerData['background']) => void} */ + const select = useCallback( + (bg) => { + service.setBackground(bg); + }, + [service], + ); + + const upload = useCallback(() => { + service.upload(); + }, [service]); + + const setTheme = useCallback( + (theme) => { + service.setTheme(theme); + }, + [service], + ); + + const deleteImage = useCallback( + (id) => { + service.deleteImage(id); + }, + [service], + ); + + return {children}; +} diff --git a/special-pages/pages/new-tab/app/customizer/components/BackgroundSection.js b/special-pages/pages/new-tab/app/customizer/components/BackgroundSection.js new file mode 100644 index 000000000..f662908f2 --- /dev/null +++ b/special-pages/pages/new-tab/app/customizer/components/BackgroundSection.js @@ -0,0 +1,165 @@ +import { h, Fragment } from 'preact'; +import cn from 'classnames'; + +import { values } from '../values.js'; +import styles from './CustomizerDrawerInner.module.css'; +import { CircleCheck } from '../../components/Icons.js'; +import { computed } from '@preact/signals'; + +/** + * @import { Widgets, WidgetConfigItem, WidgetVisibility, VisibilityMenuItem, CustomizerData } from '../../../types/new-tab.js' + */ + +/** + * @param {object} props + * @param {import('@preact/signals').Signal} props.data + * @param {(target: 'color' | 'back' | 'image' | 'gradient') => void} props.onNav + * @param {() => void} props.onUpload + */ +export function BackgroundSection({ data, onNav, onUpload }) { + console.log(' RENDER:BackgroundSection?'); + const color = values.colors.color11; + const gradient = values.gradients.gradient02; + + return ( +
+

Background

+
    +
  • + +
  • +
  • + onNav('color')} /> +
  • +
  • + onNav('gradient')} /> +
  • +
  • + onNav('image')} data={data} upload={onUpload} /> +
  • +
+
+ ); +} + +function DefaultPanel() { + return ( + <> + + Default + + ); +} + +/** + * @param {object} props + * @param {() => void} props.onClick + * @param {typeof values.colors[keyof typeof values.colors]} props.color + */ +function ColorPanel(props) { + return ( + <> + + Solid Colors + + ); +} + +/** + * @param {object} props + * @param {() => void} props.onClick + * @param {typeof values.gradients[keyof typeof values.gradients]} props.gradient + */ +function GradientPanel(props) { + return ( + <> + + Gradients + + ); +} + +/** + * @param {object} props + * @param {() => void} props.onClick + * @param {() => void} props.upload + * @param {import('@preact/signals').Signal} props.data + */ +function BackgroundImagePanel(props) { + const empty = computed(() => props.data.value.userImages.length === 0); + const selectedImage = computed(() => { + const imageId = props.data.value.background.kind === 'userImage' ? props.data.value.background.value : null; + if (imageId !== null) { + const match = props.data.value.userImages.find((i) => i.id === imageId.id); + if (match) { + return match; + } + } + return null; + }); + + const firstImage = computed(() => { + return props.data.value.userImages[0] ?? null; + }); + + if (empty.value === true) { + return ( + + + Add Background + + ); + } + + if (selectedImage.value !== null) { + return ( + + + My Backgrounds + + ); + } + + return ( + + + My Backgrounds + + ); +} diff --git a/special-pages/pages/new-tab/app/customizer/components/BrowserThemeSection.js b/special-pages/pages/new-tab/app/customizer/components/BrowserThemeSection.js new file mode 100644 index 000000000..b8a7fbc0c --- /dev/null +++ b/special-pages/pages/new-tab/app/customizer/components/BrowserThemeSection.js @@ -0,0 +1,60 @@ +import styles from './CustomizerDrawerInner.module.css'; +import cn from 'classnames'; +import { h } from 'preact'; +import { computed } from '@preact/signals'; + +/** + * @param {object} props + * @param {import('@preact/signals').Signal} props.data + * @param {(theme: import('../../../types/new-tab').CustomizerData['theme']) => void} props.setTheme + */ +export function BrowserThemeSection(props) { + console.log(' RENDER:BrowserThemeSection?'); + const current = computed(() => props.data.value.theme); + return ( +
+

Browser Theme

+
    +
  • + + Light +
  • +
  • + + Dark +
  • +
  • + + System +
  • +
+
+ ); +} diff --git a/special-pages/pages/new-tab/app/customizer/components/ColorSelection.js b/special-pages/pages/new-tab/app/customizer/components/ColorSelection.js new file mode 100644 index 000000000..76c9077a2 --- /dev/null +++ b/special-pages/pages/new-tab/app/customizer/components/ColorSelection.js @@ -0,0 +1,143 @@ +import { h, Fragment } from 'preact'; +import cn from 'classnames'; + +import { values } from '../values.js'; +import styles from './CustomizerDrawerInner.module.css'; +import { Picker } from '../../components/Icons.js'; +import { computed, useSignal } from '@preact/signals'; + +/** + * @import { Widgets, WidgetConfigItem, WidgetVisibility, VisibilityMenuItem, CustomizerData, PredefinedColor } from '../../../types/new-tab.js' + */ + +/** + * @param {object} props + * @param {import("@preact/signals").Signal} props.data + * @param {(bg: CustomizerData['background']) => void} props.select + * @param {() => void} props.back + */ +export function ColorSelection({ data, select, back }) { + console.log(' RENDER:ColorSelection?'); + + function onClick(event) { + let target = /** @type {HTMLElement|null} */ (event.target); + while (target && target !== event.currentTarget) { + if (target.getAttribute('role') === 'radio') { + event.preventDefault(); + event.stopImmediatePropagation(); + if (target.getAttribute('aria-checked') === 'false') { + if (target.dataset.key) { + const value = /** @type {PredefinedColor} */ (target.dataset.key); + select({ kind: 'color', value }); + } else { + console.warn('missing dataset.key'); + } + } else { + console.log('ignoring click on selected color'); + } + break; + } else { + target = target.parentElement; + } + } + } + + return ( + + +
+
+ + +
+
+
+ ); +} + +const entries = Object.entries(values.colors); + +/** + * @param {object} props + * @param {import("@preact/signals").Signal} props.data + */ +function ColorGrid({ data }) { + const selected = computed(() => data.value.background.kind === 'color' && data.value.background.value); + return ( + + {entries.map(([key, entry]) => { + return ( +
+ +
+ ); + })} +
+ ); +} + +/** + * @param {object} props + * @param {import("@preact/signals").Signal} props.data + * @param {(bg: CustomizerData['background']) => void} props.select + */ +function PickerPanel({ data, select }) { + const peeked = data.peek(); + const initialColor = peeked.background.kind === 'hex' ? peeked.background.value : '#000000'; + const hex = useSignal(initialColor); + const hexSelected = computed(() => data.value.background.kind === 'hex'); + + return ( +
+ + { + if (!(e.target instanceof HTMLInputElement)) { + return; + } + hex.value = e.target.value; + select({ kind: 'hex', value: hex.value }); + }} + onClick={(e) => { + select({ kind: 'hex', value: hex.value }); + }} + /> + + + + Show color picker +
+ ); +} diff --git a/special-pages/pages/new-tab/app/customizer/components/Customizer.examples.js b/special-pages/pages/new-tab/app/customizer/components/Customizer.examples.js index 16eccf1a6..5d7ddce51 100644 --- a/special-pages/pages/new-tab/app/customizer/components/Customizer.examples.js +++ b/special-pages/pages/new-tab/app/customizer/components/Customizer.examples.js @@ -2,10 +2,66 @@ import { h, Fragment } from 'preact'; import { noop } from '../../utils.js'; import { CustomizerButton } from './Customizer.js'; import { VisibilityMenu } from './VisibilityMenu.js'; +import { BackgroundSection } from './BackgroundSection.js'; +import { ColorSelection } from './ColorSelection.js'; +import { GradientSelection } from './GradientSelection.js'; +import { useSignal } from '@preact/signals'; +import { ImageSelection } from './ImageSelection.js'; /** @type {Record import("preact").ComponentChild}>} */ - export const customizerExamples = { + 'customizer.backgroundSection': { + factory: () => { + return ( + + {({ data, select }) => { + return ; + }} + + ); + }, + }, + 'customizer.colorSelection': { + factory: () => { + return ( + + {({ data, select }) => { + return ; + }} + + ); + }, + }, + 'customizer.gradientSelection': { + factory: () => { + return ( + + {({ data, select }) => { + return ; + }} + + ); + }, + }, + 'customizer.imageSelection': { + factory: () => { + return ( + + {({ data, select }) => { + return ( + + ); + }} + + ); + }, + }, 'customizer-menu': { factory: () => ( @@ -43,3 +99,20 @@ export const customizerExamples = { function MaxContent({ children }) { return
{children}
; } + +function Provider({ children }) { + /** @type {import('../../../types/new-tab.js').CustomizerData} */ + const data = { + background: { kind: 'hex', value: '#17afa8' }, + theme: 'system', + userImages: [], + }; + const dataSignal = useSignal(data); + function select(bg) { + dataSignal.value = { ...dataSignal.value, background: bg }; + } + function showPicker() { + console.log('no-op'); + } + return children({ data: dataSignal, select, showPicker }); +} diff --git a/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawer.js b/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawer.js index d8060db16..b1f388544 100644 --- a/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawer.js +++ b/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawer.js @@ -1,30 +1,26 @@ import { h } from 'preact'; import styles from './CustomizerDrawer.module.css'; -import { Suspense, lazy } from 'preact/compat'; import { useDrawerControls } from '../../components/Drawer.js'; -import { useEffect } from 'preact/hooks'; - -// eslint-disable-next-line promise/prefer-await-to-then -const CustomizerDrawerInner = lazy(() => import('./CustomizerDrawerInner').then((x) => x.CustomizerDrawerInner)); +import { useContext, useEffect } from 'preact/hooks'; +import { CustomizerContext } from '../CustomizerProvider.js'; +import { CustomizerDrawerInner } from './CustomizerDrawerInner.js'; /** * @param {object} props - * @param {object} props.onClose - * @param {object} props.wrapperRef * @param {import("@preact/signals").Signal} props.displayChildren */ -export function CustomizerDrawer({ onClose, displayChildren }) { +export function CustomizerDrawer({ displayChildren }) { const { open, close } = useDrawerControls(); useEffect(() => { const checker = () => { const shouldOpen = window.location.hash.startsWith('#/customizer'); - console.log({ shouldOpen }); if (shouldOpen) { open(); } else { close(); } }; + // check once on page load checker(); @@ -34,14 +30,16 @@ export function CustomizerDrawer({ onClose, displayChildren }) { window.removeEventListener('hashchange', checker); }; }, []); + return (
- - {displayChildren.value && ( - Loading...
}> - - - )} +
); } + +function CustomizerConsumer() { + console.log('CustomizerConsumer'); + const { data, select, upload, setTheme, deleteImage } = useContext(CustomizerContext); + return ; +} diff --git a/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.js b/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.js index 85e0031cd..dc9162a83 100644 --- a/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.js +++ b/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.js @@ -1,34 +1,65 @@ import { h } from 'preact'; +import cn from 'classnames'; import styles from './CustomizerDrawerInner.module.css'; -import { useState, useEffect } from 'preact/hooks'; -import { Customizer, getItems } from './Customizer'; -import { VisibilityMenu } from './VisibilityMenu.js'; +import { useDrawerControls } from '../../components/Drawer.js'; +import { BackgroundSection } from './BackgroundSection.js'; +import { BrowserThemeSection } from './BrowserThemeSection.js'; +import { VisibilityMenuSection } from './VisibilityMenuSection.js'; +import { ColorSelection } from './ColorSelection.js'; +import { useRef } from 'preact/hooks'; +import { GradientSelection } from './GradientSelection.js'; +import { useSignal } from '@preact/signals'; +import { ImageSelection } from './ImageSelection.js'; /** - * @import { Widgets, WidgetConfigItem, WidgetVisibility, VisibilityMenuItem } from '../../../types/new-tab.js' + * @import { Widgets, WidgetConfigItem, WidgetVisibility, VisibilityMenuItem, CustomizerData } from '../../../types/new-tab.js' */ -export function CustomizerDrawerInner() { - const [rowData, setRowData] = useState(() => { - const items = /** @type {import("./Customizer.js").VisibilityRowData[]} */ (getItems()); - return items; - }); - - useEffect(() => { - function handler() { - setRowData(getItems()); +/** + * @param {object} props + * @param {import('@preact/signals').Signal} props.data + * @param {(bg: CustomizerData['background']) => void} props.select + * @param {() => void} props.onUpload + * @param {(theme: import('../../../types/new-tab').CustomizerData['theme']) => void} props.setTheme + * @param {(id: string) => void} props.deleteImage + */ +export function CustomizerDrawerInner({ data, select, onUpload, setTheme, deleteImage }) { + console.log(' RENDER:CustomizerDrawerInner?'); + const { close } = useDrawerControls(); + const ref = useRef(/** @type {any} */ (null)); + const state = useSignal('home'); + function onNav(nav) { + const curr = ref.current; + if (!curr) return; + if (ref.current instanceof HTMLDivElement) { + ref.current.style.gridTemplateAreas = "'col2 col1'"; } - window.addEventListener(Customizer.UPDATE_EVENT, handler); - return () => { - window.removeEventListener(Customizer.UPDATE_EVENT, handler); - }; - }, []); - + state.value = nav; + } + function back() { + ref.current.style.gridTemplateAreas = "'col1 col2'"; + state.value = 'home'; + } return (
-

Customize

-
- +
+

Customize

+ +
+
+
+ + + +
+
+ {state.value === 'color' && } + {state.value === 'gradient' && } + {state.value === 'image' && ( + + )} +
+
); } diff --git a/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.module.css b/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.module.css index 028c29c61..6a8679c09 100644 --- a/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.module.css +++ b/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.module.css @@ -4,6 +4,160 @@ animation-timing-function: ease-in-out; animation-duration: .1s; padding-block: 1rem; + display: grid; + grid-auto-rows: max-content; + grid-row-gap: var(--sp-4); + padding: var(--sp-4); + overflow: hidden; + font-size: var(--small-label-font-size); + line-height: var(--small-label-line-height); + font-weight: var(--small-label-font-weight); +} + +.header { + display: flex; + justify-content: space-between; +} + +.cols { + display: grid; + grid-template-columns: 100% 100%; + grid-template-areas: 'col1 col2'; + max-width: 100%; + overflow: hidden; + transition: all .3s; +} +.col { + width: 100%; + flex-shrink: 0; +} +.col1 { + width: 100%; + grid-area: col1 +} +.col2 { + width: 100%; + grid-area: col2 +} +.mainSections { + display: grid; + grid-row-gap: 36px; +} +.backBtn { + background: none; + border: none; + outline: none; + display: flex; + padding: 0; + align-items: center; + gap: 4px; + + svg { + width: 16px; height: 16px; display: block + } + + &:active { + opacity: .8; + } + &:focus-visible { + outline: 1px solid var(--ntp-focus-outline-color) + } +} +.section { + width: 100%; +} +.sectionBody { + margin-top: 16px; +} +.sectionTitle { + font-size: var(--title-3-em-font-size); + font-weight: var(--title-3-em-font-weight); + line-height: var(--title-3-em-line-height); +} + +.bgList { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: max-content max-content; + grid-gap: 12px; +} +.bgListItem { + display: grid; + grid-row-gap: 6px; + white-space: nowrap; + position: relative; + + &:hover { + .deleteBtn { + opacity: 1 + } + } +} +.bgPanel { + display: grid; + aspect-ratio: 16/10; + border-radius: 4px; + align-items: center; + justify-content: center; + border: none; + outline: none; + + &[aria-checked="true"] { + outline: 3px solid var(--ntp-color-primary); + outline-offset: 2px; + } + &:focus-visible { + outline: 3px solid var(--ntp-focus-outline-color); + outline-offset: 2px; + } + &:active { + opacity: .9; + } +} +.bgPanelEmpty { + border: 1px solid rgba(0, 0, 0, 0.09); + background-color: rgba(0, 0, 0, 0.03); +} +.bgPanelOutlined { + border: 1px solid rgba(0, 0, 0, 0.09); + background-color: #FAFAFA; +} +.colorInputIcon { + position: absolute; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + &, svg { + pointer-events: none; + } +} +.themeList { + + display: flex; + gap: 18px; +} +.themeItem { + display: grid; + justify-items: center; + grid-row-gap: 6px; +} +.themeButton { + display: block; + width: 42px; + height: 42px; + border-radius: 50%; + border: 1px solid #0000001F; + &[aria-checked="true"] { + outline: 3px solid var(--ntp-color-primary); + outline-offset: 2px; + } + &:focus-visible { + outline: 3px solid var(--ntp-focus-outline-color); + outline-offset: 2px; + } + &:active { + opacity: .9; + } } @keyframes fade-in { @@ -17,3 +171,10 @@ visibility: visible; } } + +.deleteBtn { + opacity: 0; + position: absolute; + top: 4px; + right: 4px; +} diff --git a/special-pages/pages/new-tab/app/customizer/components/GradientSelection.js b/special-pages/pages/new-tab/app/customizer/components/GradientSelection.js new file mode 100644 index 000000000..4374a8229 --- /dev/null +++ b/special-pages/pages/new-tab/app/customizer/components/GradientSelection.js @@ -0,0 +1,95 @@ +import { h, Fragment } from 'preact'; +import cn from 'classnames'; + +import { values } from '../values.js'; +import styles from './CustomizerDrawerInner.module.css'; +import { computed } from '@preact/signals'; + +/** + * @import { Widgets, WidgetConfigItem, WidgetVisibility, VisibilityMenuItem, CustomizerData, PredefinedGradient } from '../../../types/new-tab.js' + */ + +/** + * @param {object} props + * @param {import('@preact/signals').Signal} props.data + * @param {(bg: CustomizerData['background']) => void} props.select + * @param {() => void} props.back + */ +export function GradientSelection({ data, select, back }) { + // const gradient = values.gradients.gradient02; + + function onClick(event) { + let target = /** @type {HTMLElement|null} */ (event.target); + while (target && target !== event.currentTarget) { + if (target.getAttribute('role') === 'radio') { + event.preventDefault(); + event.stopImmediatePropagation(); + if (target.getAttribute('aria-checked') === 'false') { + if (target.dataset.key) { + const value = /** @type {PredefinedGradient} */ (target.dataset.key); + select({ kind: 'gradient', value }); + } else { + console.warn('missing dataset.key'); + } + } else { + console.log('ignoring click on selected color'); + } + break; + } else { + target = target.parentElement; + } + } + } + + return ( + + +
+ +
+
+ ); +} + +const entries = Object.entries(values.gradients); +/** + * @param {object} props + * @param {import("@preact/signals").Signal} props.data + */ +function GradientGrid({ data }) { + const selected = computed(() => data.value.background.kind === 'gradient' && data.value.background.value); + return ( +
    + {entries.map(([key, entry]) => { + return ( +
  • + +
  • + ); + })} +
+ ); +} diff --git a/special-pages/pages/new-tab/app/customizer/components/ImageSelection.js b/special-pages/pages/new-tab/app/customizer/components/ImageSelection.js new file mode 100644 index 000000000..3a3dee2dc --- /dev/null +++ b/special-pages/pages/new-tab/app/customizer/components/ImageSelection.js @@ -0,0 +1,104 @@ +import { h, Fragment } from 'preact'; +import cn from 'classnames'; + +import styles from './CustomizerDrawerInner.module.css'; +import { computed } from '@preact/signals'; +import { DismissButton } from '../../components/DismissButton.jsx'; + +/** + * @import { Widgets, WidgetConfigItem, WidgetVisibility, VisibilityMenuItem, CustomizerData, PredefinedGradient } from '../../../types/new-tab.js' + */ + +/** + * @param {object} props + * @param {import('@preact/signals').Signal} props.data + * @param {(bg: CustomizerData['background']) => void} props.select + * @param {() => void} props.back + * @param {() => void} props.onUpload + * @param {(id: string) => void} props.deleteImage + */ +export function ImageSelection({ data, select, back, onUpload, deleteImage }) { + // const gradient = values.gradients.gradient02; + + function onClick(event) { + let target = /** @type {HTMLElement|null} */ (event.target); + while (target && target !== event.currentTarget) { + if (target.getAttribute('role') === 'radio') { + event.preventDefault(); + event.stopImmediatePropagation(); + if (target.getAttribute('aria-checked') === 'false') { + if (target.dataset.key) { + const value = /** @type {string} */ (target.dataset.key); + const match = data.value.userImages.find((i) => i.id === value); + if (match) { + select({ kind: 'userImage', value: match }); + } + } else { + console.warn('missing dataset.key'); + } + } else { + console.log('ignoring click on selected color'); + } + break; + } else { + target = target.parentElement; + } + } + } + + return ( + + +
+ +
+
+ ); +} + +/** + * @param {object} props + * @param {import("@preact/signals").Signal} props.data + * @param {(id: string) => void} props.deleteImage + */ +function ImageGrid({ data, deleteImage }) { + const selected = computed(() => data.value.background.kind === 'userImage' && data.value.background.value.id); + const entries = computed(() => { + return data.value.userImages; + }); + return ( +
    + {entries.value.map((entry) => { + return ( +
  • + + deleteImage(entry.id)} /> +
  • + ); + })} +
+ ); +} diff --git a/special-pages/pages/new-tab/app/customizer/components/VisibilityMenu.js b/special-pages/pages/new-tab/app/customizer/components/VisibilityMenu.js index 938e11cb5..8a7c374aa 100644 --- a/special-pages/pages/new-tab/app/customizer/components/VisibilityMenu.js +++ b/special-pages/pages/new-tab/app/customizer/components/VisibilityMenu.js @@ -1,4 +1,5 @@ import { h } from 'preact'; +import cn from 'classnames'; import { useId } from 'preact/hooks'; import { DuckFoot, Shield } from '../../components/Icons.js'; @@ -22,7 +23,7 @@ export function VisibilityMenu({ rows, variant = 'popover' }) { const MENU_ID = useId(); return ( -
    +
      {rows.map((row) => { return (
    • diff --git a/special-pages/pages/new-tab/app/customizer/components/VisibilityMenu.module.css b/special-pages/pages/new-tab/app/customizer/components/VisibilityMenu.module.css index f5d5f8b66..1fdeb6ff4 100644 --- a/special-pages/pages/new-tab/app/customizer/components/VisibilityMenu.module.css +++ b/special-pages/pages/new-tab/app/customizer/components/VisibilityMenu.module.css @@ -20,6 +20,11 @@ display: flex; flex-direction: column; gap: var(--sp-1); + font-size: var(--title-3-em-font-size); +} + +.embedded { + font-size: var(--small-label-font-size); } .menuItemLabel { @@ -27,7 +32,6 @@ align-items: center; gap: 10px; white-space: nowrap; - font-size: var(--title-3-em-font-size); height: calc(28 * var(--px-in-rem)); > * { diff --git a/special-pages/pages/new-tab/app/customizer/components/VisibilityMenuSection.js b/special-pages/pages/new-tab/app/customizer/components/VisibilityMenuSection.js new file mode 100644 index 000000000..4ffd4f059 --- /dev/null +++ b/special-pages/pages/new-tab/app/customizer/components/VisibilityMenuSection.js @@ -0,0 +1,32 @@ +import { useLayoutEffect, useState } from 'preact/hooks'; +import { Customizer, getItems } from './Customizer.js'; +import styles from './CustomizerDrawerInner.module.css'; +import { VisibilityMenu } from './VisibilityMenu.js'; +import { h } from 'preact'; + +export function VisibilityMenuSection() { + console.log(' RENDER:VisibilityMenuSection'); + const [rowData, setRowData] = useState(() => { + const items = /** @type {import("./Customizer.js").VisibilityRowData[]} */ (getItems()); + return items; + }); + useLayoutEffect(() => { + function handler() { + setRowData(getItems()); + } + + console.log('waitin..'); + window.addEventListener(Customizer.UPDATE_EVENT, handler); + return () => { + window.removeEventListener(Customizer.UPDATE_EVENT, handler); + }; + }, []); + return ( +
      +

      Sections

      +
      + +
      +
      + ); +} diff --git a/special-pages/pages/new-tab/app/customizer/customizer.md b/special-pages/pages/new-tab/app/customizer/customizer.md index 88f71c7ba..9c84460d4 100644 --- a/special-pages/pages/new-tab/app/customizer/customizer.md +++ b/special-pages/pages/new-tab/app/customizer/customizer.md @@ -22,13 +22,121 @@ title: Customizer ], "widgetConfigs": [ { "id": "favorites", "visibility": "visible" }, - { "id": "privacyStats", "visibility": "visible" }, + { "id": "privacyStats", "visibility": "visible" } ], "settings": { "customizerDrawer": { "state": "enabled" } + }, + "customizer": { + "userImages": [], + "theme": "dark", + "background": { "kind": "default" } } } ``` +## Initial Data + +- Add the key `customizer` to `initialSetup` +- The data takes the following form: {@link "NewTab Messages".CustomizerData} +- Example from `initialSetup` + +```json +{ + "...": "...", + "customizer": { + "userImages": [], + "theme": "dark", + "background": { "kind": "default" } + } +} +``` + +## Subscriptions + +- {@link "NewTab Messages".CustomizerOnBackgroundUpdateSubscription `customizer_onBackgroundUpdate`}. + - Sends {@link "NewTab Messages".CustomizerOnBackgroundUpdateSubscribe} whenever needed. + - For example: + - ```json + { + "background": { "kind": "color", "value": "color01" } + } + ``` + - ```json + { + "background": { "kind": "gradient", "value": "gradient01" } + } + ``` + - ```json + { + "background": { "kind": "hex", "value": "#cacaca" } + } + ``` + - ```json + { + "background": { "kind": "default" } + } + ``` + - ```json + { + "background": { + "kind": "userImage", + "value": { "id": "abc", "src": "...", "thumb": "...", "colorScheme": "light" } + } + } + ``` + +- {@link "NewTab Messages".CustomizerOnImagesUpdateSubscription `customizer_onImagesUpdate`}. + - Sends {@link "NewTab Messages".CustomizerOnImagesUpdateSubscribe} whenever needed. + - For example, this would be pushed into the page following a successful upload + - Note: In that situation, you'd send this followed by `customizer_onBackgroundUpdate` above + - For example: + - ```json + { + "userImages": [{"id": "abc", "src": "...", "thumb": "...", "colorScheme": "light" }] + } + ``` + +- {@link "NewTab Messages".CustomizerOnThemeUpdateSubscription `customizer_onThemeUpdate`}. + - Sends {@link "NewTab Messages".CustomizerOnThemeUpdateSubscribe} whenever needed. + - For example: + - ```json + { + "theme": "system" + } + ``` + +## Notifications + +- {@link "NewTab Messages".CustomizerSetBackgroundNotification `customizer_setBackground`}. + - Sends {@link "NewTab Messages".CustomizerSetBackgroundNotify} whenever needed. + - For example: + - ```json + { + "background": { "kind": "color", "value": "color01" } + } + ``` + +- {@link "NewTab Messages".CustomizerSetThemeNotification `customizer_setTheme`}. + - Sends {@link "NewTab Messages".CustomizerSetBackgroundNotify} whenever needed. + - For example: + - ```json + { + "theme": "light" + } + ``` + +- {@link "NewTab Messages".CustomizerUploadNotification `customizer_upload`}. + - Sent to trigger a file upload + + +- {@link "NewTab Messages".CustomizerDeleteImageNotification `customizer_deleteImage`}. + - Sends {@link "NewTab Messages".CustomizerDeleteImageNotify} whenever needed. + - For example: + - ```json + { + "id": "abc" + } + ``` diff --git a/special-pages/pages/new-tab/app/customizer/customizer.service.js b/special-pages/pages/new-tab/app/customizer/customizer.service.js new file mode 100644 index 000000000..c4adbc87f --- /dev/null +++ b/special-pages/pages/new-tab/app/customizer/customizer.service.js @@ -0,0 +1,110 @@ +/** + * @typedef {import("../../types/new-tab.js").CustomizerData} CustomizerData + */ +import { Service } from '../service.js'; + +/** + * @document ./customizer.md + */ + +export class CustomizerService { + /** + * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method. + * @param {CustomizerData} initial + * @internal + */ + constructor(ntp, initial) { + this.ntp = ntp; + /** @type {Service} */ + this.bgService = new Service( + { + subscribe: (cb) => ntp.messaging.subscribe('customizer_onBackgroundUpdate', cb), + persist: (data) => { + ntp.messaging.notify('customizer_setBackground', { background: data }); + }, + }, + initial.background, + ); + /** @type {Service} */ + this.themeService = new Service( + { + subscribe: (cb) => ntp.messaging.subscribe('customizer_onThemeUpdate', cb), + }, + initial.theme, + ); + /** @type {Service} */ + this.imagesService = new Service( + { + subscribe: (cb) => ntp.messaging.subscribe('customizer_onImagesUpdate', cb), + }, + initial.userImages, + ); + } + + /** + * @internal + */ + destroy() { + this.bgService.destroy(); + this.themeService.destroy(); + this.imagesService.destroy(); + } + + /** + * @param {(evt: {data: CustomizerData['background'], source: 'manual' | 'subscription'}) => void} cb + * @internal + */ + onBackground(cb) { + return this.bgService.onData(cb); + } + /** + * @param {(evt: {data: CustomizerData['theme'], source: 'manual' | 'subscription'}) => void} cb + * @internal + */ + onTheme(cb) { + return this.themeService.onData(cb); + } + /** + * @param {(evt: {data: CustomizerData['userImages'], source: 'manual' | 'subscription'}) => void} cb + * @internal + */ + onImages(cb) { + return this.imagesService.onData(cb); + } + + /** + * @param {CustomizerData['background']} bg + */ + setBackground(bg) { + this.bgService.update((data) => { + return bg; + }); + } + + /** + * @param {string} id + */ + deleteImage(id) { + this.imagesService.update((data) => { + return data.filter((img) => img.id !== id); + }); + this.ntp.messaging.notify('customizer_deleteImage', { id }); + } + + /** + * + */ + upload() { + this.ntp.messaging.notify('customizer_upload'); + } + + /** + * @param {import('../../types/new-tab').CustomizerData['theme']} theme + */ + setTheme(theme) { + this.themeService.update((_data) => { + return theme; + }); + this.ntp.messaging.notify('customizer_setTheme', { theme }); + } +} diff --git a/special-pages/pages/new-tab/app/customizer/values.js b/special-pages/pages/new-tab/app/customizer/values.js new file mode 100644 index 000000000..c49d10dc7 --- /dev/null +++ b/special-pages/pages/new-tab/app/customizer/values.js @@ -0,0 +1,153 @@ +/** + * @import { PredefinedColor, PredefinedGradient, BackgroundColorScheme, UserImage } from "../../types/new-tab" + * @type {{ + * colors: Record, + * gradients: Record + * userImages: Record<'01' | '02' | '03', UserImage> + * }} + */ +export const values = { + colors: { + color01: { hex: '#000000', colorScheme: 'dark' }, + color02: { hex: '#342E42', colorScheme: 'dark' }, + color03: { hex: '#4D5F7F', colorScheme: 'dark' }, + color04: { hex: '#E28499', colorScheme: 'light' }, + color05: { hex: '#F7DEE5', colorScheme: 'light' }, + color06: { hex: '#D55154', colorScheme: 'dark' }, + color07: { hex: '#E5724F', colorScheme: 'dark' }, + color08: { hex: '#F3BB44', colorScheme: 'light' }, + color09: { hex: '#E9DCCD', colorScheme: 'light' }, + color10: { hex: '#5BC787', colorScheme: 'light' }, + color11: { hex: '#4594A7', colorScheme: 'dark' }, + color12: { hex: '#B5E2CE', colorScheme: 'light' }, + color13: { hex: '#E4DEF2', colorScheme: 'light' }, + color14: { hex: '#B79ED4', colorScheme: 'light' }, + color15: { hex: '#5552AC', colorScheme: 'dark' }, + color16: { hex: '#75B9F0', colorScheme: 'light' }, + color17: { hex: '#577DE4', colorScheme: 'dark' }, + color18: { hex: '#DBDDDF', colorScheme: 'light' }, + color19: { hex: '#9A979D', colorScheme: 'dark' }, + }, + gradients: { + gradient01: { path: 'gradients/gradient01.svg', colorScheme: 'light' }, + gradient02: { path: 'gradients/gradient02.svg', colorScheme: 'light' }, + gradient03: { path: 'gradients/gradient03.svg', colorScheme: 'light' }, + gradient04: { path: 'gradients/gradient04.svg', colorScheme: 'light' }, + gradient05: { path: 'gradients/gradient05.svg', colorScheme: 'dark' }, + gradient06: { path: 'gradients/gradient06.svg', colorScheme: 'dark' }, + gradient07: { path: 'gradients/gradient07.svg', colorScheme: 'dark' }, + gradient08: { path: 'gradients/gradient08.svg', colorScheme: 'dark' }, + }, + userImages: { + '01': { + colorScheme: 'dark', + id: '01', + src: 'backgrounds/bg-01.jpg', + thumb: 'backgrounds/bg-01-thumb.jpg', + }, + '02': { + colorScheme: 'light', + id: '02', + src: 'backgrounds/bg-02.jpg', + thumb: 'backgrounds/bg-02-thumb.jpg', + }, + '03': { + colorScheme: 'light', + id: '03', + src: 'backgrounds/bg-03.jpg', + thumb: 'backgrounds/bg-03-thumb.jpg', + }, + }, +}; + +/** + * Determines if a light or dark theme should be used based on background color + * @param {string} backgroundColor - HEX color code (6 or 8 digits) + * @returns {'light' | 'dark'} - Returns 'light' or 'dark' + */ +export function detectTheme(backgroundColor) { + // Remove # if present and handle both 6 and 8 digit hex codes + const hex = backgroundColor.replace('#', ''); + + // Extract RGB values + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + + // Calculate relative luminance using sRGB coefficients + // Using the formula from WCAG 2.0 + const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b; + + // Choose theme based on luminance + // 128 is the middle value (255/2) + return luminance < 128 ? 'dark' : 'light'; +} + +// Test cases using Node's built-in assert +// const testCases = [ +// { +// input: '#FFFFFF', +// expected: 'light', +// description: 'Pure white should be light theme', +// }, +// { +// input: '#000000', +// expected: 'dark', +// description: 'Pure black should be dark theme', +// }, +// { +// input: '7B7B7B', +// expected: 'dark', +// description: 'Medium gray should be dark theme', +// }, +// { +// input: 'FFFFFF00', +// expected: 'light', +// description: 'White with alpha should be light theme', +// }, +// { +// input: '#1E90FF', +// expected: 'dark', +// description: 'Dodger blue should be dark theme', +// }, +// { +// input: '#FFD700', +// expected: 'light', +// description: 'Gold should be light theme', +// }, +// { +// input: '#98FB98', +// expected: 'light', +// description: 'Pale green should be light theme', +// }, +// { +// input: '#800080', +// expected: 'dark', +// description: 'Purple should be dark theme', +// }, +// { +// input: '#FFA07A', +// expected: 'light', +// description: 'Light salmon should be light theme', +// }, +// { +// input: '#2F4F4F', +// expected: 'dark', +// description: 'Dark slate gray should be dark theme', +// }, +// ]; + +// Run tests +// console.log('Running tests...\n'); +// testCases.forEach((testCase, index) => { +// try { +// const result = detectTheme(testCase.input); +// assert.strictEqual(result, testCase.expected); +// console.log(`✓ Test ${index + 1}: ${testCase.description}`); +// } catch (error) { +// console.error(`✗ Test ${index + 1}: ${testCase.description}`); +// console.error(` Expected: ${testCase.expected}`); +// // console.error(` Received: ${result}`); +// console.error(` Input: ${testCase.input}\n`); +// } +// }); diff --git a/special-pages/pages/new-tab/app/index.js b/special-pages/pages/new-tab/app/index.js index bcf5e1653..8fd88cfb8 100644 --- a/special-pages/pages/new-tab/app/index.js +++ b/special-pages/pages/new-tab/app/index.js @@ -13,6 +13,8 @@ import { Settings } from './settings.js'; import { Components } from './components/Components.jsx'; import { widgetEntryPoint } from './widget-list/WidgetList.js'; import { callWithRetry } from '../../../shared/call-with-retry.js'; +import { CustomizerProvider } from './customizer/CustomizerProvider.js'; +import { CustomizerService } from './customizer/customizer.service.js'; /** * @import {Telemetry} from "./telemetry/telemetry.js" @@ -82,6 +84,7 @@ export async function init(root, messaging, telemetry, baseEnvironment) { // Create an instance of the global widget api const widgetConfigAPI = new WidgetConfigService(messaging, init.widgetConfigs); + const customizerApi = new CustomizerService(messaging, init.customizer); render( - - - + + + + + diff --git a/special-pages/pages/new-tab/app/mock-transport.js b/special-pages/pages/new-tab/app/mock-transport.js index dfc53b031..1af343877 100644 --- a/special-pages/pages/new-tab/app/mock-transport.js +++ b/special-pages/pages/new-tab/app/mock-transport.js @@ -5,6 +5,7 @@ import { rmfDataExamples } from './remote-messaging-framework/mocks/rmf.data.js' import { favorites, gen } from './favorites/mocks/favorites.data.js'; import { updateNotificationExamples } from './update-notification/mocks/update-notification.data.js'; import { variants as nextSteps } from './next-steps/nextsteps.data.js'; +import { values } from './customizer/values.js'; /** * @typedef {import('../types/new-tab').Favorite} Favorite @@ -465,6 +466,7 @@ export function mockTransport() { env: 'development', locale: 'en', updateNotification, + customizer: customizerData(), }; return Promise.resolve(initial); @@ -477,6 +479,56 @@ export function mockTransport() { }); } +/** @type {()=>import('../types/new-tab').CustomizerData} */ +function customizerData() { + /** @type {import('../types/new-tab').CustomizerData} */ + const customizer = { + userImages: [], + theme: 'dark', + background: { kind: 'default' }, + }; + + if (url.searchParams.has('background')) { + const value = url.searchParams.get('background'); + if (value && value in values.colors) { + customizer.background = { + kind: 'color', + value: /** @type {import('../types/new-tab').PredefinedColor} */ (value), + }; + } else if (value && value in values.gradients) { + customizer.background = { + kind: 'gradient', + value: /** @type {import('../types/new-tab').PredefinedGradient} */ (value), + }; + } else if (value && value.startsWith('hex:')) { + const hex = value.slice(4); + if (hex.length === 6 || hex.length === 8) { + customizer.background = { + kind: 'hex', + value: `#${hex.slice(0, 6)}`, + }; + } else { + console.warn('invalid hex values'); + } + } else if (value && value.startsWith('userImage:')) { + const image = value.slice(10); + if (image in values.userImages) { + customizer.background = { + kind: 'userImage', + value: values.userImages[image], + }; + } else { + console.warn('unknown user image'); + } + } + } + + if (url.searchParams.has('userImages')) { + customizer.userImages = [values.userImages['01'], values.userImages['02'], values.userImages['03']]; + } + return customizer; +} + /** * @template {{id: string}} T * @param {T[]} array diff --git a/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css index 9f2dd8580..0d3f4361e 100644 --- a/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css +++ b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css @@ -1,5 +1,6 @@ .root { background: var(--ntp-surface-background-color); + backdrop-filter: blur(48px); border: 1px solid var(--ntp-surface-border-color); padding: var(--sp-6); border-radius: var(--border-radius-lg); diff --git a/special-pages/pages/new-tab/app/styles/ntp-theme.css b/special-pages/pages/new-tab/app/styles/ntp-theme.css index 8be2d183c..3417b08dc 100644 --- a/special-pages/pages/new-tab/app/styles/ntp-theme.css +++ b/special-pages/pages/new-tab/app/styles/ntp-theme.css @@ -1,6 +1,6 @@ :root { --ntp-background-color: white; - --ntp-surface-background-color: white; + --ntp-surface-background-color: rgba(255, 255, 255, 0.30); --ntp-surfaces-panel-background-color: white; --ntp-surface-border-color: var(--color-black-at-6); --ntp-text-normal: var(--color-black-at-84); @@ -25,6 +25,11 @@ --title-3-em-font-weight: 590; --title-3-em-line-height: 20px; + /* label small */ + --small-label-font-size: 11px; + --small-label-font-weight: 400; + --small-label-line-height: 11px; + --ntp-focus-outline-color: black; --border-radius-lg: 12px; --border-radius-md: 8px; @@ -36,7 +41,7 @@ @media (prefers-color-scheme: dark) { --ntp-background-color: var(--color-gray-85); - --ntp-surface-background-color: #2a2a2a; + --ntp-surface-background-color: rgba(0, 0, 0, 0.18); --ntp-surfaces-panel-background-color: #222222; --ntp-surface-border-color: var(--color-white-at-6); --ntp-text-normal: var(--color-white-at-84); 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 3868a8b6e..9426b5289 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 @@ -40,6 +40,7 @@ export class NewtabPage { name: this.platform.name || 'windows', }, updateNotification: { content: null }, + customizer: { theme: 'system', userImages: [], background: { kind: 'default' } }, }, stats_getConfig: {}, stats_getData: {}, diff --git a/special-pages/pages/new-tab/messages/customizer_deleteImage.notify.json b/special-pages/pages/new-tab/messages/customizer_deleteImage.notify.json new file mode 100644 index 000000000..7d778cdd7 --- /dev/null +++ b/special-pages/pages/new-tab/messages/customizer_deleteImage.notify.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + } + } +} diff --git a/special-pages/pages/new-tab/messages/customizer_getData.request.json b/special-pages/pages/new-tab/messages/customizer_getData.request.json new file mode 100644 index 000000000..0af74a319 --- /dev/null +++ b/special-pages/pages/new-tab/messages/customizer_getData.request.json @@ -0,0 +1,3 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#" +} diff --git a/special-pages/pages/new-tab/messages/customizer_getData.response.json b/special-pages/pages/new-tab/messages/customizer_getData.response.json new file mode 100644 index 000000000..60d1826a3 --- /dev/null +++ b/special-pages/pages/new-tab/messages/customizer_getData.response.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": [ + { + "$ref": "./types/customizer-data.json" + } + ] +} diff --git a/special-pages/pages/new-tab/messages/customizer_onBackgroundUpdate.subscribe.json b/special-pages/pages/new-tab/messages/customizer_onBackgroundUpdate.subscribe.json new file mode 100644 index 000000000..2fc721e9a --- /dev/null +++ b/special-pages/pages/new-tab/messages/customizer_onBackgroundUpdate.subscribe.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["background"], + "properties": { + "background": { "$ref": "./types/background.json" } + } +} diff --git a/special-pages/pages/new-tab/messages/customizer_onImagesUpdate.subscribe.json b/special-pages/pages/new-tab/messages/customizer_onImagesUpdate.subscribe.json new file mode 100644 index 000000000..a7471a245 --- /dev/null +++ b/special-pages/pages/new-tab/messages/customizer_onImagesUpdate.subscribe.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "userImages" + ], + "properties": { + "userImages": { + "type": "array", + "items": { + "$ref": "./types/user-image.json" + } + } + } +} diff --git a/special-pages/pages/new-tab/messages/customizer_onThemeUpdate.subscribe.json b/special-pages/pages/new-tab/messages/customizer_onThemeUpdate.subscribe.json new file mode 100644 index 000000000..a3ee380b1 --- /dev/null +++ b/special-pages/pages/new-tab/messages/customizer_onThemeUpdate.subscribe.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "theme" + ], + "properties": { + "theme": { + "$ref": "types/browser-theme.json" + } + } +} diff --git a/special-pages/pages/new-tab/messages/customizer_setBackground.notify.json b/special-pages/pages/new-tab/messages/customizer_setBackground.notify.json new file mode 100644 index 000000000..b3df277aa --- /dev/null +++ b/special-pages/pages/new-tab/messages/customizer_setBackground.notify.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "background" + ], + "properties": { + "background": { + "$ref": "./types/background.json" + } + } +} diff --git a/special-pages/pages/new-tab/messages/customizer_setTheme.notify.json b/special-pages/pages/new-tab/messages/customizer_setTheme.notify.json new file mode 100644 index 000000000..8acd7bb2a --- /dev/null +++ b/special-pages/pages/new-tab/messages/customizer_setTheme.notify.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["theme"], + "properties": { + "theme": { + "$ref": "types/browser-theme.json" + } + } +} diff --git a/special-pages/pages/new-tab/messages/customizer_upload.notify.json b/special-pages/pages/new-tab/messages/customizer_upload.notify.json new file mode 100644 index 000000000..0af74a319 --- /dev/null +++ b/special-pages/pages/new-tab/messages/customizer_upload.notify.json @@ -0,0 +1,3 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#" +} diff --git a/special-pages/pages/new-tab/messages/examples/widgets.js b/special-pages/pages/new-tab/messages/examples/widgets.js index e3ff48d08..7c903487b 100644 --- a/special-pages/pages/new-tab/messages/examples/widgets.js +++ b/special-pages/pages/new-tab/messages/examples/widgets.js @@ -41,6 +41,7 @@ const initialSetupResponse = { locale: 'en', platform: { name: 'windows' }, updateNotification: { content: null }, + customizer: { theme: 'system', userImages: [], background: { kind: 'default' } }, }; export {}; diff --git a/special-pages/pages/new-tab/messages/initialSetup.response.json b/special-pages/pages/new-tab/messages/initialSetup.response.json index b08fb9352..95a7ba5a1 100644 --- a/special-pages/pages/new-tab/messages/initialSetup.response.json +++ b/special-pages/pages/new-tab/messages/initialSetup.response.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", - "required": ["widgets", "widgetConfigs", "locale", "env", "platform", "updateNotification"], + "required": ["widgets", "widgetConfigs", "locale", "env", "platform", "updateNotification", "customizer"], "properties": { "widgets": { "$ref": "types/widget-list.json" @@ -29,6 +29,9 @@ } } }, + "customizer": { + "$ref": "./types/customizer-data.json" + }, "updateNotification": { "oneOf": [ { diff --git a/special-pages/pages/new-tab/messages/types/background.json b/special-pages/pages/new-tab/messages/types/background.json new file mode 100644 index 000000000..fef143870 --- /dev/null +++ b/special-pages/pages/new-tab/messages/types/background.json @@ -0,0 +1,80 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Background Variant", + "oneOf": [ + { + "type": "object", + "required": ["kind"], + "title": "Default Background", + "properties": { + "kind": { + "const": "default" + } + } + }, + { + "type": "object", + "required": [ + "kind", + "value" + ], + "title": "Solid Color Background", + "properties": { + "kind": { + "const": "color" + }, + "value": { + "$ref": "./colors.json#/definitions/colors" + } + } + }, + { + "type": "object", + "required": [ + "kind", + "value" + ], + "title": "Hex Value Background", + "properties": { + "kind": { + "const": "hex" + }, + "value": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "kind", + "value" + ], + "title": "Gradient Background", + "properties": { + "kind": { + "const": "gradient" + }, + "value": { + "$ref": "./colors.json#/definitions/gradients" + } + } + }, + { + "type": "object", + "required": [ + "kind", + "value" + ], + "title": "User Image Background", + "properties": { + "kind": { + "const": "userImage" + }, + "value": { + "$ref": "./user-image.json" + } + } + } + ] +} diff --git a/special-pages/pages/new-tab/messages/types/browser-theme.json b/special-pages/pages/new-tab/messages/types/browser-theme.json new file mode 100644 index 000000000..1ce532afb --- /dev/null +++ b/special-pages/pages/new-tab/messages/types/browser-theme.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Browser Theme", + "enum": [ + "light", + "dark", + "system" + ] +} diff --git a/special-pages/pages/new-tab/messages/types/colors.json b/special-pages/pages/new-tab/messages/types/colors.json new file mode 100644 index 000000000..05f44d2a0 --- /dev/null +++ b/special-pages/pages/new-tab/messages/types/colors.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "gradients": { + "title": "Predefined Gradient", + "enum": [ + "gradient01", + "gradient02", + "gradient03", + "gradient04", + "gradient05", + "gradient06", + "gradient07", + "gradient08" + ] + }, + "colors": { + "title": "Predefined Color", + "enum": [ + "color01", + "color02", + "color03", + "color04", + "color05", + "color06", + "color07", + "color08", + "color09", + "color10", + "color11", + "color12", + "color13", + "color14", + "color15", + "color16", + "color17", + "color18", + "color19" + ] + }, + "colorScheme": { + "title": "Background Color Scheme", + "description": "Note: this is different to the Browser Theme", + "enum": ["light", "dark"] + } + } +} diff --git a/special-pages/pages/new-tab/messages/types/customizer-data.json b/special-pages/pages/new-tab/messages/types/customizer-data.json new file mode 100644 index 000000000..1ca4fbb83 --- /dev/null +++ b/special-pages/pages/new-tab/messages/types/customizer-data.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Customizer Data", + "type": "object", + "required": [ + "background", + "theme", + "userImages" + ], + "properties": { + "background": {"$ref": "./background.json"}, + "theme": { "$ref": "./browser-theme.json" }, + "userImages": { + "type": "array", + "items": { + "$ref": "./user-image.json" + } + } + } +} diff --git a/special-pages/pages/new-tab/messages/types/user-image.json b/special-pages/pages/new-tab/messages/types/user-image.json new file mode 100644 index 000000000..1736f4413 --- /dev/null +++ b/special-pages/pages/new-tab/messages/types/user-image.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UserImage", + "type": "object", + "required": ["id", "colorScheme", "src", "thumb"], + "properties": { + "id": { + "type": "string" + }, + "src": { + "type": "string" + }, + "thumb": { + "type": "string" + }, + "colorScheme": { + "$ref": "./colors.json#/definitions/colorScheme" + } + } +} diff --git a/special-pages/pages/new-tab/src/backgrounds/bg-01-thumb.jpg b/special-pages/pages/new-tab/src/backgrounds/bg-01-thumb.jpg new file mode 100644 index 000000000..6e0da17af Binary files /dev/null and b/special-pages/pages/new-tab/src/backgrounds/bg-01-thumb.jpg differ diff --git a/special-pages/pages/new-tab/src/backgrounds/bg-01.jpg b/special-pages/pages/new-tab/src/backgrounds/bg-01.jpg new file mode 100644 index 000000000..a4af5d0fb Binary files /dev/null and b/special-pages/pages/new-tab/src/backgrounds/bg-01.jpg differ diff --git a/special-pages/pages/new-tab/src/backgrounds/bg-02-thumb.jpg b/special-pages/pages/new-tab/src/backgrounds/bg-02-thumb.jpg new file mode 100644 index 000000000..d39ec6fcb Binary files /dev/null and b/special-pages/pages/new-tab/src/backgrounds/bg-02-thumb.jpg differ diff --git a/special-pages/pages/new-tab/src/backgrounds/bg-02.jpg b/special-pages/pages/new-tab/src/backgrounds/bg-02.jpg new file mode 100644 index 000000000..c1bf4a0e3 Binary files /dev/null and b/special-pages/pages/new-tab/src/backgrounds/bg-02.jpg differ diff --git a/special-pages/pages/new-tab/src/backgrounds/bg-03-thumb.jpg b/special-pages/pages/new-tab/src/backgrounds/bg-03-thumb.jpg new file mode 100644 index 000000000..06bf64d7a Binary files /dev/null and b/special-pages/pages/new-tab/src/backgrounds/bg-03-thumb.jpg differ diff --git a/special-pages/pages/new-tab/src/backgrounds/bg-03.jpg b/special-pages/pages/new-tab/src/backgrounds/bg-03.jpg new file mode 100644 index 000000000..c7265d5f2 Binary files /dev/null and b/special-pages/pages/new-tab/src/backgrounds/bg-03.jpg differ diff --git a/special-pages/pages/new-tab/src/gradients/gradient01.svg b/special-pages/pages/new-tab/src/gradients/gradient01.svg new file mode 100644 index 000000000..a9fcd0672 --- /dev/null +++ b/special-pages/pages/new-tab/src/gradients/gradient01.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/special-pages/pages/new-tab/src/gradients/gradient02.svg b/special-pages/pages/new-tab/src/gradients/gradient02.svg new file mode 100644 index 000000000..de7ab3b7e --- /dev/null +++ b/special-pages/pages/new-tab/src/gradients/gradient02.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/special-pages/pages/new-tab/src/gradients/gradient03.svg b/special-pages/pages/new-tab/src/gradients/gradient03.svg new file mode 100644 index 000000000..0639b84c2 --- /dev/null +++ b/special-pages/pages/new-tab/src/gradients/gradient03.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/special-pages/pages/new-tab/src/gradients/gradient04.svg b/special-pages/pages/new-tab/src/gradients/gradient04.svg new file mode 100644 index 000000000..9843eade1 --- /dev/null +++ b/special-pages/pages/new-tab/src/gradients/gradient04.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/special-pages/pages/new-tab/src/gradients/gradient05.svg b/special-pages/pages/new-tab/src/gradients/gradient05.svg new file mode 100644 index 000000000..eee5ce1a3 --- /dev/null +++ b/special-pages/pages/new-tab/src/gradients/gradient05.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/special-pages/pages/new-tab/src/gradients/gradient06.svg b/special-pages/pages/new-tab/src/gradients/gradient06.svg new file mode 100644 index 000000000..a91178e5f --- /dev/null +++ b/special-pages/pages/new-tab/src/gradients/gradient06.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/special-pages/pages/new-tab/src/gradients/gradient07.svg b/special-pages/pages/new-tab/src/gradients/gradient07.svg new file mode 100644 index 000000000..1527605f9 --- /dev/null +++ b/special-pages/pages/new-tab/src/gradients/gradient07.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/special-pages/pages/new-tab/src/gradients/gradient08.svg b/special-pages/pages/new-tab/src/gradients/gradient08.svg new file mode 100644 index 000000000..a7c77f9a4 --- /dev/null +++ b/special-pages/pages/new-tab/src/gradients/gradient08.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/special-pages/pages/new-tab/src/gradients/grain.png b/special-pages/pages/new-tab/src/gradients/grain.png new file mode 100644 index 000000000..8d3ef5245 Binary files /dev/null and b/special-pages/pages/new-tab/src/gradients/grain.png differ diff --git a/special-pages/pages/new-tab/types/new-tab.ts b/special-pages/pages/new-tab/types/new-tab.ts index cae06d2cc..9f19c301d 100644 --- a/special-pages/pages/new-tab/types/new-tab.ts +++ b/special-pages/pages/new-tab/types/new-tab.ts @@ -6,6 +6,46 @@ * @module NewTab Messages */ +export type BackgroundVariant = + | DefaultBackground + | SolidColorBackground + | HexValueBackground + | GradientBackground + | UserImageBackground; +export type PredefinedColor = + | "color01" + | "color02" + | "color03" + | "color04" + | "color05" + | "color06" + | "color07" + | "color08" + | "color09" + | "color10" + | "color11" + | "color12" + | "color13" + | "color14" + | "color15" + | "color16" + | "color17" + | "color18" + | "color19"; +export type PredefinedGradient = + | "gradient01" + | "gradient02" + | "gradient03" + | "gradient04" + | "gradient05" + | "gradient06" + | "gradient07" + | "gradient08"; +/** + * Note: this is different to the Browser Theme + */ +export type BackgroundColorScheme = "light" | "dark"; +export type BrowserTheme = "light" | "dark" | "system"; /** * Represents the expansion state of a widget */ @@ -45,6 +85,10 @@ export type RMFIcon = "Announce" | "DDGAnnounce" | "CriticalUpdate" | "AppUpdate export interface NewTabMessages { notifications: | ContextMenuNotification + | CustomizerDeleteImageNotification + | CustomizerSetBackgroundNotification + | CustomizerSetThemeNotification + | CustomizerUploadNotification | FavoritesAddNotification | FavoritesMoveNotification | FavoritesOpenNotification @@ -65,6 +109,7 @@ export interface NewTabMessages { | UpdateNotificationDismissNotification | WidgetsSetConfigNotification; requests: + | CustomizerGetDataRequest | FavoritesGetConfigRequest | FavoritesGetDataRequest | InitialSetupRequest @@ -74,6 +119,9 @@ export interface NewTabMessages { | StatsGetConfigRequest | StatsGetDataRequest; subscriptions: + | CustomizerOnBackgroundUpdateSubscription + | CustomizerOnImagesUpdateSubscription + | CustomizerOnThemeUpdateSubscription | FavoritesOnConfigUpdateSubscription | FavoritesOnDataUpdateSubscription | NextStepsOnConfigUpdateSubscription @@ -101,6 +149,67 @@ export interface VisibilityMenuItem { */ title: string; } +/** + * Generated from @see "../messages/customizer_deleteImage.notify.json" + */ +export interface CustomizerDeleteImageNotification { + method: "customizer_deleteImage"; + params: CustomizerDeleteImageNotify; +} +export interface CustomizerDeleteImageNotify { + id: string; +} +/** + * Generated from @see "../messages/customizer_setBackground.notify.json" + */ +export interface CustomizerSetBackgroundNotification { + method: "customizer_setBackground"; + params: CustomizerSetBackgroundNotify; +} +export interface CustomizerSetBackgroundNotify { + background: BackgroundVariant; +} +export interface DefaultBackground { + kind: "default"; +} +export interface SolidColorBackground { + kind: "color"; + value: PredefinedColor; +} +export interface HexValueBackground { + kind: "hex"; + value: string; +} +export interface GradientBackground { + kind: "gradient"; + value: PredefinedGradient; +} +export interface UserImageBackground { + kind: "userImage"; + value: UserImage; +} +export interface UserImage { + id: string; + src: string; + thumb: string; + colorScheme: BackgroundColorScheme; +} +/** + * Generated from @see "../messages/customizer_setTheme.notify.json" + */ +export interface CustomizerSetThemeNotification { + method: "customizer_setTheme"; + params: CustomizerSetThemeNotify; +} +export interface CustomizerSetThemeNotify { + theme: BrowserTheme; +} +/** + * Generated from @see "../messages/customizer_upload.notify.json" + */ +export interface CustomizerUploadNotification { + method: "customizer_upload"; +} /** * Generated from @see "../messages/favorites_add.notify.json" */ @@ -326,6 +435,18 @@ export interface WidgetConfigItem { id: string; visibility: WidgetVisibility; } +/** + * Generated from @see "../messages/customizer_getData.request.json" + */ +export interface CustomizerGetDataRequest { + method: "customizer_getData"; + result: CustomizerData; +} +export interface CustomizerData { + background: BackgroundVariant; + theme: BrowserTheme; + userImages: UserImage[]; +} /** * Generated from @see "../messages/favorites_getConfig.request.json" */ @@ -369,6 +490,7 @@ export interface InitialSetupResponse { platform: { name: "macos" | "windows" | "android" | "ios" | "integration"; }; + customizer: CustomizerData; updateNotification: null | UpdateNotificationData; } export interface WidgetListItem { @@ -474,6 +596,36 @@ export interface TrackerCompany { displayName: string; count: number; } +/** + * Generated from @see "../messages/customizer_onBackgroundUpdate.subscribe.json" + */ +export interface CustomizerOnBackgroundUpdateSubscription { + subscriptionEvent: "customizer_onBackgroundUpdate"; + params: CustomizerOnBackgroundUpdateSubscribe; +} +export interface CustomizerOnBackgroundUpdateSubscribe { + background: BackgroundVariant; +} +/** + * Generated from @see "../messages/customizer_onImagesUpdate.subscribe.json" + */ +export interface CustomizerOnImagesUpdateSubscription { + subscriptionEvent: "customizer_onImagesUpdate"; + params: CustomizerOnImagesUpdateSubscribe; +} +export interface CustomizerOnImagesUpdateSubscribe { + userImages: UserImage[]; +} +/** + * Generated from @see "../messages/customizer_onThemeUpdate.subscribe.json" + */ +export interface CustomizerOnThemeUpdateSubscription { + subscriptionEvent: "customizer_onThemeUpdate"; + params: CustomizerOnThemeUpdateSubscribe; +} +export interface CustomizerOnThemeUpdateSubscribe { + theme: BrowserTheme; +} /** * Generated from @see "../messages/favorites_onConfigUpdate.subscribe.json" */