diff --git a/bundlesize.config.json b/bundlesize.config.json
index 7097775d2fd..6e06baa217a 100644
--- a/bundlesize.config.json
+++ b/bundlesize.config.json
@@ -102,7 +102,7 @@
},
{
"path": "static/build/page-user.css",
- "maxSize": "27KB"
+ "maxSize": "27.05KB"
}
]
}
diff --git a/openlibrary/i18n/messages.pot b/openlibrary/i18n/messages.pot
index c463ba8c654..1b02f77d596 100644
--- a/openlibrary/i18n/messages.pot
+++ b/openlibrary/i18n/messages.pot
@@ -1030,17 +1030,13 @@ msgstr ""
msgid "Solr Editions"
msgstr ""
-#: admin/inspect/store.html search/lists.html work_search.html
-msgid "No results found."
-msgstr ""
-
-#: search/lists.html work_search.html
+#: work_search.html
#, python-format
-msgid "Search for books containing the phrase \"%s\"?"
+msgid "No %(path_id)s directly matched your search"
msgstr ""
-#: work_search.html
-msgid "Add a new book to Open Library?"
+#: search/authors.html search/lists.html search/subjects.html work_search.html
+msgid "Checking Search Inside matches"
msgstr ""
#: account/reading_log.html search/authors.html search/subjects.html
@@ -1490,8 +1486,8 @@ msgstr ""
msgid "Thanks!"
msgstr ""
-#: BookByline.html SearchResultsWork.html account/notes.html
-#: account/observations.html
+#: BookByline.html FulltextSearchSuggestionItem.html SearchResultsWork.html
+#: account/notes.html account/observations.html
msgid "Unknown author"
msgstr ""
@@ -2668,6 +2664,10 @@ msgstr ""
msgid "Json"
msgstr ""
+#: admin/inspect/store.html
+msgid "No results found."
+msgstr ""
+
#: admin/ip/index.html
msgid "[Admin Center] IP Addresses"
msgstr ""
@@ -3688,10 +3688,10 @@ msgstr ""
msgid "Links"
msgstr ""
-#: IABook.html SearchResultsWork.html books/edition-sort.html
-#: jsdef/LazyWorkPreview.html lists/list_overview.html lists/preview.html
-#: lists/snippet.html lists/widget.html my_books/dropdown_content.html
-#: type/work/editions.html
+#: FulltextSearchSuggestionItem.html IABook.html SearchResultsWork.html
+#: books/edition-sort.html jsdef/LazyWorkPreview.html lists/list_overview.html
+#: lists/preview.html lists/snippet.html lists/widget.html
+#: my_books/dropdown_content.html type/work/editions.html
#, python-format
msgid "Cover of: %(title)s"
msgstr ""
@@ -6050,7 +6050,7 @@ msgid "Merge authors"
msgstr ""
#: search/authors.html
-msgid "No hits"
+msgid "No authors directly matched your search"
msgstr ""
#: search/inside.html
@@ -6058,14 +6058,13 @@ msgstr ""
msgid "Search Open Library for %s"
msgstr ""
-#: BookSearchInside.html SearchNavigation.html search/inside.html
-#: search/snippets.html
+#: BookSearchInside.html FulltextSearchSuggestion.html SearchNavigation.html
+#: search/inside.html search/snippets.html
msgid "Search Inside"
msgstr ""
#: search/inside.html
-#, python-format
-msgid "No hits for: %(query)s"
+msgid "No Search Inside text matched your search"
msgstr ""
#: search/inside.html
@@ -6090,6 +6089,10 @@ msgstr ""
msgid "Search Lists"
msgstr ""
+#: search/lists.html
+msgid "No lists directly matched your search"
+msgstr ""
+
#: search/publishers.html
msgid "Publishers Search"
msgstr ""
@@ -6155,6 +6158,10 @@ msgstr ""
msgid "Search Subjects"
msgstr ""
+#: search/subjects.html
+msgid "No subjects directly matched your search"
+msgstr ""
+
#: search/subjects.html
msgid "time"
msgstr ""
@@ -7325,10 +7332,49 @@ msgstr ""
msgid "Expires"
msgstr ""
+#: FulltextSearchSuggestion.html
+msgid "Checking for Search Inside matches"
+msgstr ""
+
+#: FulltextSearchSuggestion.html
+msgid "Search Inside Icon"
+msgstr ""
+
+#: FulltextSearchSuggestion.html
+msgid "search inside icon"
+msgstr ""
+
+#: FulltextSearchSuggestion.html
+#, python-format
+msgid "%(book_count)s books found with matching passages"
+msgstr ""
+
+#: FulltextSearchSuggestion.html
+#, python-format
+msgid "See all %(results_count)s Search Inside Matches"
+msgstr ""
+
+#: FulltextSearchSuggestion.html
+msgid "right chevron"
+msgstr ""
+
#: FulltextSnippet.html
msgid "See All Results"
msgstr ""
+#: FulltextSuggestionSnippet.html
+msgid "❝"
+msgstr ""
+
+#: FulltextSuggestionSnippet.html
+msgid "❞"
+msgstr ""
+
+#: FulltextSuggestionSnippet.html
+#, python-format
+msgid "Page: %(page)s"
+msgstr ""
+
#: IABook.html
#, python-format
msgid "Borrowed from Internet Archive: %(title)s"
diff --git a/openlibrary/macros/FulltextSearchSuggestion.html b/openlibrary/macros/FulltextSearchSuggestion.html
new file mode 100644
index 00000000000..30fd62fe4e2
--- /dev/null
+++ b/openlibrary/macros/FulltextSearchSuggestion.html
@@ -0,0 +1,35 @@
+$def with(query, results, page=1)
+
+$def render_snippet(query, doc):
+ $:macros.FulltextSuggestionSnippet(query, doc=doc)
+
+$if results and results.get('hits'):
+ $ hits = results['hits'].get('hits', [])[:4]
+ $ num_found = commify(results['hits'].get('total', 0))
+ $ ia_base_url = "https://archive.org"
+
+ $:macros.LoadingIndicator(_("Checking for Search Inside matches"))
+
+
+ $for doc in hits:
+ $if doc.get('edition'):
+ $:macros.FulltextSearchSuggestionItem(doc['edition'], snippet=render_snippet(query, doc))
+
+
\ No newline at end of file
diff --git a/openlibrary/macros/FulltextSearchSuggestionItem.html b/openlibrary/macros/FulltextSearchSuggestionItem.html
new file mode 100644
index 00000000000..220c0baa071
--- /dev/null
+++ b/openlibrary/macros/FulltextSearchSuggestionItem.html
@@ -0,0 +1,91 @@
+$def with (doc, snippet=None, attrs=None, blur=False)
+
+$code:
+ max_rendered_authors = 9
+ doc_type = (
+ 'infogami_work' if doc.get('type', {}).get('key') == '/type/work' else
+ 'infogami_edition' if doc.get('type', {}).get('key') == '/type/edition' else
+ 'solr_work' if not doc.get('editions') else
+ 'solr_edition'
+ )
+
+ selected_ed = doc
+ if doc_type == 'solr_edition':
+ selected_ed = doc.get('editions')[0]
+
+ book_url = doc.url() if doc_type.startswith('infogami_') else doc.key
+ if doc_type == 'solr_edition':
+ work_edition_url = book_url + '?edition=' + urlquote('key:' + selected_ed.key)
+ else:
+ book_provider = get_book_provider(doc)
+ if book_provider and doc_type.endswith('_work'):
+ work_edition_url = book_url + '?edition=' + urlquote(book_provider.get_best_identifier_slug(doc))
+ else:
+ work_edition_url = book_url
+
+ work_edition_all_url = work_edition_url
+ if '?' in work_edition_url:
+ work_edition_all_url += '&mode=all'
+ else:
+ work_edition_all_url += '?mode=all'
+
+ edition_work = None
+ if doc_type == 'infogami_edition' and 'works' in doc:
+ edition_work = doc['works'][0]
+
+ full_title = selected_ed.get('title', '') + (': ' + selected_ed.subtitle if selected_ed.get('subtitle') else '')
+ if doc_type == 'infogami_edition' and edition_work:
+ full_work_title = edition_work.get('title', '') + (': ' + edition_work.subtitle if edition_work.get('subtitle') else '')
+ else:
+ full_work_title = doc.get('title', '') + (': ' + doc.subtitle if doc.get('subtitle') else '')
+
+$ blur_cover = "fsi__bookcover-img-blur" if blur else ""
+
+
+
+
+ $ cover = get_cover_url(selected_ed) or "/images/icons/avatar_book-sm.png"
+
+
+
+
+
+
+
+
+ $ authors = None
+ $if doc_type == 'infogami_work':
+ $ authors = doc.get_authors()
+ $elif doc_type == 'infogami_edition':
+ $ authors = edition_work.get_authors() if edition_work else doc.get_authors()
+ $elif doc_type.startswith('solr_'):
+ $if 'authors' in doc:
+ $ authors = doc['authors']
+ $elif 'author_key' in doc:
+ $ authors = [ { 'key': '/authors/' + key, 'name': name } for key, name in zip(doc['author_key'], doc['author_name']) ]
+ $if not authors:
+ $_('Unknown author')
+ $else:
+ $code:
+ author_data = [
+ {
+ 'name': a.get('name') or a.get('author', {}).get('name'),
+ 'url': (a.get('url') or a.get('key') or a.get('author', {}).get('url') or a.get('author', {}).get('key'))
+ }
+ for a in authors
+ ]
+ $:macros.BookByline(author_data, limit=max_rendered_authors, overflow_url=work_edition_url, attrs='class="results"')
+
+
+ $if snippet:
+ $:snippet
+
+
+
+
diff --git a/openlibrary/macros/FulltextSuggestionSnippet.html b/openlibrary/macros/FulltextSuggestionSnippet.html
new file mode 100644
index 00000000000..3cad6607185
--- /dev/null
+++ b/openlibrary/macros/FulltextSuggestionSnippet.html
@@ -0,0 +1,26 @@
+$def with (q, doc=None)
+
+$ ia = doc.get('fields', {}).get('identifier', [''])[0]
+$ ia_base_url = "https://archive.org"
+$ availability = doc.get('availability', {})
+$ snippet = doc.get('highlight', {}).get('text', [''])[0]
+$ page_nums = doc.get('fields', {}).get('page_num', [])
+$if len(page_nums) == 1 and isinstance(page_nums[0], list):
+ $ page_nums = page_nums[0]
+$ page = ', '.join(str(num) for num in page_nums)
+
+$if snippet:
+
diff --git a/openlibrary/plugins/openlibrary/code.py b/openlibrary/plugins/openlibrary/code.py
index 2ef6cfdb6c1..8ece430c8cd 100644
--- a/openlibrary/plugins/openlibrary/code.py
+++ b/openlibrary/plugins/openlibrary/code.py
@@ -41,6 +41,7 @@
from openlibrary.utils.isbn import isbn_13_to_isbn_10, isbn_10_to_isbn_13, canonical
from openlibrary.core.models import Edition
from openlibrary.core.lending import get_availability
+from openlibrary.core.fulltext import fulltext_search
import openlibrary.core.stats
from openlibrary.plugins.openlibrary.home import format_work_data
from openlibrary.plugins.openlibrary.stats import increment_error_count
@@ -1114,6 +1115,7 @@ def GET(self):
args[0], args[1]
)
partial = {"partials": str(macro)}
+
elif component == 'SearchFacets':
data = json.loads(i.data)
path = data.get('path')
@@ -1150,6 +1152,18 @@ def GET(self):
"activeFacets": str(active_facets).strip(),
}
+ elif component == "FulltextSearchSuggestion":
+ query = i.get('data', '')
+ data = fulltext_search(query)
+ hits = data.get('hits', [])
+ if not hits['hits']:
+ macro = ''
+ else:
+ macro = web.template.Template.globals[
+ 'macros'
+ ].FulltextSearchSuggestion(query, data)
+ partial = {"partials": str(macro)}
+
return delegate.RawText(json.dumps(partial))
diff --git a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js
new file mode 100644
index 00000000000..96e7980d91c
--- /dev/null
+++ b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js
@@ -0,0 +1,61 @@
+export function initFulltextSearchSuggestion(fulltextSearchSuggestion) {
+ const isLoading = showLoadingIndicators(fulltextSearchSuggestion)
+ if (isLoading) {
+ const query = fulltextSearchSuggestion.dataset.query
+ getPartials(fulltextSearchSuggestion, query)
+ }
+}
+
+function showLoadingIndicators(fulltextSearchSuggestion) {
+ let isLoading = false
+ const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator')
+ if (loadingIndicator) {
+ isLoading = true
+ loadingIndicator.classList.remove('hidden')
+ }
+ return isLoading
+}
+async function getPartials(fulltextSearchSuggestion, query) {
+ const queryParam = encodeURIComponent(query)
+ return fetch(`/partials.json?_component=FulltextSearchSuggestion&data=${queryParam}`)
+ .then((resp) => {
+ if (resp.status !== 200) {
+ throw new Error(`Failed to fetch partials. Status code: ${resp.status}`)
+ }
+ return resp.json()
+ })
+ .then((data) => {
+ fulltextSearchSuggestion.innerHTML += data['partials']
+ const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator');
+ if (loadingIndicator) {
+ loadingIndicator.classList.add('hidden')
+ }
+ })
+ .catch(() => {
+ const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator')
+ if (loadingIndicator) {
+ loadingIndicator.classList.add('hidden')
+ }
+ const existingRetryAffordance = fulltextSearchSuggestion.querySelector('.fulltext-suggestions__retry')
+ if (existingRetryAffordance) {
+ existingRetryAffordance.classList.remove('hidden')
+ } else {
+ fulltextSearchSuggestion.insertAdjacentHTML('afterbegin', renderRetryLink())
+ const retryAffordance = fulltextSearchSuggestion.querySelector('.fulltext-suggestions__retry')
+ retryAffordance.addEventListener('click', () => {
+ retryAffordance.classList.add('hidden')
+ getPartials(fulltextSearchSuggestion, query)
+ })
+ }
+
+ })
+}
+
+/**
+ * Returns HTML string with error message and retry link.
+ *
+ * @returns {string} HTML for a retry link.
+ */
+function renderRetryLink() {
+ return 'Failed to fetch fulltext search suggestions. Retry?'
+}
diff --git a/openlibrary/plugins/openlibrary/js/index.js b/openlibrary/plugins/openlibrary/js/index.js
index d3412d36181..a0aed4020e8 100644
--- a/openlibrary/plugins/openlibrary/js/index.js
+++ b/openlibrary/plugins/openlibrary/js/index.js
@@ -555,4 +555,12 @@ jQuery(function () {
import(/* webpackChunkName: "affiliate-links" */ './affiliate-links')
.then(module => module.initAffiliateLinks(affiliateLinksSection))
}
+
+ // Fulltext search box:
+ const fulltextSearchSuggestion = document.querySelector('#fulltext-search-suggestion')
+ if (fulltextSearchSuggestion) {
+ import(/* webpackChunkName: "fulltext-search-suggestion" */ './fulltext-search-suggestion')
+ .then(module => module.initFulltextSearchSuggestion(fulltextSearchSuggestion))
+ }
});
+
diff --git a/openlibrary/templates/search/authors.html b/openlibrary/templates/search/authors.html
index 82871bcf79e..448efdbe4b3 100644
--- a/openlibrary/templates/search/authors.html
+++ b/openlibrary/templates/search/authors.html
@@ -48,7 +48,13 @@ $_("Search Authors")
$else:
- $_('No hits')
+
+ $:_('No authors directly matched your search')
+
+
+
+ $:macros.LoadingIndicator(_("Checking Search Inside matches"))
+
$for doc in results.docs:
diff --git a/openlibrary/templates/search/inside.html b/openlibrary/templates/search/inside.html
index a3861c0a647..4501ac525ab 100644
--- a/openlibrary/templates/search/inside.html
+++ b/openlibrary/templates/search/inside.html
@@ -24,7 +24,10 @@ $_("Search Inside")
$if not num_found:
$def escaped_query(): $q
- $:_('No hits for: %(query)s', query=escaped_query())
+
+ $:_('No Search Inside text matched your search')
+
+
$else:
$ungettext('About %(count)s result found', 'About %(count)s results found', num_found, count=commify(num_found)) $ungettext('in %(seconds)s second', 'in %(seconds)s seconds', search_time, seconds="%.2f" % search_time)
diff --git a/openlibrary/templates/search/lists.html b/openlibrary/templates/search/lists.html
index 73d0bf99b25..7ccc296a83b 100644
--- a/openlibrary/templates/search/lists.html
+++ b/openlibrary/templates/search/lists.html
@@ -22,9 +22,11 @@ $_("Search Lists")
$elif q:
-
diff --git a/openlibrary/templates/search/subjects.html b/openlibrary/templates/search/subjects.html
index 2928552c0c0..220c3fab657 100644
--- a/openlibrary/templates/search/subjects.html
+++ b/openlibrary/templates/search/subjects.html
@@ -27,7 +27,16 @@
$if q:
$ response = get_results(q, offset=offset, limit=results_per_page)
$if not response.error:
-
$ungettext('%(count)s hit', '%(count)s hits', response.num_found, count=commify(response.num_found))
+ $if response.num_found:
+ $ungettext('%(count)s hit', '%(count)s hits', response.num_found, count=commify(response.num_found))
+ $else:
+
+ $:_('No subjects directly matched your search')
+
+
+
+ $:macros.LoadingIndicator(_("Checking Search Inside matches"))
+
$if q and response.error:
diff --git a/openlibrary/templates/work_search.html b/openlibrary/templates/work_search.html
index c3d09b8ad6f..076f0c2a5e6 100644
--- a/openlibrary/templates/work_search.html
+++ b/openlibrary/templates/work_search.html
@@ -48,21 +48,21 @@ $_("Search Books")
$if param and not search_response.docs:
+ $if ctx.path == '/search':
+ $ path_id = 'books'
+ $else:
+ $ path_id = ctx.path.split('/')
$ query = query_param('q')
$# Temporarily (2020-03-26) disable automatically showing full-text
$# results because of performance issues due to increased load. See
$# this commit to revert.
- $_("No results found.")
+ $:_('No %(path_id)s directly matched your search', path_id=path_id)
-
-
-
+
+ $:macros.LoadingIndicator(_("Checking Search Inside matches"))
+
$elif param and not search_response.error and len(search_response.docs):
diff --git a/static/css/components/fulltext-search-suggestion-item.less b/static/css/components/fulltext-search-suggestion-item.less
new file mode 100644
index 00000000000..112a3e68f1f
--- /dev/null
+++ b/static/css/components/fulltext-search-suggestion-item.less
@@ -0,0 +1,96 @@
+.fsi {
+ &__main {
+ .display-flex();
+ flex-direction: row;
+ background-color: @white;
+ padding: 5px;
+ margin-bottom: 5px;
+ border-bottom: 1px solid @light-beige;
+ border-radius: 3px;
+ overflow: hidden;
+ @media (min-width: @width-breakpoint-tablet) {
+ flex-direction: row;
+ }
+ }
+
+ &__book-cover {
+ overflow: hidden;
+ height: 100%;
+ width: auto;
+ min-width: 90px;
+ border-radius: 4px;
+ align-self: center;
+ margin-top: 5px;
+ @media (min-width: @width-breakpoint-tablet) {
+ height: 130px;
+ margin-bottom: 5px;
+ }
+ }
+
+ &__book-cover-img {
+ height: 100%;
+ width: 100%;
+ object-fit: cover;
+ }
+
+ &__book-cover-img-blur {
+ filter: blur(4px);
+ }
+
+ &__book-details {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin: 5px 10px 0;
+ @media (min-width: @width-breakpoint-tablet) {
+ align-items: flex-start;
+ }
+ }
+
+ &__title-author {
+ display: flex;
+ flex-direction: column;
+ padding: 5px 3px 0 10px;
+ width: 50%;
+ @media (min-width: @width-breakpoint-mobile-s) {
+ width: 70%
+ }
+ @media (min-width: @width-breakpoint-mobile-m) {
+ width: 80%
+ }
+ }
+
+ &__book-title,
+ &__book-author {
+ width: 80%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-family: @lucida_sans_serif-6;
+ }
+
+ &__book-title {
+ font-size: large;
+ margin: 0;
+ display: block;
+ padding: 0;
+ font-weight: 700;
+ line-height: 1.2em;
+ }
+
+ &__book-title a:link,
+ &__book-title a:visited {
+ text-decoration: none;
+ color: @black;
+ }
+
+ &__book-title a:hover {
+ color: @header-nav-hover-color;
+ }
+
+ &__book-author {
+ line-height: 1.2em;
+ margin: 3px 0 0;
+ font-size: medium;
+ }
+}
diff --git a/static/css/components/fulltext-search-suggestion.less b/static/css/components/fulltext-search-suggestion.less
new file mode 100644
index 00000000000..168b5e06d7e
--- /dev/null
+++ b/static/css/components/fulltext-search-suggestion.less
@@ -0,0 +1,83 @@
+/**
+ * Search Result Item
+ * https://github.com/internetarchive/openlibrary/wiki/Design-Pattern-Library#searchresultitem
+ */
+
+.fulltext-suggestions{
+ list-style-type: none;
+ line-height: 1.5em;
+ background-color: @grey-fafafa;
+ border-radius: 5px;
+ padding: 5px;
+ margin-bottom: 3px;
+ border-bottom: 1px solid @light-beige;
+ .display-flex();
+ flex-direction: column;
+
+ &__header {
+ display: flex;
+ flex-direction: row;
+ align-items: start;
+ padding: 5px;
+ margin-bottom: 3px;
+ }
+
+ &__icon {
+ margin-top: 5px;
+ min-height: 30px;
+ min-width: 30px;
+ @media (min-width: @width-breakpoint-tablet) {
+ align-items: center;
+ }
+ }
+
+ &__title {
+ display: block;
+ margin: 0;
+ padding: 5px;
+ font-size: medium;
+ font-weight: 700;
+ font-family: @lucida_sans_serif-6;
+ @media (min-width: @width-breakpoint-tablet) {
+ font-size: large;
+ }
+ a:link,
+ a:visited {
+ color: @black;
+ text-decoration: none;
+ }
+ a:hover {
+ color: @link-blue;
+ }
+ }
+
+ &__footer {
+ display: flex;
+ justify-content: center;
+ margin: 0;
+ padding: 5px;
+ font-size: small;
+ font-weight: 700;
+ font-family: @lucida_sans_serif-6;
+ @media (min-width: @width-breakpoint-mobile-m) {
+ font-size: medium;
+ }
+ @media (min-width: @width-breakpoint-tablet) {
+ font-size: large;
+ justify-content: end;
+ }
+ }
+
+ &__right-chevron {
+ max-height: 20px;
+ max-width: 20px;
+ display: none;
+ @media (min-width: @width-breakpoint-mobile-m) {
+ display: inline;
+ }
+ }
+
+ &__retry {
+ text-align: center;
+ }
+}
diff --git a/static/css/components/fulltext-suggestion-snippet.less b/static/css/components/fulltext-suggestion-snippet.less
new file mode 100644
index 00000000000..7e8d12be771
--- /dev/null
+++ b/static/css/components/fulltext-suggestion-snippet.less
@@ -0,0 +1,47 @@
+.fsi-snippet {
+ display: inline;
+ width: 60%;
+ padding: 5px;
+ margin: 0;
+ font-size: small;
+ @media (min-width: @width-breakpoint-mobile-m) {
+ width: 90%;
+ }
+ @media (min-width: @width-breakpoint-tablet) {
+ width: auto;
+ font-size: medium;
+ }
+
+ &__main {
+ padding: 5px;
+ margin: 0 5px;
+ line-height: 1.6em;
+
+ &__page-num a {
+ color: @dark-grey;
+ display: inline;
+ text-decoration: none;
+ font-family: @lucida_sans_serif-1;
+ }
+
+ a:link,
+ a:hover,
+ a:visited {
+ text-decoration: none;
+ }
+
+ a:link {
+ font-family: @georgia_serif-1;
+ color: @black;
+ }
+
+ a:hover {
+ color: @link-blue;
+ }
+ }
+
+ &__quotation-mark {
+ font-size: larger;
+ display: inline;
+ }
+}
diff --git a/static/css/less/breakpoints.less b/static/css/less/breakpoints.less
index 7601e3292f7..8dea0230664 100644
--- a/static/css/less/breakpoints.less
+++ b/static/css/less/breakpoints.less
@@ -1,3 +1,5 @@
+@width-breakpoint-mobile-s: 375px;
+@width-breakpoint-mobile-m: 425px;
@width-breakpoint-mobile: 450px;
@width-breakpoint-tablet: 768px; // 48em
@width-breakpoint-desktop: 960px; // 60em
diff --git a/static/css/page-user.less b/static/css/page-user.less
index 8ff38472ce5..10d6154968e 100644
--- a/static/css/page-user.less
+++ b/static/css/page-user.less
@@ -224,6 +224,12 @@ tr.table-row.selected{
transform: translateY(-50%);
}
+// Import styles for fulltext-search-suggestion card
+@import (less) "components/fulltext-search-suggestion.less";
+// Import styles for fulltext-search-suggestion card item
+@import (less) "components/fulltext-search-suggestion-item.less";
+// Import styles for fulltext-search-suggestion card item snippet
+@import (less) "components/fulltext-suggestion-snippet.less";
// Import styles for author infobox
@import (less) "components/author-infobox.less";
// Import all common components
diff --git a/static/images/icons/icon_search-inside.svg b/static/images/icons/icon_search-inside.svg
new file mode 100644
index 00000000000..7982c49cd98
--- /dev/null
+++ b/static/images/icons/icon_search-inside.svg
@@ -0,0 +1 @@
+
\ No newline at end of file