Skip to content

Commit a6e8c22

Browse files
authored
NTP: Add Omnibar widget UI (#1786)
1 parent 155072e commit a6e8c22

19 files changed

+1244
-35
lines changed

special-pages/pages/new-tab/app/components/Icons.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,154 @@ export function SearchIcon({ className }) {
188188
</svg>
189189
);
190190
}
191+
192+
/**
193+
* From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Ai-Chat-16.svg. Inline SVG so that can be styled with CSS.
194+
* @param {object} params
195+
* @param {string} [params.className]
196+
*/
197+
export function AiChatIcon({ className }) {
198+
return (
199+
<svg className={className} fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
200+
<g fill="currentColor" clip-path="url(#Ai-Chat-16_svg__a)">
201+
<path
202+
fill-rule="evenodd"
203+
d="m10.54 12.57-.047.02c-.81.372-2.323.786-4.064 1.174a93 93 0 0 1-3.324.67l.297-.343c.781-.901.554-2.169-.215-2.85-1.22-1.08-1.937-2.539-1.937-4.116C1.25 4.013 4.132 1.25 8 1.25s6.75 2.763 6.75 5.875c0 2.372-1.644 4.514-4.161 5.427l-.049.017Zm.475 1.157c-1.891.868-6.545 1.75-9.2 2.206-.665.114-1.092-.66-.65-1.17l1.293-1.491c.28-.322.22-.813-.1-1.096C.902 10.886 0 9.1 0 7.125 0 3.19 3.582 0 8 0s8 3.19 8 7.125c0 2.985-2.061 5.541-4.985 6.602"
204+
clip-rule="evenodd"
205+
/>
206+
<path d="M7.62 3.271c.099-.396.661-.396.76 0l.22.878a3.27 3.27 0 0 0 2.376 2.376l.878.22c.396.099.396.661 0 .76l-.878.22A3.27 3.27 0 0 0 8.6 10.102l-.219.877c-.099.396-.661.396-.76 0l-.22-.877a3.27 3.27 0 0 0-2.377-2.377l-.877-.22c-.396-.099-.396-.661 0-.76l.877-.22A3.27 3.27 0 0 0 7.4 4.15l.22-.878Z" />
207+
</g>
208+
<defs>
209+
<clipPath id="Ai-Chat-16_svg__a">
210+
<path fill="#fff" d="M0 0h16v16H0z" />
211+
</clipPath>
212+
</defs>
213+
</svg>
214+
);
215+
}
216+
217+
/**
218+
* From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Arrow-Right-16.svg. Inline SVG so that can be styled with CSS.
219+
* @param {object} params
220+
* @param {string} [params.className]
221+
*/
222+
export function ArrowRightIcon({ className }) {
223+
return (
224+
<svg className={className} fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
225+
<path
226+
fill="currentColor"
227+
d="M8.187 1.689a.625.625 0 0 1 .885-.884l5.31 5.316c.83.83.83 2.174 0 3.004l-5.31 5.315a.625.625 0 0 1-.885-.884l5.305-5.308H.625a.625.625 0 1 1 0-1.25h12.867z"
228+
/>
229+
</svg>
230+
);
231+
}
232+
233+
/**
234+
* From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Globe-16.svg.
235+
*/
236+
export function GlobeIcon() {
237+
return (
238+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
239+
<g clip-path="url(#Globe-16_svg__a)">
240+
<path
241+
fill="#000"
242+
fill-rule="evenodd"
243+
d="M.017 7.482a8 8 0 0 1 15.967 0q.025.115.01.225a8 8 0 1 1-15.99 0 .6.6 0 0 1 .013-.225m1.247.951a6.75 6.75 0 0 0 4.197 5.823 7 7 0 0 1-.416-.781c-.555-1.213-.92-2.787-1.018-4.518a29 29 0 0 1-2.763-.524m2.739-.742a28 28 0 0 1-2.7-.535A6.76 6.76 0 0 1 5.46 1.744q-.229.372-.416.781c-.623 1.363-1.006 3.18-1.042 5.166Zm1.286 1.413c.109 1.516.436 2.852.893 3.85.59 1.292 1.28 1.796 1.818 1.796s1.228-.504 1.818-1.795c.457-1 .784-2.335.893-3.85-1.803.17-3.619.17-5.422 0Zm5.46-1.26a27.5 27.5 0 0 1-5.498 0c.018-1.904.38-3.596.93-4.799C6.774 1.755 7.462 1.25 8 1.25s1.228.504 1.818 1.795c.55 1.203.913 2.895.931 4.8Zm1.224 1.113c-.099 1.731-.463 3.305-1.018 4.518a7 7 0 0 1-.416.781 6.75 6.75 0 0 0 4.197-5.823q-1.372.33-2.763.524m2.725-1.801q-1.341.336-2.7.535c-.037-1.985-.42-3.803-1.043-5.166a7 7 0 0 0-.416-.781 6.76 6.76 0 0 1 4.159 5.412"
244+
clip-rule="evenodd"
245+
/>
246+
</g>
247+
<defs>
248+
<clipPath id="Globe-16_svg__a">
249+
<path fill="#fff" d="M0 0h16v16H0z" />
250+
</clipPath>
251+
</defs>
252+
</svg>
253+
);
254+
}
255+
256+
/**
257+
* From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/History-16.svg.
258+
*/
259+
export function HistoryIcon() {
260+
return (
261+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
262+
<g fill="#000" clip-path="url(#History-16_svg__a)">
263+
<path d="m2.072 4.918-.08-.004A6.753 6.753 0 1 1 1.246 8 .623.623 0 1 0 0 8a8 8 0 1 0 1.247-4.29V1.115a.623.623 0 0 0-1.247 0v2.977c0 1.145.928 2.072 2.072 2.072h2.486a.623.623 0 0 0 0-1.246z" />
264+
<path d="M8.625 3.625a.625.625 0 1 0-1.25 0V8c0 .166.066.325.183.442l2.375 2.375a.625.625 0 1 0 .884-.884L8.625 7.741z" />
265+
</g>
266+
<defs>
267+
<clipPath id="History-16_svg__a">
268+
<path fill="#fff" d="M0 0h16v16H0z" />
269+
</clipPath>
270+
</defs>
271+
</svg>
272+
);
273+
}
274+
275+
/**
276+
* From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Favorite-16.svg.
277+
*/
278+
export function FavoriteIcon() {
279+
return (
280+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
281+
<g clip-path="url(#Favorite-16_svg__a)">
282+
<path
283+
fill="#000"
284+
fill-rule="evenodd"
285+
d="M6.042 1.35c.73-1.732 3.186-1.732 3.916 0l1.033 2.452a.63.63 0 0 0 .489.376l2.686.38c1.774.252 2.46 2.45 1.144 3.666l-2.046 1.889a.63.63 0 0 0-.194.552l.434 2.88c.272 1.812-1.72 3.096-3.258 2.1L8.34 14.409a.63.63 0 0 0-.68 0l-1.906 1.236c-1.537.996-3.53-.288-3.258-2.1l.434-2.88a.63.63 0 0 0-.194-.552L.69 8.223C-.626 7.009.06 4.81 1.834 4.56l2.686-.381a.63.63 0 0 0 .489-.376zm2.764.486c-.3-.714-1.312-.714-1.612 0L6.16 4.287a1.88 1.88 0 0 1-1.465 1.128l-2.687.381a.875.875 0 0 0-.47 1.51l2.045 1.889c.457.421.675 1.042.582 1.656l-.433 2.88a.875.875 0 0 0 1.34.865L6.98 13.36a1.88 1.88 0 0 1 2.04 0l1.906 1.236a.875.875 0 0 0 1.341-.864l-.433-2.881a1.88 1.88 0 0 1 .582-1.656l2.046-1.89a.875.875 0 0 0-.471-1.509l-2.687-.38a1.88 1.88 0 0 1-1.464-1.13z"
286+
clip-rule="evenodd"
287+
/>
288+
</g>
289+
<defs>
290+
<clipPath id="Favorite-16_svg__a">
291+
<path fill="#fff" d="M0 0h16v16H0z" />
292+
</clipPath>
293+
</defs>
294+
</svg>
295+
);
296+
}
297+
298+
/**
299+
* From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Bookmark-16.svg.
300+
*/
301+
export function BookmarkIcon() {
302+
return (
303+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
304+
<g clip-path="url(#Bookmark-16_svg__a)">
305+
<path
306+
fill="#000"
307+
fill-rule="evenodd"
308+
d="M2 4a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v9.684c0 1.857-2.079 2.957-3.614 1.912l-1.788-1.218a1.06 1.06 0 0 0-1.196 0l-1.788 1.218C4.08 16.64 2 15.54 2 13.684zm4-2.75A2.75 2.75 0 0 0 3.25 4v9.684c0 .854.955 1.359 1.66.878l1.788-1.217a2.31 2.31 0 0 1 2.604 0l1.787 1.217a1.063 1.063 0 0 0 1.661-.878V4A2.75 2.75 0 0 0 10 1.25z"
309+
clip-rule="evenodd"
310+
/>
311+
</g>
312+
<defs>
313+
<clipPath id="Bookmark-16_svg__a">
314+
<path fill="#fff" d="M0 0h16v16H0z" />
315+
</clipPath>
316+
</defs>
317+
</svg>
318+
);
319+
}
320+
321+
/**
322+
* From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Browser-16.svg.
323+
*/
324+
export function BrowserIcon() {
325+
return (
326+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
327+
<path
328+
fill="#000"
329+
fill-rule="evenodd"
330+
d="M0 5a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v1.792c0 .478-.681.721-1.053.422a.52.52 0 0 1-.197-.4v-.819H1.25V11A2.75 2.75 0 0 0 4 13.75h2.135a.57.57 0 0 1 .497.312c.21.398-.055.938-.506.938H4a4 4 0 0 1-4-4zm1.262-.255h13.476A2.75 2.75 0 0 0 12 2.25H4a2.75 2.75 0 0 0-2.738 2.495"
331+
clip-rule="evenodd"
332+
/>
333+
<path
334+
fill="#000"
335+
fill-rule="evenodd"
336+
d="M11.5 7a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9m-1.972 7.084A3.25 3.25 0 0 1 8.288 12h.85a7.6 7.6 0 0 0 .39 2.084M8.337 10.75h.818a7.5 7.5 0 0 1 .373-1.834 3.25 3.25 0 0 0-1.191 1.834m2.495 3.233c-.226-.5-.392-1.19-.441-1.983h2.218c-.05.793-.215 1.482-.441 1.983-.299.66-.583.767-.668.767s-.37-.106-.668-.767m0-4.966c-.202.447-.356 1.045-.422 1.733h2.18c-.066-.688-.22-1.286-.422-1.733-.299-.66-.583-.767-.668-.767s-.37.106-.668.767m2.64 5.067c.213-.606.348-1.32.39-2.084h.85a3.25 3.25 0 0 1-1.24 2.084m.373-3.334h.818a3.25 3.25 0 0 0-1.19-1.834c.188.54.316 1.164.371 1.834Z"
337+
clip-rule="evenodd"
338+
/>
339+
</svg>
340+
);
341+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import cn from 'classnames';
2+
import { h } from 'preact';
3+
import { useContext } from 'preact/hooks';
4+
import { ArrowRightIcon } from '../../components/Icons';
5+
import { useTypedTranslationWith } from '../../types';
6+
import styles from './Omnibar.module.css';
7+
import { OmnibarContext } from './OmnibarProvider';
8+
9+
/**
10+
* @typedef {import('../strings.json')} Strings
11+
*/
12+
13+
/**
14+
* @param {object} props
15+
* @param {string} props.chat
16+
* @param {(chat: string) => void} props.setChat
17+
*/
18+
export function AiChatForm({ chat, setChat }) {
19+
const { submitChat } = useContext(OmnibarContext);
20+
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
21+
22+
/** @type {(event: SubmitEvent) => void} */
23+
const onSubmit = (event) => {
24+
event.preventDefault();
25+
submitChat({
26+
chat,
27+
target: 'same-tab',
28+
});
29+
};
30+
31+
return (
32+
<div class={styles.formWrap}>
33+
<form onSubmit={onSubmit} class={styles.form}>
34+
<div class={styles.inputRoot} style={{ viewTransitionName: 'omnibar-input-transition' }}>
35+
<div class={styles.inputContainer} style={{ viewTransitionName: 'omnibar-input-transition2' }}>
36+
<input
37+
type="text"
38+
class={styles.input}
39+
value={chat}
40+
placeholder={t('aiChatForm_placeholder')}
41+
aria-label={t('aiChatForm_placeholder')}
42+
autoComplete="off"
43+
onChange={(event) => {
44+
if (event.target instanceof HTMLInputElement) {
45+
setChat(event.target.value);
46+
}
47+
}}
48+
/>
49+
<div class={styles.inputActions}>
50+
<button
51+
class={cn(styles.inputAction, styles.squareButton, styles.aiSubmitButton)}
52+
aria-label={t('aiChatForm_submitButtonLabel')}
53+
>
54+
<ArrowRightIcon />
55+
</button>
56+
</div>
57+
</div>
58+
</div>
59+
</form>
60+
</div>
61+
);
62+
}
Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,61 @@
11
import { h } from 'preact';
2+
import { useState } from 'preact/hooks';
3+
import { AiChatIcon, SearchIcon } from '../../components/Icons.js';
4+
import { useTypedTranslationWith } from '../../types';
5+
import { viewTransition } from '../../utils';
6+
import { AiChatForm } from './AiChatForm';
27
import styles from './Omnibar.module.css';
8+
import { SearchForm } from './SearchForm';
39

410
/**
511
* @typedef {import('../strings.json')} Strings
612
* @typedef {import('../../../types/new-tab.js').OmnibarConfig} OmnibarConfig
7-
* @typedef {import('../../../types/new-tab.js').SuggestionsData} SuggestionsData
8-
* @typedef {import('../../../types/new-tab.js').Suggestion} Suggestion
9-
* @typedef {import('../../../types/new-tab.js').OpenTarget} OpenTarget
1013
*/
1114

1215
/**
1316
* @param {object} props
1417
* @param {OmnibarConfig['mode']} props.mode
1518
* @param {(mode: OmnibarConfig['mode']) => void} props.setMode
16-
* @param {(term: string) => Promise<SuggestionsData>} props.getSuggestions
17-
* @param {(params: {suggestion: Suggestion, target: OpenTarget}) => void} props.openSuggestion
18-
* @param {(params: {term: string, target: OpenTarget}) => void} props.submitSearch
19-
* @param {(params: {chat: string, target: OpenTarget}) => void} props.submitChat
2019
*/
21-
export function Omnibar(props) {
22-
return <div class={styles.root}>Omnibar goes here. Mode = {props.mode}</div>;
20+
export function Omnibar({ mode, setMode }) {
21+
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
22+
const [query, setQuery] = useState(/** @type {String} */ (''));
23+
return (
24+
<div class={styles.root} data-mode={mode}>
25+
<div class={styles.logoWrap}>
26+
<img src="./icons/Logo-Stacked.svg" alt={t('omnibar_logoAlt')} width={144} height={115.9} />
27+
</div>
28+
<div class={styles.tabListWrap}>
29+
<div class={styles.tabList} role="tablist" aria-label={t('omnibar_tabSwitcherLabel')}>
30+
<button
31+
class={styles.tab}
32+
role="tab"
33+
aria-selected={mode === 'search'}
34+
onClick={() => {
35+
viewTransition(() => {
36+
setMode('search');
37+
});
38+
}}
39+
>
40+
<SearchIcon className={styles.searchIcon} />
41+
{t('omnibar_searchTabLabel')}
42+
</button>
43+
<button
44+
class={styles.tab}
45+
role="tab"
46+
aria-selected={mode === 'ai'}
47+
onClick={() => {
48+
viewTransition(() => {
49+
setMode('ai');
50+
});
51+
}}
52+
>
53+
<AiChatIcon className={styles.aiChatIcon} />
54+
{t('omnibar_aiTabLabel')}
55+
</button>
56+
</div>
57+
</div>
58+
{mode === 'search' ? <SearchForm term={query} setTerm={setQuery} /> : <AiChatForm chat={query} setChat={setQuery} />}
59+
</div>
60+
);
2361
}

0 commit comments

Comments
 (0)