From e83a4fc032fa9b1af3f655d48dc24d21f4062d17 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 9 Jul 2025 13:58:50 +1000 Subject: [PATCH 01/18] Search form and suggestions list styling --- .../pages/new-tab/app/components/Icons.js | 15 ++- .../new-tab/app/omnibar/components/Omnibar.js | 6 +- .../app/omnibar/components/SearchForm.js | 100 +++++++----------- .../omnibar/components/SearchForm.module.css | 38 +++++++ .../components/SuggestionList.module.css | 48 --------- .../{SuggestionList.js => SuggestionsList.js} | 2 +- .../components/SuggestionsList.module.css | 28 +++++ 7 files changed, 114 insertions(+), 123 deletions(-) create mode 100644 special-pages/pages/new-tab/app/omnibar/components/SearchForm.module.css delete mode 100644 special-pages/pages/new-tab/app/omnibar/components/SuggestionList.module.css rename special-pages/pages/new-tab/app/omnibar/components/{SuggestionList.js => SuggestionsList.js} (98%) create mode 100644 special-pages/pages/new-tab/app/omnibar/components/SuggestionsList.module.css diff --git a/special-pages/pages/new-tab/app/components/Icons.js b/special-pages/pages/new-tab/app/components/Icons.js index e1cce5c32b..d20229d89c 100644 --- a/special-pages/pages/new-tab/app/components/Icons.js +++ b/special-pages/pages/new-tab/app/components/Icons.js @@ -166,12 +166,11 @@ export function BackChevron() { /** * From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Find-Search-16.svg. Inline SVG so that can be styled with CSS. - * @param {object} params - * @param {string} [params.className] + * @param {import('preact').JSX.SVGAttributes} props */ -export function SearchIcon({ className }) { +export function SearchIcon(props) { return ( - + - + @@ -280,7 +279,7 @@ export function FavoriteIcon() { )} - {mode === 'search' ? ( - - ) : ( - - )} + {mode === 'search' ? : } ); } diff --git a/special-pages/pages/new-tab/app/omnibar/components/SearchForm.js b/special-pages/pages/new-tab/app/omnibar/components/SearchForm.js index 2487cbff72..30eaa4e4d8 100644 --- a/special-pages/pages/new-tab/app/omnibar/components/SearchForm.js +++ b/special-pages/pages/new-tab/app/omnibar/components/SearchForm.js @@ -1,13 +1,11 @@ import cn from 'classnames'; -import { Fragment, h } from 'preact'; +import { h } from 'preact'; import { useContext, useId } from 'preact/hooks'; -import { eventToTarget } from '../../../../../shared/handlers'; -import { AiChatIcon, SearchIcon } from '../../components/Icons.js'; -import { usePlatformName } from '../../settings.provider'; +import { SearchIcon } from '../../components/Icons.js'; import { useTypedTranslationWith } from '../../types'; -import styles from './Omnibar.module.css'; import { OmnibarContext } from './OmnibarProvider'; -import { SuggestionsList } from './SuggestionList.js'; +import styles from './SearchForm.module.css'; +import { SuggestionsList } from './SuggestionsList.js'; import { useSuggestionInput } from './useSuggestionInput.js'; import { useSuggestions } from './useSuggestions'; @@ -15,17 +13,15 @@ import { useSuggestions } from './useSuggestions'; * @typedef {import('../strings.json')} Strings */ -/** +/* * @param {object} props - * @param {boolean} props.enableAi * @param {string} props.term * @param {(term: string) => void} props.setTerm */ -export function SearchForm({ enableAi, term, setTerm }) { - const { submitSearch, submitChat } = useContext(OmnibarContext); +export function SearchForm({ term, setTerm }) { + const { submitSearch } = useContext(OmnibarContext); const { t } = useTypedTranslationWith(/** @type {Strings} */ ({})); - const platformName = usePlatformName(); const suggestionsListId = useId(); const { @@ -56,54 +52,36 @@ export function SearchForm({ enableAi, term, setTerm }) { }; return ( -
-
-
-
- 0} - aria-haspopup="listbox" - aria-controls={suggestionsListId} - aria-activedescendant={selectedSuggestion?.id} - spellcheck={false} - autoComplete="off" - autoCorrect="off" - autoCapitalize="off" - onChange={onInputChange} - onKeyDown={onInputKeyDown} - onClick={onInputClick} - /> -
- - {enableAi && ( - <> -
- - - )} -
-
-
+ 0, + })} + onBlur={onFormBlur} + onSubmit={onFormSubmit} + > +
inputRef.current?.focus()}> + + 0} + aria-haspopup="listbox" + aria-controls={suggestionsListId} + aria-activedescendant={selectedSuggestion?.id} + spellcheck={false} + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + onChange={onInputChange} + onKeyDown={onInputKeyDown} + onClick={onInputClick} + /> +
+ {suggestions.length > 0 && ( - -
+ )} + ); } diff --git a/special-pages/pages/new-tab/app/omnibar/components/SearchForm.module.css b/special-pages/pages/new-tab/app/omnibar/components/SearchForm.module.css new file mode 100644 index 0000000000..4e6615770d --- /dev/null +++ b/special-pages/pages/new-tab/app/omnibar/components/SearchForm.module.css @@ -0,0 +1,38 @@ +.form.hasSuggestions { + background: var(--ntp-surfaces-panel-background-color); + border-radius: 16px; + box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.08), 0px 4px 8px 0px rgba(0, 0, 0, 0.16); +} + +.container { + align-items: center; + display: flex; + padding: var(--sp-1); + + .form:not(.hasSuggestions) & { + background: var(--ntp-surfaces-panel-background-color); + border-radius: var(--border-radius-lg); + box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.1), 0px 4px 8px 0px rgba(0, 0, 0, 0.08); + + &:has(.input:focus) { + border-radius: 14px; + box-shadow: 0 0 0 2px var(--ntp-color-primary), 0 0 0 4px rgba(57, 105, 239, 0.2); + } + } +} + +.icon { + height: var(--sp-4); + margin: var(--sp-2); + width: var(--sp-4); +} + +.input { + border: none; + flex: 1 0 0; + height: var(--sp-8); + + &:focus { + outline: none; + } +} diff --git a/special-pages/pages/new-tab/app/omnibar/components/SuggestionList.module.css b/special-pages/pages/new-tab/app/omnibar/components/SuggestionList.module.css deleted file mode 100644 index 3f4b13f959..0000000000 --- a/special-pages/pages/new-tab/app/omnibar/components/SuggestionList.module.css +++ /dev/null @@ -1,48 +0,0 @@ -/* @todo: use proper vars */ - -.list { - 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; -} - -.item { - display: flex; - align-items: center; - text-decoration: none; - 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; - gap: 8px; - - svg { - width: 16px; - height: 16px; - display: block; - } - - svg path { - fill-opacity: 1!important; - } - - &[aria-selected="true"] { - background: var(--ddg-color-primary); - color: white; - border-radius: 4px; - } -} diff --git a/special-pages/pages/new-tab/app/omnibar/components/SuggestionList.js b/special-pages/pages/new-tab/app/omnibar/components/SuggestionsList.js similarity index 98% rename from special-pages/pages/new-tab/app/omnibar/components/SuggestionList.js rename to special-pages/pages/new-tab/app/omnibar/components/SuggestionsList.js index ef78520492..0575c24052 100644 --- a/special-pages/pages/new-tab/app/omnibar/components/SuggestionList.js +++ b/special-pages/pages/new-tab/app/omnibar/components/SuggestionsList.js @@ -4,7 +4,7 @@ import { eventToTarget } from '../../../../../shared/handlers'; import { BookmarkIcon, BrowserIcon, FavoriteIcon, GlobeIcon, HistoryIcon, SearchIcon } from '../../components/Icons'; import { usePlatformName } from '../../settings.provider'; import { OmnibarContext } from './OmnibarProvider'; -import styles from './SuggestionList.module.css'; +import styles from './SuggestionsList.module.css'; /** * @typedef {import('./useSuggestions').SuggestionModel} SuggestionModel diff --git a/special-pages/pages/new-tab/app/omnibar/components/SuggestionsList.module.css b/special-pages/pages/new-tab/app/omnibar/components/SuggestionsList.module.css new file mode 100644 index 0000000000..3242a7f8ba --- /dev/null +++ b/special-pages/pages/new-tab/app/omnibar/components/SuggestionsList.module.css @@ -0,0 +1,28 @@ +.list { + border-top: 1px solid var(--ntp-surface-border-color); + display: flex; + flex-direction: column; + padding: 7px; +} + +.item { + align-items: center; + background: var(--ntp-surfaces-panel-background-color); + border: none; + cursor: pointer; + display: flex; + height: var(--sp-8); + justify-content: flex-start; + + svg { + height: var(--sp-4); + margin: var(--sp-2); + width: var(--sp-4); + } + + &[aria-selected="true"] { + background: var(--ddg-color-primary); + border-radius: 4px; + color: var(--color-white); + } +} From cf78b847da9340ce0105a7a80bd364bc83d98710 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 9 Jul 2025 14:14:50 +1000 Subject: [PATCH 02/18] Root container styling --- .../new-tab/app/omnibar/components/Omnibar.js | 40 +--- .../app/omnibar/components/Omnibar.module.css | 203 +----------------- .../omnibar/components/SearchForm.module.css | 12 +- .../app/omnibar/components/TabSwitcher.js | 52 +++++ .../omnibar/components/TabSwitcher.module.css | 50 +++++ 5 files changed, 123 insertions(+), 234 deletions(-) create mode 100644 special-pages/pages/new-tab/app/omnibar/components/TabSwitcher.js create mode 100644 special-pages/pages/new-tab/app/omnibar/components/TabSwitcher.module.css diff --git a/special-pages/pages/new-tab/app/omnibar/components/Omnibar.js b/special-pages/pages/new-tab/app/omnibar/components/Omnibar.js index 711e863d6e..07b9a359e5 100644 --- a/special-pages/pages/new-tab/app/omnibar/components/Omnibar.js +++ b/special-pages/pages/new-tab/app/omnibar/components/Omnibar.js @@ -1,11 +1,10 @@ import { h } from 'preact'; import { useState } from 'preact/hooks'; -import { AiChatIcon, SearchIcon } from '../../components/Icons.js'; import { useTypedTranslationWith } from '../../types'; -import { viewTransition } from '../../utils'; import { AiChatForm } from './AiChatForm'; import styles from './Omnibar.module.css'; import { SearchForm } from './SearchForm'; +import { TabSwitcher } from './TabSwitcher'; /** * @typedef {import('../strings.json')} Strings @@ -23,41 +22,8 @@ export function Omnibar({ mode, setMode, enableAi }) { const [query, setQuery] = useState(/** @type {String} */ ('')); return (
-
- {t('omnibar_logoAlt')} -
- {enableAi && ( -
-
- - -
-
- )} + {t('omnibar_logoAlt')} + {enableAi && } {mode === 'search' ? : }
); diff --git a/special-pages/pages/new-tab/app/omnibar/components/Omnibar.module.css b/special-pages/pages/new-tab/app/omnibar/components/Omnibar.module.css index 9b3e206139..224cef1295 100644 --- a/special-pages/pages/new-tab/app/omnibar/components/Omnibar.module.css +++ b/special-pages/pages/new-tab/app/omnibar/components/Omnibar.module.css @@ -1,194 +1,11 @@ -/* @todo: use proper vars */ - .root { - margin-bottom: var(--ntp-gap); - position: relative; - } - - .logoWrap { - display: flex; - flex-direction: column; - align-items: center; - } - - .tabListWrap { - display: flex; - 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); - } - - .tabList { - display: flex; - background: var(--color-black-at-6); - padding: 4px; - border-radius: 999px; - gap: 4px; - width: auto; - } - - .tab { - 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; - transition: background .2s; - - svg { - width: 16px; - height: 16px; - } - - &:hover { - background: var(--color-black-at-9); - } - - &[aria-selected="true"] { - background: var(--color-white); - box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.08), 0px 2px 4px 0px rgba(0, 0, 0, 0.10); - - .searchIcon { - color: blue; - } - - .aiChatIcon { - color: var(--duckai-purple); - } - } - } - - .inputRoot { - position: relative; - width: 100%; - margin: 0 auto; - padding: 8px 8px; - display: flex; - flex-direction: column; - gap: 8px; - } - - ::view-transition-new(omnibar-input-transition) { - animation: 0.25s grow-y; - } - - .inputContainer { - position: relative; - display: flex; - align-items: center; - height: 32px; - } - -.input { - flex: 1; - height: 32px; - padding: 0; - padding: 0 8px; - border: none; - font-size: 16px; - outline: none; - color: #202124; - background-color: transparent; - } - - .input::placeholder { - color: #9aa0a6; - } - - .inputActions { - display: flex; - align-items: center; - } - - .inputAction { - 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; - } - - .aiChatIcon { - color: var(--color-black-at-84); - } - } - - .inputAction:hover { - background-color: rgba(60, 64, 67, 0.08); - } - - .inputAction.active { - color: #1a73e8; - } - - .separator { - height: 24px; - width: 1px; - background-color: #dadce0; - margin: 0 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%); - } - } - - .aiSubmitButton { - background-color: var(--duckai-purple); - color: #fff; - position: relative; - width: 32px; - height: 32px; - - &:hover { - background-color: var(--duckai-purple-hover); - } - - &:active { - background-color: var(--duckai-purple-active); - } - } + align-items: center; + display: flex; + flex-direction: column; + gap: 10px; + padding-bottom: var(--sp-15); +} + +.logo { + margin-bottom: var(--sp-4); +} diff --git a/special-pages/pages/new-tab/app/omnibar/components/SearchForm.module.css b/special-pages/pages/new-tab/app/omnibar/components/SearchForm.module.css index 4e6615770d..46ebde3470 100644 --- a/special-pages/pages/new-tab/app/omnibar/components/SearchForm.module.css +++ b/special-pages/pages/new-tab/app/omnibar/components/SearchForm.module.css @@ -1,7 +1,11 @@ -.form.hasSuggestions { - background: var(--ntp-surfaces-panel-background-color); - border-radius: 16px; - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.08), 0px 4px 8px 0px rgba(0, 0, 0, 0.16); +.form { + align-self: stretch; + + &.hasSuggestions { + background: var(--ntp-surfaces-panel-background-color); + border-radius: 16px; + box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.08), 0px 4px 8px 0px rgba(0, 0, 0, 0.16); + } } .container { diff --git a/special-pages/pages/new-tab/app/omnibar/components/TabSwitcher.js b/special-pages/pages/new-tab/app/omnibar/components/TabSwitcher.js new file mode 100644 index 0000000000..42baa53196 --- /dev/null +++ b/special-pages/pages/new-tab/app/omnibar/components/TabSwitcher.js @@ -0,0 +1,52 @@ +import { h } from 'preact'; +import { AiChatIcon, SearchIcon } from '../../components/Icons.js'; +import { useTypedTranslationWith } from '../../types'; +import { viewTransition } from '../../utils'; +import styles from './TabSwitcher.module.css'; + +/** + * @typedef {import('../strings.json')} Strings + * @typedef {import('../../../types/new-tab.js').OmnibarConfig} OmnibarConfig + */ + +/** + * @param {object} props + * @param {OmnibarConfig['mode']} props.mode + * @param {(mode: OmnibarConfig['mode']) => void} props.setMode + */ +export function TabSwitcher({ mode, setMode }) { + const { t } = useTypedTranslationWith(/** @type {Strings} */ ({})); + + return ( +
+
+ + +
+
+ ); +} diff --git a/special-pages/pages/new-tab/app/omnibar/components/TabSwitcher.module.css b/special-pages/pages/new-tab/app/omnibar/components/TabSwitcher.module.css new file mode 100644 index 0000000000..c373ff85c5 --- /dev/null +++ b/special-pages/pages/new-tab/app/omnibar/components/TabSwitcher.module.css @@ -0,0 +1,50 @@ +.tabListWrap { + display: flex; + justify-content: center; + /* margin-top: 24px; */ +} + +.tabList { + display: flex; + background: var(--color-black-at-6); + padding: 4px; + border-radius: 999px; + gap: 4px; + width: auto; +} + +.tab { + 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; + transition: background .2s; + + svg { + width: 16px; + height: 16px; + } + + &:hover { + background: var(--color-black-at-9); + } + + &[aria-selected="true"] { + background: var(--color-white); + box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.08), 0px 2px 4px 0px rgba(0, 0, 0, 0.10); + + .searchIcon { + color: blue; + } + + .aiChatIcon { + color: var(--duckai-purple); + } + } +} From 4f1f7819ec43e2a982d8b34b41ecc11bdab0d064 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 10 Jul 2025 09:51:25 +1000 Subject: [PATCH 03/18] Search form and root container spacing tweaks --- special-pages/pages/new-tab/app/omnibar/components/Omnibar.js | 2 +- .../pages/new-tab/app/omnibar/components/Omnibar.module.css | 1 + .../pages/new-tab/app/omnibar/components/SearchForm.module.css | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/special-pages/pages/new-tab/app/omnibar/components/Omnibar.js b/special-pages/pages/new-tab/app/omnibar/components/Omnibar.js index 07b9a359e5..895f5932a0 100644 --- a/special-pages/pages/new-tab/app/omnibar/components/Omnibar.js +++ b/special-pages/pages/new-tab/app/omnibar/components/Omnibar.js @@ -22,7 +22,7 @@ export function Omnibar({ mode, setMode, enableAi }) { const [query, setQuery] = useState(/** @type {String} */ ('')); return (
- {t('omnibar_logoAlt')} + {t('omnibar_logoAlt')} {enableAi && } {mode === 'search' ? : }
diff --git a/special-pages/pages/new-tab/app/omnibar/components/Omnibar.module.css b/special-pages/pages/new-tab/app/omnibar/components/Omnibar.module.css index 224cef1295..540b5ef3c7 100644 --- a/special-pages/pages/new-tab/app/omnibar/components/Omnibar.module.css +++ b/special-pages/pages/new-tab/app/omnibar/components/Omnibar.module.css @@ -8,4 +8,5 @@ .logo { margin-bottom: var(--sp-4); + width: 123px; } diff --git a/special-pages/pages/new-tab/app/omnibar/components/SearchForm.module.css b/special-pages/pages/new-tab/app/omnibar/components/SearchForm.module.css index 46ebde3470..fcb1e7abc5 100644 --- a/special-pages/pages/new-tab/app/omnibar/components/SearchForm.module.css +++ b/special-pages/pages/new-tab/app/omnibar/components/SearchForm.module.css @@ -1,5 +1,6 @@ .form { align-self: stretch; + margin: 0 calc(-1 * var(--sp-1)); &.hasSuggestions { background: var(--ntp-surfaces-panel-background-color); @@ -15,7 +16,7 @@ .form:not(.hasSuggestions) & { background: var(--ntp-surfaces-panel-background-color); - border-radius: var(--border-radius-lg); + border-radius: 11px; box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.1), 0px 4px 8px 0px rgba(0, 0, 0, 0.08); &:has(.input:focus) { From 92a0c5c7b2e61f3d2ff7040c0253fb7ef3887358 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 10 Jul 2025 10:40:46 +1000 Subject: [PATCH 04/18] AI chat form styling --- .../app/omnibar/components/AiChatForm.js | 70 ++++++++++------- .../omnibar/components/AiChatForm.module.css | 78 +++++++++++++++++++ 2 files changed, 121 insertions(+), 27 deletions(-) create mode 100644 special-pages/pages/new-tab/app/omnibar/components/AiChatForm.module.css diff --git a/special-pages/pages/new-tab/app/omnibar/components/AiChatForm.js b/special-pages/pages/new-tab/app/omnibar/components/AiChatForm.js index 0763c3c89b..4e3fb4108a 100644 --- a/special-pages/pages/new-tab/app/omnibar/components/AiChatForm.js +++ b/special-pages/pages/new-tab/app/omnibar/components/AiChatForm.js @@ -1,9 +1,8 @@ -import cn from 'classnames'; import { h } from 'preact'; -import { useContext } from 'preact/hooks'; +import { useContext, useRef } from 'preact/hooks'; import { ArrowRightIcon } from '../../components/Icons'; import { useTypedTranslationWith } from '../../types'; -import styles from './Omnibar.module.css'; +import styles from './AiChatForm.module.css'; import { OmnibarContext } from './OmnibarProvider'; /** @@ -19,6 +18,9 @@ export function AiChatForm({ chat, setChat }) { const { submitChat } = useContext(OmnibarContext); const { t } = useTypedTranslationWith(/** @type {Strings} */ ({})); + const formRef = useRef(/** @type {HTMLFormElement|null} */ (null)); + const textAreaRef = useRef(/** @type {HTMLTextAreaElement|null} */ (null)); + /** @type {(event: SubmitEvent) => void} */ const onSubmit = (event) => { event.preventDefault(); @@ -29,30 +31,44 @@ export function AiChatForm({ chat, setChat }) { }; return ( -
-
-
-
- setChat(event.currentTarget.value)} - /> -
- -
-
+ +
textAreaRef.current?.focus()}> +