[docs] Autocomplete styling tweaks (#2732)
This PR tweaks the styling in the autocomplete. It removes icons and matches styles to the sidebar. It improves the mobile design to hide the search bar on mobile. ### Change Type - [ ] `patch` — Bug fix - [ ] `minor` — New feature - [ ] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [x] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know
This commit is contained in:
parent
e6e4e7f6cb
commit
538734782c
13 changed files with 81819 additions and 81820 deletions
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,8 +1,8 @@
|
|||
import { SearchResult } from '@/types/search-types'
|
||||
import { getDb } from '@/utils/ContentDatabase'
|
||||
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
|
||||
import assert from 'assert'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
|
||||
|
||||
type Data = {
|
||||
results: {
|
||||
|
@ -42,36 +42,44 @@ export async function GET(req: NextRequest) {
|
|||
const queryResults = await vdb.query(query, 25)
|
||||
queryResults.sort((a, b) => b.score - a.score)
|
||||
|
||||
const headings = await Promise.all(
|
||||
queryResults.map(async (result) => {
|
||||
if (result.type !== 'heading') return // bleg
|
||||
const headings = (
|
||||
await Promise.all(
|
||||
queryResults.map(async (result) => {
|
||||
try {
|
||||
if (result.type !== 'heading') return // bleg
|
||||
|
||||
const article = await db.db.get(
|
||||
`SELECT id, title, description, categoryId, sectionId, keywords FROM articles WHERE id = ?`,
|
||||
result.id
|
||||
)
|
||||
assert(article, `No article found for heading ${result.id}`)
|
||||
const category = await db.db.get(
|
||||
`SELECT id, title FROM categories WHERE id = ?`,
|
||||
article.categoryId
|
||||
)
|
||||
const section = await db.db.get(
|
||||
`SELECT id, title FROM sections WHERE id = ?`,
|
||||
article.sectionId
|
||||
)
|
||||
const heading = await db.db.get(`SELECT * FROM headings WHERE slug = ?`, result.slug)
|
||||
assert(heading, `No heading found for ${result.id} ${result.slug}`)
|
||||
const article = await db.db.get(
|
||||
`SELECT id, title, description, categoryId, sectionId, keywords FROM articles WHERE id = ?`,
|
||||
result.id
|
||||
)
|
||||
assert(article, `No article found for heading ${result.id}`)
|
||||
const category = await db.db.get(
|
||||
`SELECT id, title FROM categories WHERE id = ?`,
|
||||
article.categoryId
|
||||
)
|
||||
const section = await db.db.get(
|
||||
`SELECT id, title FROM sections WHERE id = ?`,
|
||||
article.sectionId
|
||||
)
|
||||
const heading = await db.db.get(`SELECT * FROM headings WHERE slug = ?`, result.slug)
|
||||
assert(heading, `No heading found for ${result.id} ${result.slug}`)
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
article,
|
||||
category,
|
||||
section,
|
||||
heading,
|
||||
score: result.score,
|
||||
}
|
||||
})
|
||||
)
|
||||
return {
|
||||
id: result.id,
|
||||
article,
|
||||
category,
|
||||
section,
|
||||
heading,
|
||||
score: result.score,
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e.message)
|
||||
// something went wrong
|
||||
return
|
||||
}
|
||||
})
|
||||
)
|
||||
).filter(Boolean)
|
||||
|
||||
const visited = new Set<string>()
|
||||
for (const result of headings) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { SearchResult } from '@/types/search-types'
|
||||
import { getDb } from '@/utils/ContentDatabase'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
type Data = {
|
||||
results: {
|
||||
|
|
|
@ -47,11 +47,11 @@
|
|||
}
|
||||
|
||||
.autocomplete__cancel {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.autocomplete__wrapper:focus-within .autocomplete__cancel {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.autocomplete__cancel {
|
||||
|
@ -73,16 +73,25 @@
|
|||
}
|
||||
|
||||
.autocomplete__item__icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex: 0 0 24px;
|
||||
}
|
||||
|
||||
.autocomplete__group {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
padding-left: 8px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
position: relative;
|
||||
letter-spacing: 0.5px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
--bg: transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
@ -110,16 +119,12 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.autocomplete__item {
|
||||
height: 2.25rem;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete__popover {
|
||||
margin-top: 4px;
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
min-width: 240px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
max-height: min(var(--popover-available-height, 300px), 300px);
|
||||
flex-direction: column;
|
||||
|
@ -135,39 +140,60 @@
|
|||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgb(0 0 0 / 0.1),
|
||||
0 10px 14px -3px rgb(0 0 0 / 0.1),
|
||||
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
:is([data-theme="dark"] .autocomplete__popover) {
|
||||
:is([data-theme='dark'] .autocomplete__popover) {
|
||||
border-color: hsl(204 3% 26%);
|
||||
background-color: hsl(204 3% 18%);
|
||||
color: hsl(204 20% 100%);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgb(0 0 0 / 0.25),
|
||||
0 10px 14px -3px rgb(0 0 0 / 0.25),
|
||||
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.autocomplete__empty {
|
||||
padding: 0px 8px;
|
||||
}
|
||||
|
||||
.autocomplete__item {
|
||||
position: relative;
|
||||
padding: 0px 8px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
scroll-margin: 0.5rem;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
outline: none !important;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
justify-content: flex-start;
|
||||
color: var(--color-text);
|
||||
--bg: transparent;
|
||||
height: 32px;
|
||||
flex-grow: 2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.autocomplete__item:hover {
|
||||
background-color: hsl(204 100% 80% / 0.4);
|
||||
.autocomplete__item::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
inset: 1px 0px;
|
||||
background-color: var(--bg);
|
||||
border-radius: var(--border-radius-menu);
|
||||
transition: background-color 0.12s ease-in-out;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
.autocomplete__item[data-active-item] {
|
||||
background-color: hsl(204 100% 40%);
|
||||
color: hsl(204 20% 100%);
|
||||
.autocomplete__item[data-active-item]::after {
|
||||
background-color: var(--color-tint-2);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.autocomplete__item:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.autocomplete__item:hover::after {
|
||||
--bg: var(--color-tint-1);
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete__item:active,
|
||||
|
@ -176,18 +202,18 @@
|
|||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
:is([data-theme="dark"] .autocomplete__item) {
|
||||
:is([data-theme='dark'] .autocomplete__item) {
|
||||
color: hsl(204 20% 100%);
|
||||
}
|
||||
|
||||
:is([data-theme="dark"] .autocomplete__item__icon path) {
|
||||
:is([data-theme='dark'] .autocomplete__item__icon path) {
|
||||
fill: hsl(204 20% 100%);
|
||||
}
|
||||
|
||||
:is([data-theme="dark"] .autocomplete__item:hover) {
|
||||
:is([data-theme='dark'] .autocomplete__item:hover) {
|
||||
background-color: hsl(204 100% 40% / 0.25);
|
||||
}
|
||||
|
||||
:is([data-theme="dark"] .autocomplete__item)[data-active-item] {
|
||||
:is([data-theme='dark'] .autocomplete__item)[data-active-item] {
|
||||
background-color: hsl(204 100% 40%);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
import { ComponentType, ForwardedRef, forwardRef, startTransition, useState } from 'react'
|
||||
import './Autocomplete.css'
|
||||
import { Icon } from './Icon'
|
||||
import Spinner from './Spinner'
|
||||
import { Spinner } from './Spinner'
|
||||
|
||||
export type DropdownOption = {
|
||||
label: string
|
||||
|
@ -59,10 +59,10 @@ const Autocomplete = forwardRef(function Autocomplete(
|
|||
if (filteredOptions.length === 0) return null
|
||||
|
||||
return (
|
||||
<ComboboxGroup>
|
||||
<ComboboxGroup key={group}>
|
||||
{groupsToLabel?.[group] && (
|
||||
<ComboboxGroupLabel className="autocomplete__group">
|
||||
{groupsToLabel?.[group]}
|
||||
<ComboboxGroupLabel key={`${group}-group`} className="autocomplete__group">
|
||||
{groupsToLabel[group]}
|
||||
</ComboboxGroupLabel>
|
||||
)}
|
||||
{filteredOptions.map(({ label, value }) => {
|
||||
|
@ -111,7 +111,7 @@ const Autocomplete = forwardRef(function Autocomplete(
|
|||
{value && (
|
||||
<ComboboxPopover className="autocomplete__popover">
|
||||
{customUI}
|
||||
{options.length === 0 && <span>No results found.</span>}
|
||||
{options.length === 0 && <span className="autocomplete__empty">No results found.</span>}
|
||||
{options.length !== 0 && renderedGroups}
|
||||
</ComboboxPopover>
|
||||
)}
|
||||
|
@ -120,4 +120,4 @@ const Autocomplete = forwardRef(function Autocomplete(
|
|||
)
|
||||
})
|
||||
|
||||
export default Autocomplete
|
||||
export { Autocomplete }
|
||||
|
|
|
@ -5,7 +5,7 @@ import { debounce } from '@/utils/debounce'
|
|||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import Autocomplete, { DropdownOption } from './Autocomplete'
|
||||
import { Autocomplete, DropdownOption } from './Autocomplete'
|
||||
|
||||
const HOST_URL =
|
||||
process.env.NODE_ENV === 'development'
|
||||
|
@ -67,6 +67,7 @@ export function Search() {
|
|||
|
||||
const handleSearchTypeChange = () => {
|
||||
setSearchType(searchType === SEARCH_TYPE.AI ? SEARCH_TYPE.NORMAL : SEARCH_TYPE.AI)
|
||||
handleInputChange(query)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -95,7 +96,6 @@ export function Search() {
|
|||
}
|
||||
groups={['examples', 'docs', 'reference']}
|
||||
groupsToLabel={{ examples: 'Examples', docs: 'Articles', reference: 'Reference' }}
|
||||
groupsToIcon={{ examples: CodeIcon, docs: DocIcon, reference: ReferenceIcon }}
|
||||
options={searchResults}
|
||||
isLoading={isLoading}
|
||||
onInputChange={handleInputChange}
|
||||
|
@ -111,52 +111,3 @@ export function Search() {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/*!
|
||||
* Author: Dazzle UI
|
||||
* License: https://www.svgrepo.com/page/licensing/#CC%20Attribution
|
||||
*/
|
||||
const CodeIcon = ({ className }: { className?: string }) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9.95305 16.9123L8.59366 18.3608L2.03125 12.2016L8.19037 5.63922L9.64868 7.00791L4.85826 12.112L9.95254 16.8932L9.95305 16.9123Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
<path
|
||||
d="M14.0478 16.9123L15.4072 18.3608L21.9696 12.2016L15.8105 5.63922L14.3522 7.00791L19.1426 12.112L14.0483 16.8932L14.0478 16.9123Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
/*!
|
||||
* Author: Solar Icons
|
||||
* License: https://www.svgrepo.com/page/licensing/#CC%20Attribution
|
||||
*/
|
||||
const DocIcon = ({ className }: { className?: string }) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3 10C3 6.22876 3 4.34315 4.17157 3.17157C5.34315 2 7.22876 2 11 2H13C16.7712 2 18.6569 2 19.8284 3.17157C21 4.34315 21 6.22876 21 10V14C21 17.7712 21 19.6569 19.8284 20.8284C18.6569 22 16.7712 22 13 22H11C7.22876 22 5.34315 22 4.17157 20.8284C3 19.6569 3 17.7712 3 14V10Z"
|
||||
stroke="#000"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path d="M8 12H16" stroke="#000" stroke-width="1.5" stroke-linecap="round" />
|
||||
<path d="M8 8H16" stroke="#000" stroke-width="1.5" stroke-linecap="round" />
|
||||
<path d="M8 16H13" stroke="#000" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
/*!
|
||||
* Author: Konstantin Filatov
|
||||
* License: https://www.svgrepo.com/page/licensing/#CC%20Attribution
|
||||
*/
|
||||
const ReferenceIcon = ({ className }: { className?: string }) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M19 23H7C4.27504 23 2 20.7055 2 18V6C2 3.23858 4.23858 1 7 1H19C20.6569 1 22 2.34315 22 4V20C22 21.6569 20.6569 23 19 23ZM7 3C5.34315 3 4 4.34315 4 6V14.9996C4.83566 14.3719 5.87439 14 7 14H19C19.3506 14 19.6872 14.0602 20 14.1707V4C20 3.44772 19.5523 3 19 3H18V9C18 9.3688 17.797 9.70765 17.4719 9.88167C17.1467 10.0557 16.7522 10.0366 16.4453 9.83205L14 8.20185L11.5547 9.83205C11.2478 10.0366 10.8533 10.0557 10.5281 9.88167C10.203 9.70765 10 9.3688 10 9V3H7ZM12 3H16V7.13148L14.5547 6.16795C14.2188 5.94402 13.7812 5.94402 13.4453 6.16795L12 7.13148V3ZM19 16C19.5523 16 20 16.4477 20 17V20C20 20.5523 19.5523 21 19 21H7C5.5135 21 4.04148 19.9162 4.04148 18.5C4.04148 17.0532 5.5135 16 7 16H19Z"
|
||||
fill="#000"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
|
|
@ -136,20 +136,15 @@ function SidebarCategory({
|
|||
)
|
||||
if (articles.length === 0) return null
|
||||
|
||||
const value = `${linkCtx?.categoryId}-${group}-${linkCtx?.articleId}`
|
||||
return (
|
||||
<Accordion.Item
|
||||
key={group}
|
||||
value={`${linkCtx?.categoryId}-${group}-${linkCtx?.articleId}`}
|
||||
>
|
||||
<Accordion.Trigger
|
||||
className="sidebar__section__group__title"
|
||||
style={{ marginLeft: '8px', paddingRight: '8px' }}
|
||||
>
|
||||
<Accordion.Item key={value} value={value}>
|
||||
<Accordion.Trigger className="sidebar__section__group__title">
|
||||
{group}
|
||||
<Chevron />
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
<ul className="sidebar__list sidebar__group" style={{ paddingLeft: '8px' }}>
|
||||
<ul className="sidebar__list sidebar__group">
|
||||
{articles.map((link) => (
|
||||
<SidebarLink key={link.url} headings={headings} {...link} />
|
||||
))}
|
||||
|
@ -202,7 +197,7 @@ function SidebarArticle({
|
|||
?.filter((heading) => heading.level < 4)
|
||||
.map((heading) => (
|
||||
<li
|
||||
key={heading.slug}
|
||||
key={`${heading.slug}`}
|
||||
data-heading-level={heading.title === 'Constructor' ? 2 : heading.level}
|
||||
>
|
||||
<Link href={`#${heading.slug}`} title={heading.title} className="sidebar__link">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// TODO(mime): copied from tldraw package, needs to be in another shared location.
|
||||
export default function Spinner(props: React.SVGProps<SVGSVGElement>) {
|
||||
export function Spinner(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width={16} height={16} viewBox="0 0 16 16" {...props}>
|
||||
<g strokeWidth={2} fill="none" fillRule="evenodd">
|
||||
|
|
|
@ -774,9 +774,15 @@ body {
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sidebar,
|
||||
.sidebar button,
|
||||
.sidebar a {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar a,
|
||||
.sidebar .sidebar__link {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@ -813,7 +819,6 @@ body {
|
|||
.sidebar__list li .sidebar__category li a,
|
||||
.sidebar__list li .sidebar__article li a {
|
||||
padding-left: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sidebar__list li .sidebar__category li[data-heading-level='3'] a,
|
||||
|
@ -821,6 +826,10 @@ body {
|
|||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.sidebar__group {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.sidebar__list li .sidebar__category .sidebar__group li[data-heading-level='2'] a,
|
||||
.sidebar__list li .sidebar__article .sidebar__group li[data-heading-level='2'] a {
|
||||
padding-left: 16px;
|
||||
|
@ -892,7 +901,6 @@ body {
|
|||
}
|
||||
|
||||
.sidebar__section__group__title {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin-bottom: 4px;
|
||||
position: relative;
|
||||
|
@ -906,6 +914,8 @@ body {
|
|||
border: 0;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
padding-right: 8px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.sidebar__sections__list > *:nth-last-of-type(n + 2) > .sidebar__list {
|
||||
|
@ -955,10 +965,6 @@ body {
|
|||
box-shadow: var(--shadow-small);
|
||||
}
|
||||
|
||||
.sidebar__category__title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* --------------------- Search --------------------- */
|
||||
|
||||
.sidebar .search__wrapper {
|
||||
|
@ -1007,6 +1013,7 @@ body {
|
|||
cursor: pointer;
|
||||
margin: 4px 0;
|
||||
background: transparent;
|
||||
padding: 0px 8px 8px 8px;
|
||||
}
|
||||
|
||||
/* --------------------- Desktop M --------------------- */
|
||||
|
@ -1077,6 +1084,15 @@ body {
|
|||
pointer-events: all;
|
||||
}
|
||||
|
||||
.search__wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.layout__header {
|
||||
grid-template-columns: auto auto;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: none;
|
||||
width: 100%;
|
||||
|
|
|
@ -125,7 +125,10 @@ const MATCH_HEADINGS = /(?:^|\n)(#{1,6})\s+(.+?)(?=\n|$)/g
|
|||
function getHeadingLinks(content: string) {
|
||||
let match
|
||||
const headings: ArticleHeadings = []
|
||||
const visited = new Set<string>()
|
||||
while ((match = MATCH_HEADINGS.exec(content)) !== null) {
|
||||
if (visited.has(match[2])) continue
|
||||
visited.add(match[2])
|
||||
slugs.reset()
|
||||
headings.push({
|
||||
level: match[1].length,
|
||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue