From 8f72543bfaeaf3d3319ca686cdf5108bec035908 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 11 Jun 2025 20:59:06 +0100 Subject: [PATCH 01/16] ntp: +search --- .../new-tab/app/components/App.module.css | 2 +- .../pages/new-tab/app/entry-points/search.js | 11 ++ .../app/favorites/components/Favorites.js | 4 +- .../favorites/components/Favorites.module.css | 2 +- .../pages/new-tab/app/mock-transport.js | 1 + .../app/protections/components/Protections.js | 2 +- .../new-tab/app/search/components/Search.js | 77 ++++++++++ .../app/search/components/Search.module.css | 57 +++++++ .../app/search/components/SearchInput.js | 112 ++++++++++++++ .../search/components/SearchInput.module.css | 140 ++++++++++++++++++ .../pages/new-tab/app/styles/ntp-theme.css | 8 + .../new-tab/public/icons/search/Logo.svg | 15 ++ .../new-tab/public/icons/search/Logotype.svg | 12 ++ 13 files changed, 438 insertions(+), 5 deletions(-) create mode 100644 special-pages/pages/new-tab/app/entry-points/search.js create mode 100644 special-pages/pages/new-tab/app/search/components/Search.js create mode 100644 special-pages/pages/new-tab/app/search/components/Search.module.css create mode 100644 special-pages/pages/new-tab/app/search/components/SearchInput.js create mode 100644 special-pages/pages/new-tab/app/search/components/SearchInput.module.css create mode 100644 special-pages/pages/new-tab/public/icons/search/Logo.svg create mode 100644 special-pages/pages/new-tab/public/icons/search/Logotype.svg 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 d3c15f21c7..9d678319ad 100644 --- a/special-pages/pages/new-tab/app/components/App.module.css +++ b/special-pages/pages/new-tab/app/components/App.module.css @@ -29,7 +29,7 @@ body[data-animate-background="true"] { :global(.layout-centered) { margin-inline: auto; width: 100%; - max-width: calc(504 * var(--px-in-rem)); + max-width: calc(var(--default-ntp-tube-width) * var(--px-in-rem)); } :global(.vertical-space) { diff --git a/special-pages/pages/new-tab/app/entry-points/search.js b/special-pages/pages/new-tab/app/entry-points/search.js new file mode 100644 index 0000000000..e001fa2b0f --- /dev/null +++ b/special-pages/pages/new-tab/app/entry-points/search.js @@ -0,0 +1,11 @@ +import { h } from 'preact'; +import { Centered } from '../components/Layout.js'; +import { Search } from '../search/components/Search.js'; + +export function factory() { + return ( + + + + ); +} diff --git a/special-pages/pages/new-tab/app/favorites/components/Favorites.js b/special-pages/pages/new-tab/app/favorites/components/Favorites.js index 0861b5a4e2..b241918757 100644 --- a/special-pages/pages/new-tab/app/favorites/components/Favorites.js +++ b/special-pages/pages/new-tab/app/favorites/components/Favorites.js @@ -20,7 +20,7 @@ import { useDocumentVisibility } from '../../../../../shared/components/Document * @typedef {import('../../../types/new-tab.js').FavoritesOpenAction['target']} OpenTarget */ export const FavoritesMemo = memo(Favorites); -export const ROW_CAPACITY = 6; +export const ROW_CAPACITY = 8; /** * Note: These values MUST match exactly what's defined in the CSS. */ @@ -65,7 +65,7 @@ export function Favorites({ favorites, expansion, toggle, openContextMenu, openF return ( -
+
+
{ + mode.value = next; + }); + } + + return ( +
+
+ Search + Search +
+
+
+ + +
+
+ +
+ ); +} + +export function SearchIcon() { + return ( + + + + ); +} + +export function DuckAiIcon() { + return ( + + + + + + + + + + + + ); +} diff --git a/special-pages/pages/new-tab/app/search/components/Search.module.css b/special-pages/pages/new-tab/app/search/components/Search.module.css new file mode 100644 index 0000000000..6b5c8f8029 --- /dev/null +++ b/special-pages/pages/new-tab/app/search/components/Search.module.css @@ -0,0 +1,57 @@ +.root { + margin-bottom: var(--ntp-gap); +} +.icons { + display: flex; + flex-direction: column; + align-items: center; +} +.iconSearch { + width: 84px; +} +.iconText { + width: 144px; + margin-top: 12px; +} +.wrap { + display: flex; + justify-content: center; + margin-top: 24px; +} +.pillSwitcher { + display: flex; + background: var(--color-black-at-6); + padding: 4px; + border-radius: 999px; + gap: 4px; + width: auto; +} +.pillOption { + padding: 8px 16px; + border: none; + background: transparent; + cursor: pointer; + border-radius: 99px; + white-space: nowrap; + display: flex; + align-items: center; + gap: 6px; + font-weight: 600; + + svg { + width: 16px; + height: 16px; + } +} + +.pillOption.active { + background: var(--color-white); + /* Shadows/On Light/Elevation 10 */ + box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.08), 0px 2px 4px 0px rgba(0, 0, 0, 0.10); + svg[data-name="search"] { + color: blue; + } + svg[data-name="duckai"] { + color: var(--duckai-purple); + } +} \ No newline at end of file diff --git a/special-pages/pages/new-tab/app/search/components/SearchInput.js b/special-pages/pages/new-tab/app/search/components/SearchInput.js new file mode 100644 index 0000000000..c9556dbf85 --- /dev/null +++ b/special-pages/pages/new-tab/app/search/components/SearchInput.js @@ -0,0 +1,112 @@ +import cn from 'classnames'; +import { h } from 'preact'; +import styles from './SearchInput.module.css'; +import { DuckAiIcon, SearchIcon } from './Search.js'; +import { useEffect, useRef } from 'preact/hooks'; + +/** + * Renders a search input component with optional mode-based controls and tab switching functionality. + * + * @param {Object} props - The props for the SearchInput component. + * @param {import('@preact/signals').Signal<'ai' | 'search'>} props.mode - The mode in which the component operates. Determines the presence of additional controls. + */ +export function SearchInput({ mode }) { + const inputRef = useRef(/** @type {HTMLInputElement|null} */ (null)); + + useEffect(() => { + return mode.subscribe(() => { + inputRef.current?.focus(); + }); + }, [mode]); + + return ( +
+
+ + {mode.value === 'search' && ( +
+ +
+ +
+ )} + {mode.value === 'ai' && ( +
+ +
+ )} +
+ {mode.value === 'ai' && ( +
+ + +
+ )} +
+ ); +} + +function Arrow() { + return ( + + + + ); +} + +function Plus() { + return ( + + + + ); +} + +function Opts() { + return ( + + + + + ); +} diff --git a/special-pages/pages/new-tab/app/search/components/SearchInput.module.css b/special-pages/pages/new-tab/app/search/components/SearchInput.module.css new file mode 100644 index 0000000000..38538266b5 --- /dev/null +++ b/special-pages/pages/new-tab/app/search/components/SearchInput.module.css @@ -0,0 +1,140 @@ +.root { + position: relative; + width: 100%; + margin: 0 auto; + border-radius: 16px; + background-color: #fff; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + margin-top: 16px; + padding: 8px 8px; + display: flex; + flex-direction: column; + gap: 8px; +} + +/*@keyframes grow-y {*/ +/* from {*/ +/* transform: scaleY(0);*/ +/* }*/ +/* to {*/ +/* transform: scaleY(1);*/ +/* }*/ +/*}*/ + +/*@keyframes shrink-y {*/ +/* from {*/ +/* transform: scaleY(1);*/ +/* }*/ +/* to {*/ +/* transform: scaleY(0);*/ +/* }*/ +/*}*/ + +/*::view-transition-old(search-input-transition) {*/ +/* animation: 0.25s linear both shrink-y;*/ +/*}*/ + +::view-transition-new(search-input-transition) { + animation: 0.25s grow-y; +} + +.searchContainer { + position: relative; + display: flex; + align-items: center; + height: 32px; +} + +.searchInput { + flex: 1; + height: 32px; + padding: 0; + padding: 0 8px; + border: none; + font-size: 16px; + outline: none; + color: #202124; + background-color: transparent; +} + +.searchInput::placeholder { + color: #9aa0a6; +} + +.searchActions { + display: flex; + align-items: center; +} + +.searchTypeButton { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 32px; + border: none; + background-color: transparent; + cursor: pointer; + transition: background-color 0.2s; + padding: 0; + + svg { + width: 16px; + height: 16px; + } + + svg[data-name="duckai"] { + color: var(--color-black-at-84); + } +} + +.searchTypeButton:hover { + background-color: rgba(60, 64, 67, 0.08); +} + +.searchTypeButton.active { + color: #1a73e8; +} + +.separator { + height: 24px; + width: 1px; + background-color: #dadce0; + margin: 0 8px; +} + +.secondaryControls { + height: 40px; + display: flex; + gap: 8px; +} + +.squareButton { + width: 40px; + height: 40px; + border: none; + background-color: transparent; + cursor: pointer; + transition: background-color 0.2s; + padding: 0; + border-radius: 8px; + position: relative; + svg { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } +} + +.submit { + background-color: var(--duckai-purple); + color: #fff; + position: relative; + top: 4px; +} + +.buttonSecondary { + background-color: rgba(0, 0, 0, 0.06); + color: var(--color-black-at-84); +} \ No newline at end of file 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 795b90eae4..b128cee42b 100644 --- a/special-pages/pages/new-tab/app/styles/ntp-theme.css +++ b/special-pages/pages/new-tab/app/styles/ntp-theme.css @@ -35,13 +35,21 @@ --border-radius-xs: 4px; --focus-ring: 0px 0px 0px 1px var(--color-white), 0px 0px 0px 3px var(--ntp-focus-outline-color); --focus-ring-thin: 0px 0px 0px 1px var(--ntp-focus-outline-color), 0px 0px 0px 1px var(--color-white); + + --duckai-purple: rgba(107, 78, 186, 1); } body { --default-light-background-color: var(--color-gray-0); --default-dark-background-color: var(--color-gray-85); + --default-ntp-tube-width: 504; + --default-favorites-count: 6; } +body:has([data-entry-point="search"]) { + --default-ntp-tube-width: 680; + --default-favorites-count: 8; +} [data-theme=light] { --ntp-surface-background-color: var(--color-white-at-30); diff --git a/special-pages/pages/new-tab/public/icons/search/Logo.svg b/special-pages/pages/new-tab/public/icons/search/Logo.svg new file mode 100644 index 0000000000..8583f54763 --- /dev/null +++ b/special-pages/pages/new-tab/public/icons/search/Logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/special-pages/pages/new-tab/public/icons/search/Logotype.svg b/special-pages/pages/new-tab/public/icons/search/Logotype.svg new file mode 100644 index 0000000000..0ff8348480 --- /dev/null +++ b/special-pages/pages/new-tab/public/icons/search/Logotype.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + From 3134fc220feb40d0937b240f38c2fbc9907adb32 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 11 Jun 2025 21:01:39 +0100 Subject: [PATCH 02/16] relative icons --- special-pages/pages/new-tab/app/search/components/Search.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index 6e7d153ae5..da69ece551 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -17,8 +17,8 @@ export function Search() { return (
- Search - Search + Search + Search
From 55cc64284d97c83ce20c68f8325ee0cf35e0566d Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Thu, 12 Jun 2025 11:05:44 +0100 Subject: [PATCH 03/16] basic docs --- .../pages/new-tab/app/mock-transport.js | 2 + special-pages/pages/new-tab/app/new-tab.md | 1 + .../new-tab/app/search/components/Search.js | 34 +- .../app/search/components/Search.module.css | 13 + .../app/search/components/SearchInput.js | 304 ++++++++++++++++-- .../search/components/SearchInput.module.css | 4 + .../app/search/mocks/getSuggestions.json | 87 +++++ .../app/search/mocks/search.mock-transport.js | 35 ++ .../new-tab/app/search/mocks/search.mocks.js | 128 ++++++++ .../pages/new-tab/app/search/search.md | 31 ++ .../search_getSuggestions.request.json | 10 + .../search_getSuggestions.response.json | 169 ++++++++++ .../messages/types/suggestions-data.json | 0 special-pages/pages/new-tab/types/new-tab.ts | 60 ++++ 14 files changed, 852 insertions(+), 26 deletions(-) create mode 100644 special-pages/pages/new-tab/app/search/mocks/getSuggestions.json create mode 100644 special-pages/pages/new-tab/app/search/mocks/search.mock-transport.js create mode 100644 special-pages/pages/new-tab/app/search/mocks/search.mocks.js create mode 100644 special-pages/pages/new-tab/app/search/search.md create mode 100644 special-pages/pages/new-tab/messages/search_getSuggestions.request.json create mode 100644 special-pages/pages/new-tab/messages/search_getSuggestions.response.json create mode 100644 special-pages/pages/new-tab/messages/types/suggestions-data.json diff --git a/special-pages/pages/new-tab/app/mock-transport.js b/special-pages/pages/new-tab/app/mock-transport.js index cab3125936..4cc8909cce 100644 --- a/special-pages/pages/new-tab/app/mock-transport.js +++ b/special-pages/pages/new-tab/app/mock-transport.js @@ -9,6 +9,7 @@ import { customizerData, customizerMockTransport } from './customizer/mocks.js'; import { freemiumPIRDataExamples } from './freemium-pir-banner/mocks/freemiumPIRBanner.data.js'; import { activityMockTransport } from './activity/mocks/activity.mock-transport.js'; import { protectionsMockTransport } from './protections/mocks/protections.mock-transport.js'; +import { searchMockTransport } from './search/mocks/search.mock-transport.js'; /** * @typedef {import('../types/new-tab').Favorite} Favorite @@ -106,6 +107,7 @@ export function mockTransport() { customizer: customizerMockTransport(), activity: activityMockTransport(), protections: protectionsMockTransport(), + search: searchMockTransport(), }; return new TestTransportConfig({ diff --git a/special-pages/pages/new-tab/app/new-tab.md b/special-pages/pages/new-tab/app/new-tab.md index 3a62eb8cb8..128cc8aa93 100644 --- a/special-pages/pages/new-tab/app/new-tab.md +++ b/special-pages/pages/new-tab/app/new-tab.md @@ -10,6 +10,7 @@ children: - ./next-steps/next-steps.md - ./customizer/customizer.md - ./protections/protections.md + - ./search/search.md --- ## Requests diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index da69ece551..7f78c99577 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -1,21 +1,28 @@ import { h } from 'preact'; -import { useSignal } from '@preact/signals'; +import { useComputed, useSignal } from '@preact/signals'; import classNames from 'classnames'; import styles from './Search.module.css'; -import { SearchInput } from './SearchInput.js'; +import { SearchInput, toDisplay } from './SearchInput.js'; import { viewTransition } from '../../utils.js'; export function Search() { const mode = useSignal(/** @type {"search" | "ai"} */ ('search')); + const suggestions = useSignal(/** @type {import('../../../types/new-tab').Suggestions} */ ([])); + const showing = useComputed(() => { + if (suggestions.value.length > 0) return 'showing-suggestions'; + return 'none'; + }); function setMode(next) { viewTransition(() => { mode.value = next; + suggestions.value = []; + window.dispatchEvent(new Event('reset-mode')); }); } return ( -
+
Search Search @@ -35,7 +42,26 @@ export function Search() {
- + + +
+ ); +} + +/** + * @param {object} props + * @param {import("@preact/signals").Signal} props.suggestions + */ +function SuggestionList({ suggestions }) { + return ( +
+ {suggestions.value.map((x) => { + return ( +
+ {x.kind}: {toDisplay(x)} +
+ ); + })}
); } diff --git a/special-pages/pages/new-tab/app/search/components/Search.module.css b/special-pages/pages/new-tab/app/search/components/Search.module.css index 6b5c8f8029..580fdf0e69 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.module.css +++ b/special-pages/pages/new-tab/app/search/components/Search.module.css @@ -1,5 +1,6 @@ .root { margin-bottom: var(--ntp-gap); + position: relative; } .icons { display: flex; @@ -54,4 +55,16 @@ svg[data-name="duckai"] { color: var(--duckai-purple); } +} +.list { + position: absolute; + top: 100%; + width: 100%; + background: white; + z-index: 1; + border-bottom-right-radius: 16px; + border-bottom-left-radius: 16px; +} +.list:not(:empty) { + padding: 8px; } \ No newline at end of file diff --git a/special-pages/pages/new-tab/app/search/components/SearchInput.js b/special-pages/pages/new-tab/app/search/components/SearchInput.js index c9556dbf85..e6a5f4e832 100644 --- a/special-pages/pages/new-tab/app/search/components/SearchInput.js +++ b/special-pages/pages/new-tab/app/search/components/SearchInput.js @@ -2,38 +2,25 @@ import cn from 'classnames'; import { h } from 'preact'; import styles from './SearchInput.module.css'; import { DuckAiIcon, SearchIcon } from './Search.js'; -import { useEffect, useRef } from 'preact/hooks'; +import { useCallback, useEffect, useRef } from 'preact/hooks'; +import { useMessaging } from '../../types.js'; +import { useSignal } from '@preact/signals'; /** + * + * @import { Signal } from '@preact/signals'; + * * Renders a search input component with optional mode-based controls and tab switching functionality. * * @param {Object} props - The props for the SearchInput component. * @param {import('@preact/signals').Signal<'ai' | 'search'>} props.mode - The mode in which the component operates. Determines the presence of additional controls. + * @param {Signal} props.suggestions - The mode in which the component operates. Determines the presence of additional controls. */ -export function SearchInput({ mode }) { - const inputRef = useRef(/** @type {HTMLInputElement|null} */ (null)); - - useEffect(() => { - return mode.subscribe(() => { - inputRef.current?.focus(); - }); - }, [mode]); - +export function SearchInput({ mode, suggestions }) { return (
- + {mode.value === 'search' && (
- - + + {showing.value === 'showing-suggestions' && }
); } @@ -51,15 +74,61 @@ export function Search() { /** * @param {object} props * @param {import("@preact/signals").Signal} props.suggestions + * @param {Signal} props.selected */ -function SuggestionList({ suggestions }) { +function SuggestionList({ suggestions, selected }) { + const ref = useRef(/** @type {HTMLDivElement|null} */ (null)); + const list = useComputed(() => { + const index = selected.value; + return suggestions.value.map((x, i) => { + return { item: x, selected: i === index }; + }); + }); + useEffect(() => { + const listener = () => { + if (!ref.current?.contains(document.activeElement)) { + window.dispatchEvent(new Event('clear-suggestions')); + } + }; + window.addEventListener('focusin', listener); + return () => { + window.removeEventListener('focusin', listener); + }; + }, []); + useEffect(() => { + const listener = (e) => { + if (e.key === 'ArrowDown') { + if (selected.value === null) { + selected.value = 0; + } else { + const next = Math.min(selected.value + 1, list.value.length - 1); + selected.value = next; + } + } + if (e.key === 'ArrowUp') { + if (selected.value === null) return; + if (selected.value === 0) { + selected.value = null; + window.dispatchEvent(new Event('focus-input')); + } else { + const next = Math.max(selected.value - 1, 0); + selected.value = next; + } + } + }; + window.addEventListener('keydown', listener); + return () => { + window.removeEventListener('keydown', listener); + }; + }, [selected]); + return ( -
- {suggestions.value.map((x) => { +
+ {list.value.map((x) => { return ( -
- {x.kind}: {toDisplay(x)} -
+ + {x.item.kind}: {toDisplay(x.item)} + ); })}
diff --git a/special-pages/pages/new-tab/app/search/components/Search.module.css b/special-pages/pages/new-tab/app/search/components/Search.module.css index 580fdf0e69..df5af7e32c 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.module.css +++ b/special-pages/pages/new-tab/app/search/components/Search.module.css @@ -67,4 +67,11 @@ } .list:not(:empty) { padding: 8px; +} +.item { + display: block; + text-decoration: none; + &[data-selected="true"] { + background: var(--color-black-at-6); + } } \ No newline at end of file diff --git a/special-pages/pages/new-tab/app/search/components/SearchInput.js b/special-pages/pages/new-tab/app/search/components/SearchInput.js index e6a5f4e832..fcf9291cb0 100644 --- a/special-pages/pages/new-tab/app/search/components/SearchInput.js +++ b/special-pages/pages/new-tab/app/search/components/SearchInput.js @@ -4,7 +4,7 @@ import styles from './SearchInput.module.css'; import { DuckAiIcon, SearchIcon } from './Search.js'; import { useCallback, useEffect, useRef } from 'preact/hooks'; import { useMessaging } from '../../types.js'; -import { useSignal } from '@preact/signals'; +import { useSignal, useSignalEffect } from '@preact/signals'; /** * @@ -14,13 +14,14 @@ import { useSignal } from '@preact/signals'; * * @param {Object} props - The props for the SearchInput component. * @param {import('@preact/signals').Signal<'ai' | 'search'>} props.mode - The mode in which the component operates. Determines the presence of additional controls. - * @param {Signal} props.suggestions - The mode in which the component operates. Determines the presence of additional controls. + * @param {import('@preact/signals').Signal} props.suggestions - The mode in which the component operates. Determines the presence of additional controls. + * @param {import('@preact/signals').Signal} props.selected */ -export function SearchInput({ mode, suggestions }) { +export function SearchInput({ mode, suggestions, selected }) { return (
- + {mode.value === 'search' && (
- - {showing.value === 'showing-suggestions' && } +
+ + + {showing.value === 'showing-suggestions' && } +
); } +function SelectedInput({ selected }) { + if (selected.value === null) return null; + return ; +} + /** * @param {object} props * @param {import("@preact/signals").Signal} props.suggestions diff --git a/special-pages/pages/new-tab/app/search/components/SearchInput.js b/special-pages/pages/new-tab/app/search/components/SearchInput.js index fcf9291cb0..8c030216fd 100644 --- a/special-pages/pages/new-tab/app/search/components/SearchInput.js +++ b/special-pages/pages/new-tab/app/search/components/SearchInput.js @@ -6,6 +6,8 @@ import { useCallback, useEffect, useRef } from 'preact/hooks'; import { useMessaging } from '../../types.js'; import { useSignal, useSignalEffect } from '@preact/signals'; +const url = new URL(window.location.href); + /** * * @import { Signal } from '@preact/signals'; @@ -84,6 +86,7 @@ function InputFieldWithSuggestions({ mode, suggestions, selected }) { data-testid="searchInput" onInput={onInput} onKeyDown={onKeydown} + name="term" /> ); } @@ -105,11 +108,13 @@ function useSuggestions(suggestions, mode, selected) { const ntp = useMessaging(); /** + * @param {string} reason * @param {string} value * @param {number} start * @param {number} end */ - function setValueAndRange(value, start, end) { + function setValueAndRange(reason, value, start, end) { + if (url.searchParams.getAll('debug').includes('reason')) console.log('reason:', reason); const input = ref.current; if (!input || typeof input.selectionStart !== 'number') return console.warn('no'); input.value = value; @@ -120,7 +125,7 @@ function useSuggestions(suggestions, mode, selected) { const listener = () => { const input = ref.current; if (!input || typeof input.selectionStart !== 'number') return console.warn('no'); - setValueAndRange(last.current, input.value.length, input.value.length); + setValueAndRange('reset-mode', last.current, input.value.length, input.value.length); }; window.addEventListener('reset-mode', listener); return () => { @@ -139,7 +144,7 @@ function useSuggestions(suggestions, mode, selected) { case 'autocomplete': { const { value, range } = result; const { start, end } = range; - setValueAndRange(value, start, end); + setValueAndRange('suggestions changed', value, start, end); } } }); @@ -156,7 +161,7 @@ function useSuggestions(suggestions, mode, selected) { case 'autocomplete': { const { value, range } = result; const { start, end } = range; - setValueAndRange(value, start, end); + setValueAndRange('selected.value useSignalEffect', value, start, end); } } } @@ -168,14 +173,18 @@ function useSuggestions(suggestions, mode, selected) { return; } if (mode.peek() === 'ai') return; - console.log(`✉️ search_getSuggestions('${e.target.value}')`); + if (url.searchParams.getAll('debug').includes('api')) { + console.log(`✉️ search_getSuggestions('${e.target.value}')`); + } ntp.messaging .request('search_getSuggestions', { term: e.target.value }) // eslint-disable-next-line promise/prefer-await-to-then .then((/** @type {import('../../../types/new-tab').SuggestionsData} */ data) => { - console.group(`✅ search_getSuggestions`); - console.log(data); - console.groupEnd(); + if (url.searchParams.getAll('debug').includes('api')) { + console.group(`✅ search_getSuggestions`); + console.log(data); + console.groupEnd(); + } const flat = [ ...data.suggestions.topHits, ...data.suggestions.duckduckgoSuggestions, @@ -199,7 +208,7 @@ function useSuggestions(suggestions, mode, selected) { switch (result.kind) { case 'autocomplete': { const { value, range } = result; - setValueAndRange(value, range.start, range.end); + setValueAndRange('other keydown', value, range.start, range.end); } } }, diff --git a/special-pages/pages/new-tab/app/search/search.md b/special-pages/pages/new-tab/app/search/search.md index a3626a23b7..638592e0ed 100644 --- a/special-pages/pages/new-tab/app/search/search.md +++ b/special-pages/pages/new-tab/app/search/search.md @@ -25,7 +25,16 @@ title: Search ## Requests: ### `search_getSuggestions` - {@link "NewTab Messages".SearchGetSuggestionsRequest} -- Used to fetch the initial data (during the first render) - returns {@link "NewTab Messages".SuggestionsData} -{@includeCode ./mocks/getSuggestions.json} \ No newline at end of file +{@includeCode ./mocks/getSuggestions.json} + +## Notifications: +### `search_openSuggestion` +- {@link "NewTab Messages".SearchOpenSuggestionNotification} +- Sends {@link "NewTab Messages".SearchOpenSuggestion} + +## Notifications: +### `search_submit` +- {@link "NewTab Messages".SearchSubmitNotification} +- Sends {@link "NewTab Messages".SearchSubmitParams} \ No newline at end of file diff --git a/special-pages/pages/new-tab/messages/search_openSuggestion.notify.json b/special-pages/pages/new-tab/messages/search_openSuggestion.notify.json new file mode 100644 index 0000000000..ad83fd1352 --- /dev/null +++ b/special-pages/pages/new-tab/messages/search_openSuggestion.notify.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Search Open Suggestion", + "type": "object", + "required": [ + "target", + "suggestion" + ], + "properties": { + "suggestion": { + "$ref": "./search_getSuggestions.response.json#/definitions/Suggestion" + }, + "target": { + "$ref": "./types/open-target.json" + } + } +} diff --git a/special-pages/pages/new-tab/messages/search_submit.notify.json b/special-pages/pages/new-tab/messages/search_submit.notify.json new file mode 100644 index 0000000000..0988b710e6 --- /dev/null +++ b/special-pages/pages/new-tab/messages/search_submit.notify.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Search Submit Params", + "type": "object", + "required": [ + "term", + "target" + ], + "properties": { + "term": { + "type": "string" + }, + "target": { + "$ref": "./types/open-target.json" + } + } +} diff --git a/special-pages/pages/new-tab/types/new-tab.ts b/special-pages/pages/new-tab/types/new-tab.ts index cb8ed70614..b0b0c43e2d 100644 --- a/special-pages/pages/new-tab/types/new-tab.ts +++ b/special-pages/pages/new-tab/types/new-tab.ts @@ -126,6 +126,8 @@ export interface NewTabMessages { | RmfDismissNotification | RmfPrimaryActionNotification | RmfSecondaryActionNotification + | SearchOpenSuggestionNotification + | SearchSubmitNotification | StatsShowLessNotification | StatsShowMoreNotification | TelemetryEventNotification @@ -516,6 +518,67 @@ export interface RmfSecondaryActionNotification { export interface RMFSecondaryAction { id: string; } +/** + * Generated from @see "../messages/search_openSuggestion.notify.json" + */ +export interface SearchOpenSuggestionNotification { + method: "search_openSuggestion"; + params: SearchOpenSuggestion; +} +export interface SearchOpenSuggestion { + suggestion: + | BookmarkSuggestion + | OpenTabSuggestion + | PhraseSuggestion + | WebsiteSuggestion + | HistoryEntrySuggestion + | InternalPageSuggestion; + target: OpenTarget; +} +export interface BookmarkSuggestion { + kind: "bookmark"; + title: string; + url: string; + isFavorite: boolean; + score: number; +} +export interface OpenTabSuggestion { + kind: "openTab"; + title: string; + tabId: string; + score: number; +} +export interface PhraseSuggestion { + kind: "phrase"; + phrase: string; +} +export interface WebsiteSuggestion { + kind: "website"; + url: string; +} +export interface HistoryEntrySuggestion { + kind: "historyEntry"; + title: string; + url: string; + score: number; +} +export interface InternalPageSuggestion { + kind: "internalPage"; + title: string; + url: string; + score: number; +} +/** + * Generated from @see "../messages/search_submit.notify.json" + */ +export interface SearchSubmitNotification { + method: "search_submit"; + params: SearchSubmitParams; +} +export interface SearchSubmitParams { + term: string; + target: OpenTarget; +} /** * Generated from @see "../messages/stats_showLess.notify.json" */ @@ -865,39 +928,6 @@ export interface SuggestionsData { localSuggestions: Suggestions; }; } -export interface BookmarkSuggestion { - kind: "bookmark"; - title: string; - url: string; - isFavorite: boolean; - score: number; -} -export interface OpenTabSuggestion { - kind: "openTab"; - title: string; - tabId: string; - score: number; -} -export interface PhraseSuggestion { - kind: "phrase"; - phrase: string; -} -export interface WebsiteSuggestion { - kind: "website"; - url: string; -} -export interface HistoryEntrySuggestion { - kind: "historyEntry"; - title: string; - url: string; - score: number; -} -export interface InternalPageSuggestion { - kind: "internalPage"; - title: string; - url: string; - score: number; -} /** * Generated from @see "../messages/stats_getData.request.json" */ From 4afb175cd32fcd534908e16219d49d08bbd31147 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Thu, 12 Jun 2025 16:54:26 +0100 Subject: [PATCH 06/16] styles --- .../app/favorites/components/Tile.module.css | 3 +- .../components/Protections.module.css | 1 + .../new-tab/app/search/components/Search.js | 14 +++++---- .../app/search/components/Search.module.css | 22 +++++++++++++- .../search/components/SearchInput.module.css | 30 ------------------- .../pages/new-tab/app/styles/ntp-theme.css | 1 + 6 files changed, 33 insertions(+), 38 deletions(-) diff --git a/special-pages/pages/new-tab/app/favorites/components/Tile.module.css b/special-pages/pages/new-tab/app/favorites/components/Tile.module.css index 79c5df3bc5..f589dea1a8 100644 --- a/special-pages/pages/new-tab/app/favorites/components/Tile.module.css +++ b/special-pages/pages/new-tab/app/favorites/components/Tile.module.css @@ -42,7 +42,8 @@ .draggable { backdrop-filter: blur(48px); background: var(--ntp-surface-background-color); - box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.12), 0px 0px 3px 0px rgba(0, 0, 0, 0.16); + /*box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.12), 0px 0px 3px 0px rgba(0, 0, 0, 0.16);*/ + box-shadow: var(--ntp-surface-shadow); transition: transform .2s; &:hover { diff --git a/special-pages/pages/new-tab/app/protections/components/Protections.module.css b/special-pages/pages/new-tab/app/protections/components/Protections.module.css index 6e6ff274d2..8398437ff7 100644 --- a/special-pages/pages/new-tab/app/protections/components/Protections.module.css +++ b/special-pages/pages/new-tab/app/protections/components/Protections.module.css @@ -2,6 +2,7 @@ background: var(--ntp-surface-background-color); backdrop-filter: blur(48px); border: 1px solid var(--ntp-surface-border-color); + box-shadow: var(--ntp-surface-shadow); padding: var(--sp-6); border-radius: var(--border-radius-lg); display: grid; diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index 41dbce37e3..afcb85e915 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -69,7 +69,7 @@ export function Search() { } return ( -
+
Search Search @@ -89,11 +89,13 @@ export function Search() {
-
- - - {showing.value === 'showing-suggestions' && } - +
+
+ + + {showing.value === 'showing-suggestions' && } + +
); } diff --git a/special-pages/pages/new-tab/app/search/components/Search.module.css b/special-pages/pages/new-tab/app/search/components/Search.module.css index df5af7e32c..4b0a2b2532 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.module.css +++ b/special-pages/pages/new-tab/app/search/components/Search.module.css @@ -19,6 +19,20 @@ justify-content: center; margin-top: 24px; } +.formWrap { + height: 48px; + margin-top: 16px; + position: relative; + z-index: 1; + [data-mode="ai"] & { + height: 96px; + } +} +.form { + border-radius: 16px; + background-color: #fff; + box-shadow: var(--ntp-surface-shadow); +} .pillSwitcher { display: flex; background: var(--color-black-at-6); @@ -57,13 +71,16 @@ } } .list { - position: absolute; top: 100%; width: 100%; background: white; z-index: 1; border-bottom-right-radius: 16px; border-bottom-left-radius: 16px; + border-top: 1px solid var(--color-black-at-6); + display: flex; + flex-direction: column; + gap: 4px; } .list:not(:empty) { padding: 8px; @@ -71,6 +88,9 @@ .item { display: block; text-decoration: none; + font-size: var(--title-3-em-font-size); + padding: 4px 8px; + color: var(--ntp-text-normal); &[data-selected="true"] { background: var(--color-black-at-6); } diff --git a/special-pages/pages/new-tab/app/search/components/SearchInput.module.css b/special-pages/pages/new-tab/app/search/components/SearchInput.module.css index 4ad5ea08a0..c8929fc1f7 100644 --- a/special-pages/pages/new-tab/app/search/components/SearchInput.module.css +++ b/special-pages/pages/new-tab/app/search/components/SearchInput.module.css @@ -2,42 +2,12 @@ position: relative; width: 100%; margin: 0 auto; - border-radius: 16px; - background-color: #fff; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); - margin-top: 16px; padding: 8px 8px; display: flex; flex-direction: column; gap: 8px; - [data-state="showing-suggestions"] & { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - } } -/*@keyframes grow-y {*/ -/* from {*/ -/* transform: scaleY(0);*/ -/* }*/ -/* to {*/ -/* transform: scaleY(1);*/ -/* }*/ -/*}*/ - -/*@keyframes shrink-y {*/ -/* from {*/ -/* transform: scaleY(1);*/ -/* }*/ -/* to {*/ -/* transform: scaleY(0);*/ -/* }*/ -/*}*/ - -/*::view-transition-old(search-input-transition) {*/ -/* animation: 0.25s linear both shrink-y;*/ -/*}*/ - ::view-transition-new(search-input-transition) { animation: 0.25s grow-y; } 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 b128cee42b..3e025f0136 100644 --- a/special-pages/pages/new-tab/app/styles/ntp-theme.css +++ b/special-pages/pages/new-tab/app/styles/ntp-theme.css @@ -3,6 +3,7 @@ --ntp-drawer-width: calc(224 * var(--px-in-rem)); --ntp-drawer-scroll-width: 12px; --ntp-combined-width: calc(var(--ntp-drawer-width) + var(--ntp-drawer-scroll-width)); + --ntp-surface-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.13); /* Mac/System/Callout */ --callout-font-size: 12px; From a939be87215e85e01a77fa989e5f178873bc896b Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Thu, 12 Jun 2025 17:16:57 +0100 Subject: [PATCH 07/16] style stuff --- .../app/favorites/components/Tile.module.css | 1 - .../new-tab/app/search/components/Search.js | 36 ++++++++++++++++--- .../app/search/components/Search.module.css | 15 +++++++- .../app/search/components/SearchInput.js | 6 ++-- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/special-pages/pages/new-tab/app/favorites/components/Tile.module.css b/special-pages/pages/new-tab/app/favorites/components/Tile.module.css index f589dea1a8..a4609584b2 100644 --- a/special-pages/pages/new-tab/app/favorites/components/Tile.module.css +++ b/special-pages/pages/new-tab/app/favorites/components/Tile.module.css @@ -42,7 +42,6 @@ .draggable { backdrop-filter: blur(48px); background: var(--ntp-surface-background-color); - /*box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.12), 0px 0px 3px 0px rgba(0, 0, 0, 0.16);*/ box-shadow: var(--ntp-surface-shadow); transition: transform .2s; diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index afcb85e915..60aa7687a6 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -23,7 +23,7 @@ export function Search() { viewTransition(() => { mode.value = next; suggestions.value = []; - window.dispatchEvent(new Event('reset-mode')); + window.dispatchEvent(new Event('reset-back-to-last-typed-value')); }); } @@ -37,6 +37,7 @@ export function Search() { window.removeEventListener('clear-suggestions', listener); }; }, []); + useEffect(() => { const listener = (e) => { if (e.key === 'Escape') { @@ -129,6 +130,17 @@ function SuggestionList({ suggestions, selected }) { window.removeEventListener('focusin', listener); }; }, []); + useEffect(() => { + const listener = (e) => { + if (!ref.current?.contains(e.target)) { + window.dispatchEvent(new Event('clear-suggestions')); + } + }; + document.addEventListener('click', listener); + return () => { + document.removeEventListener('click', listener); + }; + }, []); useEffect(() => { const listener = (e) => { if (e.key === 'ArrowDown') { @@ -156,13 +168,27 @@ function SuggestionList({ suggestions, selected }) { }; }, [selected]); + function mouseEnter(e) { + const button = e.target.closest('button[value]'); + if (button && button instanceof HTMLButtonElement) { + selected.value = Number(e.target.value); + } + } + return ( -
- {list.value.map((x) => { +
{ + window.dispatchEvent(new Event('reset-back-to-last-typed-value')); + }} + > + {list.value.map((x, index) => { return ( - + ); })}
diff --git a/special-pages/pages/new-tab/app/search/components/Search.module.css b/special-pages/pages/new-tab/app/search/components/Search.module.css index 4b0a2b2532..d1f02983be 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.module.css +++ b/special-pages/pages/new-tab/app/search/components/Search.module.css @@ -91,7 +91,20 @@ font-size: var(--title-3-em-font-size); padding: 4px 8px; color: var(--ntp-text-normal); + background-color: transparent; + border: 0; + text-align: left; + cursor: pointer; + + &:hover { + background: var(--ddg-color-primary); + color: white; + border-radius: 4px; + } + &[data-selected="true"] { - background: var(--color-black-at-6); + background: var(--ddg-color-primary); + color: white; + border-radius: 4px; } } \ No newline at end of file diff --git a/special-pages/pages/new-tab/app/search/components/SearchInput.js b/special-pages/pages/new-tab/app/search/components/SearchInput.js index 8c030216fd..22717482c1 100644 --- a/special-pages/pages/new-tab/app/search/components/SearchInput.js +++ b/special-pages/pages/new-tab/app/search/components/SearchInput.js @@ -125,11 +125,11 @@ function useSuggestions(suggestions, mode, selected) { const listener = () => { const input = ref.current; if (!input || typeof input.selectionStart !== 'number') return console.warn('no'); - setValueAndRange('reset-mode', last.current, input.value.length, input.value.length); + setValueAndRange('reset-back-to-last-typed-value', last.current, input.value.length, input.value.length); }; - window.addEventListener('reset-mode', listener); + window.addEventListener('reset-back-to-last-typed-value', listener); return () => { - window.removeEventListener('reset-mode', listener); + window.removeEventListener('reset-back-to-last-typed-value', listener); }; }, []); From 0913fb2065d2798f2bf1e3e85d0490dc0ef5a8e3 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Thu, 12 Jun 2025 17:25:20 +0100 Subject: [PATCH 08/16] better mouse events --- .../new-tab/app/search/components/Search.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index 60aa7687a6..5cb49f5a69 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -168,12 +168,18 @@ function SuggestionList({ suggestions, selected }) { }; }, [selected]); - function mouseEnter(e) { - const button = e.target.closest('button[value]'); - if (button && button instanceof HTMLButtonElement) { - selected.value = Number(e.target.value); + useEffect(() => { + function mouseEnter(e) { + const button = e.target.closest('button[value]'); + if (button && button instanceof HTMLButtonElement) { + selected.value = Number(e.target.value); + } } - } + ref.current?.addEventListener('mouseenter', mouseEnter, true); + return () => { + ref.current?.removeEventListener('mouseenter', mouseEnter, true); + }; + }, [selected]); return (
{list.value.map((x, index) => { return ( - ); From c4b2138b4ded82f79b3608e0a67ff8c4bdb9aa4f Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Thu, 12 Jun 2025 17:40:35 +0100 Subject: [PATCH 09/16] fixing dynamix lookup --- .../pages/new-tab/app/search/components/Search.js | 4 ++-- .../pages/new-tab/app/search/components/Search.module.css | 8 +++++++- .../pages/new-tab/app/search/components/SearchInput.js | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index 5cb49f5a69..9f73bf4fec 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -172,7 +172,7 @@ function SuggestionList({ suggestions, selected }) { function mouseEnter(e) { const button = e.target.closest('button[value]'); if (button && button instanceof HTMLButtonElement) { - selected.value = Number(e.target.value); + selected.value = Number(button.value); } } ref.current?.addEventListener('mouseenter', mouseEnter, true); @@ -193,7 +193,7 @@ function SuggestionList({ suggestions, selected }) { {list.value.map((x, index) => { return ( ); })} diff --git a/special-pages/pages/new-tab/app/search/components/Search.module.css b/special-pages/pages/new-tab/app/search/components/Search.module.css index d1f02983be..06bc192edb 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.module.css +++ b/special-pages/pages/new-tab/app/search/components/Search.module.css @@ -86,7 +86,8 @@ padding: 8px; } .item { - display: block; + display: flex; + align-items: center; text-decoration: none; font-size: var(--title-3-em-font-size); padding: 4px 8px; @@ -95,6 +96,11 @@ border: 0; text-align: left; cursor: pointer; + gap: 8px; + + svg path { + fill-opacity: 1!important; + } &:hover { background: var(--ddg-color-primary); diff --git a/special-pages/pages/new-tab/app/search/components/SearchInput.js b/special-pages/pages/new-tab/app/search/components/SearchInput.js index 22717482c1..9dbd7d7b2d 100644 --- a/special-pages/pages/new-tab/app/search/components/SearchInput.js +++ b/special-pages/pages/new-tab/app/search/components/SearchInput.js @@ -156,6 +156,7 @@ function useSuggestions(suggestions, mode, selected) { const input = ref.current; if (!input || typeof input.selectionStart !== 'number') return console.warn('no'); const suggestion = suggestions.peek()[sub]; + if (!suggestion) console.warn('missing suggestion', sub); const result = pick(last.current, input.value, last.current.length, suggestion); switch (result.kind) { case 'autocomplete': { From 81072a2f583efefb0feeaf8a7038a22fb8a7332d Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Thu, 12 Jun 2025 17:53:19 +0100 Subject: [PATCH 10/16] icons --- .../new-tab/app/search/components/Search.js | 85 ++++++++++++++++++- .../app/search/components/Search.module.css | 6 ++ .../new-tab/app/search/mocks/search.mocks.js | 10 +++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index 9f73bf4fec..e0beeca36f 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -191,9 +191,10 @@ function SuggestionList({ suggestions, selected }) { }} > {list.value.map((x, index) => { + const icon = iconFor(x.item); return ( ); })} @@ -201,6 +202,30 @@ function SuggestionList({ suggestions, selected }) { ); } +/** + * + * @param {import('../../../types/new-tab').Suggestions[number]} suggestion + */ +function iconFor(suggestion) { + switch (suggestion.kind) { + case 'phrase': + return ; + case 'website': + return ; + case 'historyEntry': + return ; + case 'bookmark': + if (suggestion.isFavorite) { + return ; + } + return ; + case 'openTab': + case 'internalPage': + console.warn('icon not implemented for ', suggestion.kind); + return null; + } +} + export function SearchIcon() { return ( @@ -213,6 +238,64 @@ export function SearchIcon() { ); } +export function BookmarkIcon() { + return ( + + + + ); +} + +export function FavoriteIcon() { + return ( + + + + + ); +} + +export function GlobeIcon() { + return ( + + + + ); +} + +export function HistoryIcon() { + return ( + + + + + ); +} + export function DuckAiIcon() { return ( diff --git a/special-pages/pages/new-tab/app/search/components/Search.module.css b/special-pages/pages/new-tab/app/search/components/Search.module.css index 06bc192edb..88841a1021 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.module.css +++ b/special-pages/pages/new-tab/app/search/components/Search.module.css @@ -98,6 +98,12 @@ cursor: pointer; gap: 8px; + svg { + width: 16px; + height: 16px; + display: block; + } + svg path { fill-opacity: 1!important; } diff --git a/special-pages/pages/new-tab/app/search/mocks/search.mocks.js b/special-pages/pages/new-tab/app/search/mocks/search.mocks.js index 19639696c5..22689a7f48 100644 --- a/special-pages/pages/new-tab/app/search/mocks/search.mocks.js +++ b/special-pages/pages/new-tab/app/search/mocks/search.mocks.js @@ -98,6 +98,16 @@ export function getMockSuggestions(searchTerm) { score: 95 + Math.floor(Math.random() * 5), })), duckduckgoSuggestions: [ + ...pizzaRelatedData.websites + .filter((phrase) => phrase.toLowerCase().includes(term)) + .slice(0, 2) + .map((website, index) => ({ + kind: /** @type {const} */ ('bookmark'), + title: website, + url: website, + isFavorite: index === 0, + score: 95 + Math.floor(Math.random() * 5), + })), ...pizzaRelatedData.phrases .filter((phrase) => phrase.toLowerCase().includes(term)) .slice(3, 8) From 25893d5b66bcd0299fde470cc6514d433e7b79bd Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 13 Jun 2025 10:52:19 +0100 Subject: [PATCH 11/16] handoff --- .../new-tab/app/search/components/Search.js | 7 +++++++ .../app/search/components/Search.module.css | 6 +++++- .../search/components/SearchInput.module.css | 7 +++++++ .../app/search/mocks/search.mock-transport.js | 4 +++- .../pages/new-tab/app/search/search.md | 13 ++++++++++++- .../pages/new-tab/app/styles/ntp-theme.css | 2 ++ .../messages/search_submitChat.notify.json | 17 +++++++++++++++++ special-pages/pages/new-tab/types/new-tab.ts | 12 ++++++++++++ 8 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 special-pages/pages/new-tab/messages/search_submitChat.notify.json diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index e0beeca36f..166d5147f2 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -55,7 +55,13 @@ export function Search() { const data = new FormData(e.target); const term = data.get('term'); const selected = data.get('selected'); + const mode = data.get('mode'); const target = eventToTarget(e, platformName); + + if (mode === 'ai' && term) { + return ntp.messaging.notify('search_submitChat', { chat: String(term), target }); + } + if (term && selected) { const suggestion = suggestions.value[Number(selected)]; if (suggestion) { @@ -94,6 +100,7 @@ export function Search() {
+ {showing.value === 'showing-suggestions' && }
diff --git a/special-pages/pages/new-tab/app/search/components/Search.module.css b/special-pages/pages/new-tab/app/search/components/Search.module.css index 88841a1021..9f4b88f6ea 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.module.css +++ b/special-pages/pages/new-tab/app/search/components/Search.module.css @@ -52,16 +52,20 @@ align-items: center; gap: 6px; font-weight: 600; + transition: background .2s; svg { width: 16px; height: 16px; } + + &:hover { + background: var(--color-black-at-9); + } } .pillOption.active { background: var(--color-white); - /* Shadows/On Light/Elevation 10 */ box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.08), 0px 2px 4px 0px rgba(0, 0, 0, 0.10); svg[data-name="search"] { color: blue; diff --git a/special-pages/pages/new-tab/app/search/components/SearchInput.module.css b/special-pages/pages/new-tab/app/search/components/SearchInput.module.css index c8929fc1f7..38ddaddf1d 100644 --- a/special-pages/pages/new-tab/app/search/components/SearchInput.module.css +++ b/special-pages/pages/new-tab/app/search/components/SearchInput.module.css @@ -106,8 +106,15 @@ color: #fff; position: relative; top: 4px; + &:hover { + background-color: var(--duckai-purple-hover); + } + &:active { + background-color: var(--duckai-purple-active); + } } + .buttonSecondary { background-color: rgba(0, 0, 0, 0.06); color: var(--color-black-at-84); diff --git a/special-pages/pages/new-tab/app/search/mocks/search.mock-transport.js b/special-pages/pages/new-tab/app/search/mocks/search.mock-transport.js index a3d78b1089..cba59a723f 100644 --- a/special-pages/pages/new-tab/app/search/mocks/search.mock-transport.js +++ b/special-pages/pages/new-tab/app/search/mocks/search.mock-transport.js @@ -8,7 +8,9 @@ export function searchMockTransport() { const msg = /** @type {any} */ (_msg); switch (msg.method) { default: { - console.warn('unhandled notification', msg); + console.group('unhandled notification', msg); + console.warn(JSON.stringify(msg)); + console.groupEnd(); } } }, diff --git a/special-pages/pages/new-tab/app/search/search.md b/special-pages/pages/new-tab/app/search/search.md index 638592e0ed..fad653c670 100644 --- a/special-pages/pages/new-tab/app/search/search.md +++ b/special-pages/pages/new-tab/app/search/search.md @@ -37,4 +37,15 @@ title: Search ## Notifications: ### `search_submit` - {@link "NewTab Messages".SearchSubmitNotification} -- Sends {@link "NewTab Messages".SearchSubmitParams} \ No newline at end of file +- Sends {@link "NewTab Messages".SearchSubmitParams} + +## Notifications: +### `search_submitChat` +- {@link "NewTab Messages".SearchSubmitChatNotification} +- Sends {@link "NewTab Messages".SearchSubmitChatParams} +```json +{ + "chat": "Give me 5 pub-quiz style facts about Austria", + "target": "same-tab" +} +``` \ No newline at end of file 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 3e025f0136..5796f12b60 100644 --- a/special-pages/pages/new-tab/app/styles/ntp-theme.css +++ b/special-pages/pages/new-tab/app/styles/ntp-theme.css @@ -38,6 +38,8 @@ --focus-ring-thin: 0px 0px 0px 1px var(--ntp-focus-outline-color), 0px 0px 0px 1px var(--color-white); --duckai-purple: rgba(107, 78, 186, 1); + --duckai-purple-hover: rgb(128, 101, 208); + --duckai-purple-active: rgb(85, 64, 160); } body { diff --git a/special-pages/pages/new-tab/messages/search_submitChat.notify.json b/special-pages/pages/new-tab/messages/search_submitChat.notify.json new file mode 100644 index 0000000000..6f3126d2fc --- /dev/null +++ b/special-pages/pages/new-tab/messages/search_submitChat.notify.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Search Submit Chat Params", + "type": "object", + "required": [ + "chat", + "target" + ], + "properties": { + "chat": { + "type": "string" + }, + "target": { + "$ref": "./types/open-target.json" + } + } +} diff --git a/special-pages/pages/new-tab/types/new-tab.ts b/special-pages/pages/new-tab/types/new-tab.ts index b0b0c43e2d..3917aa95b8 100644 --- a/special-pages/pages/new-tab/types/new-tab.ts +++ b/special-pages/pages/new-tab/types/new-tab.ts @@ -128,6 +128,7 @@ export interface NewTabMessages { | RmfSecondaryActionNotification | SearchOpenSuggestionNotification | SearchSubmitNotification + | SearchSubmitChatNotification | StatsShowLessNotification | StatsShowMoreNotification | TelemetryEventNotification @@ -579,6 +580,17 @@ export interface SearchSubmitParams { term: string; target: OpenTarget; } +/** + * Generated from @see "../messages/search_submitChat.notify.json" + */ +export interface SearchSubmitChatNotification { + method: "search_submitChat"; + params: SearchSubmitChatParams; +} +export interface SearchSubmitChatParams { + chat: string; + target: OpenTarget; +} /** * Generated from @see "../messages/stats_showLess.notify.json" */ From a343c728f7e76df7bc0272aa96c5f73c2978b09f Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 13 Jun 2025 15:12:25 +0100 Subject: [PATCH 12/16] reset --- .../new-tab/app/search/components/Search.js | 5 +++++ .../app/search/components/SearchInput.js | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index 166d5147f2..bbf9f4e7c6 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -52,6 +52,9 @@ export function Search() { function onSubmit(e) { e.preventDefault(); + + if (!(e.target instanceof HTMLFormElement)) return; + const data = new FormData(e.target); const term = data.get('term'); const selected = data.get('selected'); @@ -73,6 +76,8 @@ export function Search() { } else if (term) { ntp.messaging.notify('search_submit', { term: String(term), target }); } + + window.dispatchEvent(new Event('clear-all')); } return ( diff --git a/special-pages/pages/new-tab/app/search/components/SearchInput.js b/special-pages/pages/new-tab/app/search/components/SearchInput.js index 9dbd7d7b2d..12212a8e89 100644 --- a/special-pages/pages/new-tab/app/search/components/SearchInput.js +++ b/special-pages/pages/new-tab/app/search/components/SearchInput.js @@ -121,6 +121,17 @@ function useSuggestions(suggestions, mode, selected) { input.setSelectionRange(start, end); } + /** + * @param {string} reason + */ + function reset(reason) { + if (url.searchParams.getAll('debug').includes('reason')) console.log('[reset] reason:', reason); + const input = ref.current; + if (!input) return console.warn('missing input'); + input.value = ''; + last.current = ''; + } + useEffect(() => { const listener = () => { const input = ref.current; @@ -133,6 +144,16 @@ function useSuggestions(suggestions, mode, selected) { }; }, []); + useEffect(() => { + const listener = () => { + reset('window event "clear-all"'); + }; + window.addEventListener('clear-all', listener); + return () => { + window.removeEventListener('clear-all', listener); + }; + }, []); + useEffect(() => { return suggestions.subscribe((values) => { // const flat = values From f7c2211045ccd9e5627062f9b9f3aaacea8589d0 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 13 Jun 2025 15:20:15 +0100 Subject: [PATCH 13/16] also clear suggestions --- special-pages/pages/new-tab/app/search/components/Search.js | 1 + 1 file changed, 1 insertion(+) diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index bbf9f4e7c6..7ed8c9f356 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -78,6 +78,7 @@ export function Search() { } window.dispatchEvent(new Event('clear-all')); + window.dispatchEvent(new Event('clear-suggestions')); } return ( From 5bf201b5dd4b0959b9be668aa47c867075623dbe Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 13 Jun 2025 16:01:26 +0100 Subject: [PATCH 14/16] input --- .../new-tab/app/search/components/Search.js | 49 ++++++++++++++----- .../app/search/components/SearchInput.js | 15 +++++- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index 7ed8c9f356..569ef4d331 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -9,6 +9,7 @@ import { useMessaging } from '../../types.js'; import { usePlatformName } from '../../settings.provider.js'; export function Search() { + const formRef = useRef(null); const mode = useSignal(/** @type {"search" | "ai"} */ ('search')); const suggestions = useSignal(/** @type {import('../../../types/new-tab').Suggestions} */ ([])); const selected = useSignal(/** @type {null|number} */ (null)); @@ -50,17 +51,31 @@ export function Search() { }; }, []); - function onSubmit(e) { - e.preventDefault(); - - if (!(e.target instanceof HTMLFormElement)) return; + useEffect(() => { + const listener = (/** @type {CustomEvent} */ evt) => { + if (!formRef.current) return console.warn('formRef.current is null'); + const { detail } = evt; + const data = new FormData(formRef.current); + const term = data.get('term')?.toString().trim() || ''; - const data = new FormData(e.target); - const term = data.get('term'); - const selected = data.get('selected'); - const mode = data.get('mode'); - const target = eventToTarget(e, platformName); + if (term) { + accept(term, 'ai', detail.target, null); + } + }; + window.addEventListener('accept-ai', listener); + return () => { + window.removeEventListener('accept-ai', listener); + }; + }, []); + /** + * @param {string} term + * @param {"search" | "ai"} mode + * @param {import('../../../types/new-tab').OpenTarget} target + * @param {string|null} selected + * @returns {*} + */ + function accept(term, mode, target, selected) { if (mode === 'ai' && term) { return ntp.messaging.notify('search_submitChat', { chat: String(term), target }); } @@ -68,7 +83,6 @@ export function Search() { if (term && selected) { const suggestion = suggestions.value[Number(selected)]; if (suggestion) { - console.log({ term, selected }); ntp.messaging.notify('search_openSuggestion', { suggestion, target }); } else { console.warn('not found'); @@ -81,6 +95,19 @@ export function Search() { window.dispatchEvent(new Event('clear-suggestions')); } + function onSubmit(e) { + e.preventDefault(); + + if (!(e.target instanceof HTMLFormElement)) return; + + const data = new FormData(e.target); + const term = data.get('term')?.toString().trim() || ''; + const selected = data.get('selected')?.toString() || null; + const mode = /** @type {"search"|"ai"} */ (data.get('mode')?.toString() || 'search'); + const target = eventToTarget(e, platformName); + accept(term, mode, target, selected); + } + return (
@@ -103,7 +130,7 @@ export function Search() {
-
+ diff --git a/special-pages/pages/new-tab/app/search/components/SearchInput.js b/special-pages/pages/new-tab/app/search/components/SearchInput.js index 12212a8e89..e8e222112c 100644 --- a/special-pages/pages/new-tab/app/search/components/SearchInput.js +++ b/special-pages/pages/new-tab/app/search/components/SearchInput.js @@ -5,6 +5,8 @@ import { DuckAiIcon, SearchIcon } from './Search.js'; import { useCallback, useEffect, useRef } from 'preact/hooks'; import { useMessaging } from '../../types.js'; import { useSignal, useSignalEffect } from '@preact/signals'; +import { eventToTarget } from '../../utils.js'; +import { usePlatformName } from '../../settings.provider.js'; const url = new URL(window.location.href); @@ -20,17 +22,26 @@ const url = new URL(window.location.href); * @param {import('@preact/signals').Signal} props.selected */ export function SearchInput({ mode, suggestions, selected }) { + const platformName = usePlatformName(); return (
{mode.value === 'search' && (
-
-
From 4350d8956d54d04292c442caa48106691023fbbc Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 13 Jun 2025 16:19:18 +0100 Subject: [PATCH 15/16] fixes --- .../new-tab/app/search/components/Search.js | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index 569ef4d331..8bc3162569 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -31,6 +31,7 @@ export function Search() { useEffect(() => { const listener = () => { suggestions.value = []; + console.log('did wipe'); selected.value = null; }; window.addEventListener('clear-suggestions', listener); @@ -88,6 +89,7 @@ export function Search() { console.warn('not found'); } } else if (term) { + console.log({ term, selected, v: selected }); ntp.messaging.notify('search_submit', { term: String(term), target }); } @@ -102,10 +104,11 @@ export function Search() { const data = new FormData(e.target); const term = data.get('term')?.toString().trim() || ''; - const selected = data.get('selected')?.toString() || null; + const selectedForm = data.get('selected')?.toString() || null; + const mode = /** @type {"search"|"ai"} */ (data.get('mode')?.toString() || 'search'); const target = eventToTarget(e, platformName); - accept(term, mode, target, selected); + accept(term, mode, target, selectedForm); } return ( @@ -173,7 +176,9 @@ function SuggestionList({ suggestions, selected }) { useEffect(() => { const listener = (e) => { if (!ref.current?.contains(e.target)) { - window.dispatchEvent(new Event('clear-suggestions')); + setTimeout(() => { + window.dispatchEvent(new Event('clear-suggestions')); + }, 0); } }; document.addEventListener('click', listener); @@ -262,7 +267,7 @@ function iconFor(suggestion) { case 'openTab': case 'internalPage': console.warn('icon not implemented for ', suggestion.kind); - return null; + return ; } } @@ -321,6 +326,17 @@ export function GlobeIcon() { ); } +export function BrowserIcon() { + return ( + + + + ); +} + export function HistoryIcon() { return ( From ad705a0f2638cf0920eeae1793130a56b9e941ba Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 13 Jun 2025 16:23:21 +0100 Subject: [PATCH 16/16] clear all always --- .../new-tab/app/search/components/Search.js | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/special-pages/pages/new-tab/app/search/components/Search.js b/special-pages/pages/new-tab/app/search/components/Search.js index 8bc3162569..fd7ae82e86 100644 --- a/special-pages/pages/new-tab/app/search/components/Search.js +++ b/special-pages/pages/new-tab/app/search/components/Search.js @@ -77,20 +77,22 @@ export function Search() { * @returns {*} */ function accept(term, mode, target, selected) { - if (mode === 'ai' && term) { - return ntp.messaging.notify('search_submitChat', { chat: String(term), target }); - } - - if (term && selected) { - const suggestion = suggestions.value[Number(selected)]; - if (suggestion) { - ntp.messaging.notify('search_openSuggestion', { suggestion, target }); - } else { - console.warn('not found'); + if (mode === 'ai') { + if (term) { + ntp.messaging.notify('search_submitChat', { chat: String(term), target }); + } + } else if (mode === 'search') { + if (term && selected) { + const suggestion = suggestions.value[Number(selected)]; + if (suggestion) { + ntp.messaging.notify('search_openSuggestion', { suggestion, target }); + } else { + console.warn('not found'); + } + } else if (term) { + console.log({ term, selected, v: selected }); + ntp.messaging.notify('search_submit', { term: String(term), target }); } - } else if (term) { - console.log({ term, selected, v: selected }); - ntp.messaging.notify('search_submit', { term: String(term), target }); } window.dispatchEvent(new Event('clear-all')); @@ -176,9 +178,10 @@ function SuggestionList({ suggestions, selected }) { useEffect(() => { const listener = (e) => { if (!ref.current?.contains(e.target)) { - setTimeout(() => { - window.dispatchEvent(new Event('clear-suggestions')); - }, 0); + // todo: re-instate the click-outside + // setTimeout(() => { + // window.dispatchEvent(new Event('clear-suggestions')); + // }, 0); } }; document.addEventListener('click', listener);