docs: rework search UI (#2723)

Reworks search to not be a page and instead to be inline dropdown.

<img width="763" alt="Screenshot 2024-02-05 at 13 22 58"
src="https://github.com/tldraw/tldraw/assets/469604/4e5a8076-62cd-44bb-b8e7-7f5ecdc4af24">


- rework search completely
- rm Search Results css
- uses Ariakit and add appropriate hooks / styling
- I couldn't use Radix unfortunately since they're still working on
adding a Combox: https://github.com/radix-ui/primitives/issues/1342
- I'm open to other suggestions but Ariakit plays nicely with Radix and
keeps things open to migrate to Radix in the future
- fixes bug with not scrolling to right place when having a direct link
- adds categories in the search results - examples / reference / learn
- and adds category icons. Let me know if there's a better policy for
adding new SVG icons cc @steveruizok

### Change Type

- [x] `minor` — New feature

### Test Plan

1. Test searches using normal method for each type (examples, docs,
refs)
2. Test searches using AI for each type (ditto)

### Release Notes

- Docs: rework the search to be an inline dropdown.
This commit is contained in:
Mime Čuvalo 2024-02-05 14:32:50 +00:00 committed by GitHub
parent 591d3129c7
commit 157d24db73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 11965 additions and 11696 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -2,11 +2,13 @@ import { SearchResult } from '@/types/search-types'
import { getDb } from '@/utils/ContentDatabase' import { getDb } from '@/utils/ContentDatabase'
import assert from 'assert' import assert from 'assert'
import { NextRequest } from 'next/server' import { NextRequest } from 'next/server'
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
type Data = { type Data = {
results: { results: {
articles: SearchResult[] articles: SearchResult[]
apiDocs: SearchResult[] apiDocs: SearchResult[]
examples: SearchResult[]
} }
status: 'success' | 'error' | 'no-query' status: 'success' | 'error' | 'no-query'
} }
@ -16,13 +18,11 @@ const BANNED_HEADINGS = ['new', 'constructor', 'properties', 'example', 'methods
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url) const { searchParams } = new URL(req.url)
const query = searchParams.get('q')?.toLowerCase() const query = searchParams.get('q')?.toLowerCase()
if (!query) { if (!query) {
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
results: { results: structuredClone(SEARCH_RESULTS),
articles: [],
apiDocs: [],
},
status: 'error', status: 'error',
error: 'No query', error: 'No query',
}), }),
@ -33,10 +33,7 @@ export async function GET(req: NextRequest) {
} }
try { try {
const results: Data['results'] = { const results: Data['results'] = structuredClone(SEARCH_RESULTS)
articles: [],
apiDocs: [],
}
const db = await getDb() const db = await getDb()
const getVectorDb = (await import('@/utils/ContentVectorDatabase')).getVectorDb const getVectorDb = (await import('@/utils/ContentVectorDatabase')).getVectorDb
@ -44,9 +41,11 @@ export async function GET(req: NextRequest) {
const vdb = await getVectorDb() const vdb = await getVectorDb()
const queryResults = await vdb.query(query, 25) const queryResults = await vdb.query(query, 25)
queryResults.sort((a, b) => b.score - a.score) queryResults.sort((a, b) => b.score - a.score)
const headings = await Promise.all( const headings = await Promise.all(
queryResults.map(async (result) => { queryResults.map(async (result) => {
if (result.type !== 'heading') return // bleg if (result.type !== 'heading') return // bleg
const article = await db.db.get( const article = await db.db.get(
`SELECT id, title, description, categoryId, sectionId, keywords FROM articles WHERE id = ?`, `SELECT id, title, description, categoryId, sectionId, keywords FROM articles WHERE id = ?`,
result.id result.id
@ -62,6 +61,7 @@ export async function GET(req: NextRequest) {
) )
const heading = await db.db.get(`SELECT * FROM headings WHERE slug = ?`, result.slug) const heading = await db.db.get(`SELECT * FROM headings WHERE slug = ?`, result.slug)
assert(heading, `No heading found for ${result.id} ${result.slug}`) assert(heading, `No heading found for ${result.id} ${result.slug}`)
return { return {
id: result.id, id: result.id,
article, article,
@ -72,18 +72,23 @@ export async function GET(req: NextRequest) {
} }
}) })
) )
const visited = new Set<string>() const visited = new Set<string>()
for (const result of headings) { for (const result of headings) {
if (!result) continue if (!result) continue
if (visited.has(result.id)) continue if (visited.has(result.id)) continue
visited.add(result.id) visited.add(result.id)
const { category, section, article, heading, score } = result const { category, section, article, heading, score } = result
const isUncategorized = category.id === section.id + '_ucg' const isUncategorized = category.id === section.id + '_ucg'
if (BANNED_HEADINGS.some((h) => heading.slug.endsWith(h))) continue if (BANNED_HEADINGS.some((h) => heading.slug.endsWith(h))) continue
results[section.id === 'reference' ? 'apiDocs' : 'articles'].push({
results[searchBucket(section.id)].push({
id: result.id, id: result.id,
type: 'heading', type: 'heading',
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`, subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
sectionType: sectionTypeBucket(section.id),
title: title:
section.id === 'reference' section.id === 'reference'
? article.title + '.' + heading.title ? article.title + '.' + heading.title
@ -94,6 +99,7 @@ export async function GET(req: NextRequest) {
score, score,
}) })
} }
const articles = await Promise.all( const articles = await Promise.all(
queryResults.map(async (result) => ({ queryResults.map(async (result) => ({
score: result.score, score: result.score,
@ -103,8 +109,10 @@ export async function GET(req: NextRequest) {
), ),
})) }))
) )
for (const { score, article } of articles.filter(Boolean)) { for (const { score, article } of articles.filter(Boolean)) {
if (visited.has(article.id)) continue if (visited.has(article.id)) continue
visited.add(article.id) visited.add(article.id)
const category = await db.db.get( const category = await db.db.get(
`SELECT id, title FROM categories WHERE categories.id = ?`, `SELECT id, title FROM categories WHERE categories.id = ?`,
@ -115,10 +123,12 @@ export async function GET(req: NextRequest) {
article.sectionId article.sectionId
) )
const isUncategorized = category.id === section.id + '_ucg' const isUncategorized = category.id === section.id + '_ucg'
results[section.id === 'reference' ? 'apiDocs' : 'articles'].push({
results[searchBucket(section.id)].push({
id: article.id, id: article.id,
type: 'article', type: 'article',
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`, subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
sectionType: sectionTypeBucket(section.id),
title: article.title, title: article.title,
url: isUncategorized url: isUncategorized
? `${section.id}/${article.id}` ? `${section.id}/${article.id}`
@ -126,23 +136,18 @@ export async function GET(req: NextRequest) {
score, score,
}) })
} }
const apiDocsScores = results.apiDocs.map((a) => a.score)
const maxScoreApiDocs = Math.max(...apiDocsScores) Object.keys(results).forEach((section: string) => {
const minScoreApiDocs = Math.min(...apiDocsScores) const scores = results[section as keyof Data['results']].map((a) => a.score)
const apiDocsBottom = minScoreApiDocs + (maxScoreApiDocs - minScoreApiDocs) * 0.75 const maxScore = Math.max(...scores)
results.apiDocs const minScore = Math.min(...scores)
.filter((a) => a.score > apiDocsBottom) const bottomScore = minScore + (maxScore - minScore) * (section === 'apiDocs' ? 0.75 : 0.5)
.sort((a, b) => b.score - a.score) results[section as keyof Data['results']]
.sort((a, b) => (b.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1)) .filter((a) => a.score > bottomScore)
.slice(0, 10) .sort((a, b) => b.score - a.score)
const articleScores = results.articles.map((a) => a.score) .sort((a, b) => (b.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1))
const maxScoreArticles = Math.max(...articleScores) })
const minScoreArticles = Math.min(...articleScores)
const articlesBottom = minScoreArticles + (maxScoreArticles - minScoreArticles) * 0.5
results.articles
.filter((a) => a.score > articlesBottom)
.sort((a, b) => b.score - a.score)
.sort((a, b) => (b.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1))
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
results, results,
@ -155,10 +160,7 @@ export async function GET(req: NextRequest) {
} catch (e: any) { } catch (e: any) {
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
results: { results: structuredClone(SEARCH_RESULTS),
articles: [],
apiDocs: [],
},
status: 'error', status: 'error',
error: e.message, error: e.message,
}), }),

View file

@ -1,11 +1,13 @@
import { SearchResult } from '@/types/search-types' import { SearchResult } from '@/types/search-types'
import { getDb } from '@/utils/ContentDatabase' import { getDb } from '@/utils/ContentDatabase'
import { NextRequest } from 'next/server' import { NextRequest } from 'next/server'
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
type Data = { type Data = {
results: { results: {
articles: SearchResult[] articles: SearchResult[]
apiDocs: SearchResult[] apiDocs: SearchResult[]
examples: SearchResult[]
} }
status: 'success' | 'error' | 'no-query' status: 'success' | 'error' | 'no-query'
} }
@ -23,10 +25,7 @@ export async function GET(req: NextRequest) {
if (!query) { if (!query) {
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
results: { results: structuredClone(SEARCH_RESULTS),
articles: [],
apiDocs: [],
},
status: 'no-query', status: 'no-query',
}), }),
{ {
@ -36,15 +35,9 @@ export async function GET(req: NextRequest) {
} }
try { try {
const results: Data['results'] = { const results: Data['results'] = structuredClone(SEARCH_RESULTS)
articles: [],
apiDocs: [],
}
const db = await getDb() const db = await getDb()
const queryWithoutSpaces = query.replace(/\s/g, '') const queryWithoutSpaces = query.replace(/\s/g, '')
const searchForArticle = await db.db.prepare( const searchForArticle = await db.db.prepare(
` `
SELECT id, title, sectionId, categoryId, content SELECT id, title, sectionId, categoryId, content
@ -61,16 +54,16 @@ export async function GET(req: NextRequest) {
await searchForArticle.all(query).then(async (queryResults) => { await searchForArticle.all(query).then(async (queryResults) => {
for (const article of queryResults) { for (const article of queryResults) {
const isApiDoc = article.sectionId === 'reference'
const section = await db.getSection(article.sectionId) const section = await db.getSection(article.sectionId)
const category = await db.getCategory(article.categoryId) const category = await db.getCategory(article.categoryId)
const isUncategorized = category.id === section.id + '_ucg' const isUncategorized = category.id === section.id + '_ucg'
results[isApiDoc ? 'apiDocs' : 'articles'].push({ results[searchBucket(article.sectionId)].push({
id: article.id, id: article.id,
type: 'article', type: 'article',
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`, subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
title: article.title, title: article.title,
sectionType: sectionTypeBucket(section.id),
url: isUncategorized url: isUncategorized
? `${section.id}/${article.id}` ? `${section.id}/${article.id}`
: `${section.id}/${category.id}/${article.id}`, : `${section.id}/${category.id}/${article.id}`,
@ -93,17 +86,17 @@ export async function GET(req: NextRequest) {
await searchForArticleHeadings.all(queryWithoutSpaces).then(async (queryResults) => { await searchForArticleHeadings.all(queryWithoutSpaces).then(async (queryResults) => {
for (const heading of queryResults) { for (const heading of queryResults) {
if (BANNED_HEADINGS.some((h) => heading.slug.endsWith(h))) continue if (BANNED_HEADINGS.some((h) => heading.slug.endsWith(h))) continue
const article = await db.getArticle(heading.articleId)
const isApiDoc = article.sectionId === 'reference' const article = await db.getArticle(heading.articleId)
const section = await db.getSection(article.sectionId) const section = await db.getSection(article.sectionId)
const category = await db.getCategory(article.categoryId) const category = await db.getCategory(article.categoryId)
const isUncategorized = category.id === section.id + '_ucg' const isUncategorized = category.id === section.id + '_ucg'
results[isApiDoc ? 'apiDocs' : 'articles'].push({ results[searchBucket(article.sectionId)].push({
id: article.id + '#' + heading.slug, id: article.id + '#' + heading.slug,
type: 'heading', type: 'heading',
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`, subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
sectionType: sectionTypeBucket(section.id),
title: title:
section.id === 'reference' section.id === 'reference'
? article.title + '.' + heading.title ? article.title + '.' + heading.title
@ -116,8 +109,8 @@ export async function GET(req: NextRequest) {
} }
}) })
results.apiDocs.sort((a, b) => b.score - a.score) Object.keys(results).forEach((section: string) => results[section as keyof Data['results']].sort((a, b) => b.score - a.score))
results.articles.sort((a, b) => b.score - a.score)
results.articles.sort( results.articles.sort(
(a, b) => (b.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1) (a, b) => (b.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1)
) )
@ -128,10 +121,7 @@ export async function GET(req: NextRequest) {
} catch (e: any) { } catch (e: any) {
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
results: { results: structuredClone(SEARCH_RESULTS),
articles: [],
apiDocs: [],
},
status: 'error', status: 'error',
error: e.message, error: e.message,
}), }),

View file

@ -1,97 +0,0 @@
import { Header } from '@/components/Header'
import { Sidebar } from '@/components/Sidebar'
import { SearchResult } from '@/types/search-types'
import { getDb } from '@/utils/ContentDatabase'
import Link from 'next/link'
import process from 'process'
const HOST_URL =
process.env.NODE_ENV === 'development'
? 'http://localhost:3001'
: process.env.NEXT_PUBLIC_SITE_URL ?? 'https://www.tldraw.dev'
export default async function SearchResultsPage({
searchParams,
}: {
searchParams: { q: string; t: string }
}) {
const query = searchParams.q?.toString() as string
const type = searchParams.t?.toString() as string
const db = await getDb()
const sidebar = await db.getSidebarContentList({})
let results: {
articles: SearchResult[]
apiDocs: SearchResult[]
error: null | string
} = {
articles: [],
apiDocs: [],
error: null,
}
if (query) {
const endPoint =
type === 'ai' ? `${HOST_URL}/api/ai?q=${query}` : `${HOST_URL}/api/search?q=${query}`
const res = await fetch(endPoint)
if (!res.ok) {
results.error = await res.text()
} else {
const json = await res.json()
results = json.results
}
}
return (
<>
<Header searchQuery={query} searchType={type} />
<Sidebar {...sidebar} searchQuery={query} searchType={type} />
<main className="article list">
<div className="page-header">
<h2>{`Found ${
results.articles.length + results.apiDocs.length
} results for "${query}"`}</h2>
<div className="search__results__switcher">
{type === 'ai' ? (
<Link href={`/search-results?q=${query}&t=n`}>Search again using exact match...</Link>
) : (
// TODO: replace emoji with icon
<Link href={`/search-results?q=${query}&t=ai`}> Search again using AI...</Link>
)}
</div>
</div>
<ResultsList results={results.articles} type={type} />
{results.articles.length > 0 && results.apiDocs.length > 0 && (
<>
<hr />
<h2>API Docs</h2>
</>
)}
{results.apiDocs.length > 0 && <ResultsList results={results.apiDocs} type={type} />}
</main>
</>
)
}
function ResultsList({ results, type }: { results: SearchResult[]; type?: string }) {
return (
<ul className="search__results__list">
{results.map((result) => (
<Link className="search__results__link" key={result.id} href={`${result.url}`}>
<li className="search__results__article">
<div className="search__results__article__details">
<h4>{result.subtitle}</h4>
{type === 'ai' && (
<span className="search__results__article__score">
{(result.score * 100).toFixed()}%
</span>
)}
</div>
<h3>{result.title}</h3>
</li>
</Link>
))}
</ul>
)
}

View file

@ -0,0 +1,193 @@
.autocomplete__wrapper {
position: relative;
display: flex;
height: 40px;
flex-direction: row;
border-radius: 24px;
padding: 0 16px;
background-color: var(--color-tint-1);
}
.autocomplete__input {
position: relative;
padding-left: 20px;
height: 100%;
width: 100%;
border-radius: 4px;
border: none;
background-color: var(--color-background);
font-family: var(--font-body);
font-size: 14px;
background-color: none;
background: none;
}
.autocomplete__input:disabled {
/* background-color: var(--color-tint-1); */
color: var(--color-tint-5);
}
.autocomplete__input::placeholder {
color: var(--color-tint-5);
}
.autocomplete__input:focus {
outline: none;
}
.autocomplete__icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
color: var(--color-tint-5);
left: 12px;
z-index: 2;
pointer-events: none;
transition: color 0.12s;
}
.autocomplete__cancel {
display: none;
}
.autocomplete__wrapper:focus-within .autocomplete__cancel {
display: flex;
}
.autocomplete__cancel {
justify-content: center;
align-items: center;
position: absolute;
top: 50%;
right: 12px;
transform: translateY(-50%);
z-index: 2;
border: 0;
cursor: pointer;
height: 24px;
min-width: 24px;
line-height: 26px;
color: var(--color-tint-6);
background-color: var(--color-tint-2);
border-radius: 2px;
}
.autocomplete__item__icon {
width: 24px;
height: 24px;
flex: 0 0 24px;
}
.autocomplete__group {
font-size: 14px;
font-weight: normal;
color: var(--color-text-secondary);
text-transform: uppercase;
white-space: nowrap;
}
.autocomplete__wrapper:focus-within .autocomplete__icon {
color: var(--color-text);
}
.autocomplete__item {
position: relative;
display: flex;
height: 2.5rem;
cursor: default;
scroll-margin-top: 0.25rem;
scroll-margin-bottom: 0.25rem;
align-items: center;
border-radius: 0.25rem;
padding-left: 1.75rem;
padding-right: 1.75rem;
color: hsl(204 10% 10%);
outline: 2px solid transparent;
outline-offset: 2px;
}
.autocomplete__item [data-user-value] {
font-weight: bold;
}
@media (min-width: 640px) {
.autocomplete__item {
height: 2.25rem;
font-size: 15px;
}
}
.autocomplete__popover {
position: relative;
z-index: 50;
display: flex;
max-height: min(var(--popover-available-height, 300px), 300px);
flex-direction: column;
overflow: auto;
overscroll-behavior: contain;
border-radius: 0.5rem;
border-width: 1px;
border-style: solid;
border-color: hsl(204 20% 88%);
background-color: hsl(204 20% 100%);
padding: 0.5rem;
color: hsl(204 10% 10%);
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow:
0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1);
}
: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 4px 6px -4px rgb(0 0 0 / 0.1);
}
.autocomplete__item {
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;
}
.autocomplete__item:hover {
background-color: hsl(204 100% 80% / 0.4);
}
.autocomplete__item[data-active-item] {
background-color: hsl(204 100% 40%);
color: hsl(204 20% 100%);
}
.autocomplete__item:active,
.autocomplete__item[data-active] {
padding-top: 9px;
padding-bottom: 7px;
}
:is([data-theme="dark"] .autocomplete__item) {
color: hsl(204 20% 100%);
}
:is([data-theme="dark"] .autocomplete__item__icon path) {
fill: hsl(204 20% 100%);
}
:is([data-theme="dark"] .autocomplete__item:hover) {
background-color: hsl(204 100% 40% / 0.25);
}
:is([data-theme="dark"] .autocomplete__item)[data-active-item] {
background-color: hsl(204 100% 40%);
}

View file

@ -0,0 +1,123 @@
import {
Combobox,
ComboboxCancel,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxItem,
ComboboxItemValue,
ComboboxPopover,
ComboboxProvider,
} from '@ariakit/react'
import { ComponentType, ForwardedRef, forwardRef, startTransition, useState } from 'react'
import './Autocomplete.css'
import { Icon } from './Icon'
import Spinner from './Spinner'
export type DropdownOption = {
label: string
value: string
group?: string
}
type AutocompleteProps = {
customUI?: React.ReactNode
groups?: string[]
groupsToIcon?: {
[key: string]: ComponentType<{
className?: string
}>
}
groupsToLabel?: { [key: string]: string }
isLoading: boolean
options: DropdownOption[]
onChange: (value: string) => void
onInputChange: (value: string) => void
}
const DEFAULT_GROUP = 'autocomplete-default'
const Autocomplete = forwardRef(function Autocomplete(
{
customUI,
groups = [DEFAULT_GROUP],
groupsToIcon,
groupsToLabel,
isLoading,
options,
onInputChange,
onChange,
}: AutocompleteProps,
ref: ForwardedRef<HTMLInputElement>
) {
const [open, setOpen] = useState(false)
const [value, setValue] = useState('')
const renderedGroups = groups.map((group) => {
const filteredOptions = options.filter(
({ group: optionGroup }) => optionGroup === group || group === DEFAULT_GROUP
)
if (filteredOptions.length === 0) return null
return (
<ComboboxGroup>
{groupsToLabel?.[group] && (
<ComboboxGroupLabel className="autocomplete__group">
{groupsToLabel?.[group]}
</ComboboxGroupLabel>
)}
{filteredOptions.map(({ label, value }) => {
const Icon = groupsToIcon?.[group]
return (
<ComboboxItem key={`${label}-${value}`} className="autocomplete__item" value={value}>
{Icon && <Icon className="autocomplete__item__icon" />}
<ComboboxItemValue value={label} />
</ComboboxItem>
)
})}
</ComboboxGroup>
)
})
return (
<ComboboxProvider<string>
defaultSelectedValue=""
open={open}
setOpen={setOpen}
resetValueOnHide
includesBaseElement={false}
setValue={(newValue) => {
startTransition(() => setValue(newValue))
onInputChange(newValue)
}}
setSelectedValue={(newValue) => onChange(newValue)}
>
<div className="autocomplete__wrapper">
{isLoading ? (
<Spinner className="autocomplete__icon" />
) : (
<Icon className="autocomplete__icon" icon="search" small />
)}
<Combobox
autoSelect
placeholder="Search…"
ref={ref}
className="autocomplete__input"
value={value}
/>
{value && <ComboboxCancel className="autocomplete__cancel" />}
{value && (
<ComboboxPopover className="autocomplete__popover">
{customUI}
{options.length === 0 && <span>No results found.</span>}
{options.length !== 0 && renderedGroups}
</ComboboxPopover>
)}
</div>
</ComboboxProvider>
)
})
export default Autocomplete

View file

@ -7,15 +7,7 @@ import { Chevron } from './Icons'
import { Search } from './Search' import { Search } from './Search'
import { ThemeSwitcher } from './ThemeSwitcher' import { ThemeSwitcher } from './ThemeSwitcher'
export function Header({ export function Header({ sectionId }: { sectionId?: string }) {
searchQuery,
searchType,
sectionId,
}: {
searchQuery?: string
searchType?: string
sectionId?: string
}) {
return ( return (
<div className="layout__header"> <div className="layout__header">
<Link href="/quick-start"> <Link href="/quick-start">
@ -27,7 +19,7 @@ export function Header({
}} }}
/> />
</Link> </Link>
<Search prevQuery={searchQuery} prevType={searchType} /> <Search />
<div className="layout__header__sections_and_socials"> <div className="layout__header__sections_and_socials">
<SectionLinks sectionId={sectionId} /> <SectionLinks sectionId={sectionId} />
<ThemeSwitcher /> <ThemeSwitcher />

View file

@ -1,77 +1,162 @@
'use client' 'use client'
import { usePathname, useRouter } from 'next/navigation' import { SEARCH_TYPE, SearchResult } from '@/types/search-types'
import { useCallback, useEffect, useRef, useState } from 'react' import { debounce } from '@/utils/debounce'
import { useRouter } from 'next/navigation'
import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { Icon } from './Icon' import Autocomplete, { DropdownOption } from './Autocomplete'
export function Search({ const HOST_URL =
prevType = 'n', process.env.NODE_ENV === 'development'
prevQuery = '', ? 'http://localhost:3001'
}: { : process.env.NEXT_PUBLIC_SITE_URL ?? 'https://www.tldraw.dev'
prevType?: string
prevQuery?: string
}) {
const [query, setQuery] = useState(prevQuery)
const [isDisabled, setIsDisabled] = useState(false)
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value)
}, [])
export function Search() {
const [searchType, setSearchType] = useState<SEARCH_TYPE>(SEARCH_TYPE.NORMAL)
const [isLoading, setIsLoading] = useState(false)
const [searchResults, setSearchResults] = useState<DropdownOption[]>([])
const [query, setQuery] = useState('')
const [platform, setPlatform] = useState<'mac' | 'nonMac' | null>()
const rInput = useRef<HTMLInputElement>(null) const rInput = useRef<HTMLInputElement>(null)
const pathName = usePathname()
const router = useRouter() const router = useRouter()
const handleInputChange = debounce((query: string) => setQuery(query), 200)
useEffect(() => {
async function handleFetchResults() {
if (!query) {
return
}
setIsLoading(true)
try {
const endPoint =
searchType === SEARCH_TYPE.AI
? `${HOST_URL}/api/ai?q=${query}`
: `${HOST_URL}/api/search?q=${query}`
const res = await fetch(endPoint)
if (res.ok) {
const json = await res.json()
const topArticles = json.results.articles.slice(0, 10)
const topAPI = json.results.apiDocs.slice(0, 10)
const topExamples = json.results.examples.slice(0, 10)
const allResults = topExamples.concat(topArticles).concat(topAPI)
setSearchResults(
allResults.map((result: SearchResult) => ({
label: result.title,
value: result.url,
group: result.sectionType,
}))
)
}
} catch (err) {
console.error(err)
}
setIsLoading(false)
}
handleFetchResults()
}, [query, searchType])
const handleChange = (url: string) => {
router.push(url.startsWith('/') ? url : `/${url}`)
}
const handleSearchTypeChange = () => {
setSearchType(searchType === SEARCH_TYPE.AI ? SEARCH_TYPE.NORMAL : SEARCH_TYPE.AI)
}
useEffect(() => {
setPlatform(
// TODO(mime): we should have a standard hook for this.
// And also, we should navigator.userAgentData.platform where available.
// eslint-disable-next-line deprecation/deprecation
typeof window !== 'undefined' && /mac/i.test(window.navigator.platform) ? 'mac' : 'nonMac'
)
}, [])
useHotkeys('meta+k,ctrl+k', (e) => { useHotkeys('meta+k,ctrl+k', (e) => {
e.preventDefault() e.preventDefault()
rInput.current?.focus() rInput.current?.focus()
rInput.current?.select() rInput.current?.select()
}) })
useEffect(() => {
setIsDisabled(false)
}, [pathName])
const handleFocus = useCallback(() => {
// focus input and select all
rInput.current!.focus()
rInput.current!.select()
}, [])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
const currentQuery = rInput.current!.value
if (e.key === 'Enter' && currentQuery !== prevQuery) {
setIsDisabled(true)
router.push(`/search-results?q=${currentQuery}&t=${prevType}`)
} else if (e.key === 'Escape') {
rInput.current!.blur()
}
},
[router, prevQuery, prevType]
)
return ( return (
<div className="search__wrapper"> <div className="search__wrapper">
<div className="search"> <Autocomplete
<Icon className="search__icon" icon="search" small /> ref={rInput}
<input customUI={
ref={rInput} <button className="search__ai-toggle" onClick={handleSearchTypeChange}>
type="text" {searchType === SEARCH_TYPE.NORMAL ? '✨ Search using AI' : '⭐ Search without AI'}
className="search__input" </button>
placeholder="Search..." }
value={query} groups={['examples', 'docs', 'reference']}
onChange={handleChange} groupsToLabel={{ examples: 'Examples', docs: 'Articles', reference: 'Reference' }}
onFocus={handleFocus} groupsToIcon={{ examples: CodeIcon, docs: DocIcon, reference: ReferenceIcon }}
onKeyDown={handleKeyDown} options={searchResults}
autoCapitalize="off" isLoading={isLoading}
autoComplete="off" onInputChange={handleInputChange}
autoCorrect="off" onChange={handleChange}
disabled={isDisabled} />
/> {platform && (
</div> <span className="search__keyboard">
{platform === 'mac' && <kbd data-platform="mac"></kbd>}
{platform === 'nonMac' && <kbd data-platform="win">Ctrl</kbd>}
<kbd>K</kbd>
</span>
)}
</div> </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>
)

View file

@ -28,15 +28,7 @@ const linkContext = createContext<{
sectionId: string | null sectionId: string | null
} | null>(null) } | null>(null)
export function Sidebar({ export function Sidebar({ headings, links, sectionId, categoryId, articleId }: SidebarProps) {
headings,
links,
sectionId,
categoryId,
articleId,
searchQuery,
searchType,
}: SidebarProps) {
const activeId = articleId ?? categoryId ?? sectionId const activeId = articleId ?? categoryId ?? sectionId
const pathName = usePathname() const pathName = usePathname()
@ -45,17 +37,13 @@ export function Sidebar({
document.body.classList.remove('sidebar-open') document.body.classList.remove('sidebar-open')
document.querySelector('.sidebar__nav [data-active=true]')?.scrollIntoView({ block: 'center' }) document.querySelector('.sidebar__nav [data-active=true]')?.scrollIntoView({ block: 'center' })
// XXX(mime): scrolling the sidebar into position also scrolls the page to the wrong
// spot. this compensates for that but, ugh.
document.documentElement.scrollTop = 0
}, [pathName]) }, [pathName])
return ( return (
<> <>
<linkContext.Provider value={{ activeId, articleId, categoryId, sectionId }}> <linkContext.Provider value={{ activeId, articleId, categoryId, sectionId }}>
<div className="sidebar" onScroll={(e) => e.stopPropagation()}> <div className="sidebar" onScroll={(e) => e.stopPropagation()}>
<Search prevQuery={searchQuery} prevType={searchType} /> <Search />
<div className="sidebar__section__links"> <div className="sidebar__section__links">
<SectionLinks sectionId={sectionId} /> <SectionLinks sectionId={sectionId} />
</div> </div>

View file

@ -0,0 +1,20 @@
// TODO(mime): copied from tldraw package, needs to be in another shared location.
export default 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">
<circle strokeOpacity={0.25} cx={8} cy={8} r={7} stroke="currentColor" />
<path strokeLinecap="round" d="M15 8c0-4.5-4.5-7-7-7" stroke="currentColor">
<animateTransform
attributeName="transform"
type="rotate"
from="0 8 8"
to="360 8 8"
dur="1s"
repeatCount="indefinite"
/>
</path>
</g>
</svg>
)
}

View file

@ -8,4 +8,3 @@ status: published
--- ---
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.12) [View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.12)

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,6 @@ status: published
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.14) [View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.14)
#### Disable styles panel button on mobile when using the laser tool. ([#1704](https://github.com/tldraw/tldraw/pull/1704)) #### Disable styles panel button on mobile when using the laser tool. ([#1704](https://github.com/tldraw/tldraw/pull/1704))
- Disable the styles panel button for laser tool on mobile. - Disable the styles panel button for laser tool on mobile.
@ -112,4 +111,4 @@ status: published
- David Sheldrick ([@ds300](https://github.com/ds300)) - David Sheldrick ([@ds300](https://github.com/ds300))
- Lu Wilson ([@TodePond](https://github.com/TodePond)) - Lu Wilson ([@TodePond](https://github.com/TodePond))
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek)) - Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok)) - Steve Ruiz ([@steveruizok](https://github.com/steveruizok))

View file

@ -9,7 +9,6 @@ status: published
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.15) [View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.15)
#### frame label fix ([#2016](https://github.com/tldraw/tldraw/pull/2016)) #### frame label fix ([#2016](https://github.com/tldraw/tldraw/pull/2016))
- Add a brief release note for your PR here. - Add a brief release note for your PR here.
@ -36,8 +35,14 @@ This pr add the custom defined shapes that's being passed to Tldraw
Before/After Before/After
<img width="300" src="https://github.com/tldraw/tldraw/assets/98838967/91ea55c8-0fcc-4f73-b61e-565829a5f25e" /> <img
<img width="300" src="https://github.com/tldraw/tldraw/assets/98838967/ee4070fe-e236-4818-8fb4-43520210102b" /> width="300"
src="https://github.com/tldraw/tldraw/assets/98838967/91ea55c8-0fcc-4f73-b61e-565829a5f25e"
/>
<img
width="300"
src="https://github.com/tldraw/tldraw/assets/98838967/ee4070fe-e236-4818-8fb4-43520210102b"
/>
#### [fix] pinch events ([#1979](https://github.com/tldraw/tldraw/pull/1979)) #### [fix] pinch events ([#1979](https://github.com/tldraw/tldraw/pull/1979))
@ -49,8 +54,14 @@ Before/After
Before/After Before/After
<image width="350" src="https://github.com/tldraw/tldraw/assets/98838967/320171b4-61e0-4a41-b8d3-830bd90bea65" /> <image
<image width="350" src="https://github.com/tldraw/tldraw/assets/98838967/b42d7156-0ce9-4894-9692-9338dc931b79" /> width="350"
src="https://github.com/tldraw/tldraw/assets/98838967/320171b4-61e0-4a41-b8d3-830bd90bea65"
/>
<image
width="350"
src="https://github.com/tldraw/tldraw/assets/98838967/b42d7156-0ce9-4894-9692-9338dc931b79"
/>
#### Remove focus management ([#1953](https://github.com/tldraw/tldraw/pull/1953)) #### Remove focus management ([#1953](https://github.com/tldraw/tldraw/pull/1953))
@ -71,8 +82,14 @@ Before/After
Before & After: Before & After:
<image width="250" src="https://github.com/tldraw/tldraw/assets/98838967/e0ca7d54-506f-4014-b65a-6b61a98e3665" /> <image
<image width="250" src="https://github.com/tldraw/tldraw/assets/98838967/90c9fa12-1bcb-430d-80c7-97e1faacea16" /> width="250"
src="https://github.com/tldraw/tldraw/assets/98838967/e0ca7d54-506f-4014-b65a-6b61a98e3665"
/>
<image
width="250"
src="https://github.com/tldraw/tldraw/assets/98838967/90c9fa12-1bcb-430d-80c7-97e1faacea16"
/>
#### Allow right clicking selection backgrounds ([#1968](https://github.com/tldraw/tldraw/pull/1968)) #### Allow right clicking selection backgrounds ([#1968](https://github.com/tldraw/tldraw/pull/1968))
@ -88,7 +105,7 @@ Before & After:
#### Lokalise: Translations update ([#1964](https://github.com/tldraw/tldraw/pull/1964)) #### Lokalise: Translations update ([#1964](https://github.com/tldraw/tldraw/pull/1964))
* Updated community translations for German and Galician - Updated community translations for German and Galician
#### [improvement] improve arrows (for real) ([#1957](https://github.com/tldraw/tldraw/pull/1957)) #### [improvement] improve arrows (for real) ([#1957](https://github.com/tldraw/tldraw/pull/1957))
@ -132,7 +149,7 @@ Before & After:
#### :recycle: fix: editing is not terminated after the conversion is confirmed. ([#1885](https://github.com/tldraw/tldraw/pull/1885)) #### :recycle: fix: editing is not terminated after the conversion is confirmed. ([#1885](https://github.com/tldraw/tldraw/pull/1885))
- fix: editing is not terminated after the conversion is confirmed. - fix: editing is not terminated after the conversion is confirmed.
#### Update community translations ([#1889](https://github.com/tldraw/tldraw/pull/1889)) #### Update community translations ([#1889](https://github.com/tldraw/tldraw/pull/1889))
@ -450,10 +467,12 @@ Removed a feature to reset the viewport back to a shape that is being edited.
- `@tldraw/editor`, `@tldraw/store`, `@tldraw/tldraw`, `@tldraw/tlschema` - `@tldraw/editor`, `@tldraw/store`, `@tldraw/tldraw`, `@tldraw/tlschema`
- Migrate snapshot [#1843](https://github.com/tldraw/tldraw/pull/1843) ([@steveruizok](https://github.com/steveruizok)) - Migrate snapshot [#1843](https://github.com/tldraw/tldraw/pull/1843) ([@steveruizok](https://github.com/steveruizok))
- `@tldraw/tldraw` - `@tldraw/tldraw`
- export asset stuff [#1829](https://github.com/tldraw/tldraw/pull/1829) ([@steveruizok](https://github.com/steveruizok)) - export asset stuff [#1829](https://github.com/tldraw/tldraw/pull/1829) ([@steveruizok](https://github.com/steveruizok)
)
- [feature] Asset props [#1824](https://github.com/tldraw/tldraw/pull/1824) ([@steveruizok](https://github.com/steveruizok)) - [feature] Asset props [#1824](https://github.com/tldraw/tldraw/pull/1824) ([@steveruizok](https://github.com/steveruizok))
- [feature] unlock all action [#1820](https://github.com/tldraw/tldraw/pull/1820) ([@steveruizok](https://github.com/steveruizok)) - [feature] unlock all action [#1820](https://github.com/tldraw/tldraw/pull/1820) ([@steveruizok](https://github.com/steveruizok))
- export `UiEventsProvider` [#1774](https://github.com/tldraw/tldraw/pull/1774) ([@steveruizok](https://github.com/steveruizok)) - export `UiEventsProvider` [#1774](https://github.com/tldraw/tldraw/pull/1774) ([@steveruizok](https://github.com/steveruizok)
)
- `@tldraw/editor` - `@tldraw/editor`
- Add className as prop to Canvas [#1827](https://github.com/tldraw/tldraw/pull/1827) ([@steveruizok](https://github.com/steveruizok)) - Add className as prop to Canvas [#1827](https://github.com/tldraw/tldraw/pull/1827) ([@steveruizok](https://github.com/steveruizok))
- refactor `parentsToChildrenWithIndexes` [#1764](https://github.com/tldraw/tldraw/pull/1764) ([@steveruizok](https://github.com/steveruizok)) - refactor `parentsToChildrenWithIndexes` [#1764](https://github.com/tldraw/tldraw/pull/1764) ([@steveruizok](https://github.com/steveruizok))
@ -634,4 +653,4 @@ Removed a feature to reset the viewport back to a shape that is being edited.
- Ricardo Crespo ([@ricardo-crespo](https://github.com/ricardo-crespo)) - Ricardo Crespo ([@ricardo-crespo](https://github.com/ricardo-crespo))
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok)) - Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
- Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git)) - Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git))
- Takuto Mori Gump ([@mr04vv](https://github.com/mr04vv)) - Takuto Mori Gump ([@mr04vv](https://github.com/mr04vv))

View file

@ -9,7 +9,6 @@ status: published
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.16) [View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.16)
#### Fix shape opacity when erasing ([#2055](https://github.com/tldraw/tldraw/pull/2055)) #### Fix shape opacity when erasing ([#2055](https://github.com/tldraw/tldraw/pull/2055))
- Fixes opacity of shapes while erasing in a group or frame. - Fixes opacity of shapes while erasing in a group or frame.
@ -24,9 +23,15 @@ status: published
Before/after: Before/after:
<image width="250" src="https://github.com/tldraw/tldraw/assets/98838967/763a93eb-ffaa-405c-9255-e68ba88ed9a2" /> <image
width="250"
src="https://github.com/tldraw/tldraw/assets/98838967/763a93eb-ffaa-405c-9255-e68ba88ed9a2"
/>
<image width="250" src="https://github.com/tldraw/tldraw/assets/98838967/dc9d3f77-c1c5-40f2-a9fe-10c723b6a21c" /> <image
width="250"
src="https://github.com/tldraw/tldraw/assets/98838967/dc9d3f77-c1c5-40f2-a9fe-10c723b6a21c"
/>
#### fix: proper label for opacity tooltip on hover ([#2044](https://github.com/tldraw/tldraw/pull/2044)) #### fix: proper label for opacity tooltip on hover ([#2044](https://github.com/tldraw/tldraw/pull/2044))
@ -97,4 +102,4 @@ Before/after:
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek)) - Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Prince Mendiratta ([@Prince-Mendiratta](https://github.com/Prince-Mendiratta)) - Prince Mendiratta ([@Prince-Mendiratta](https://github.com/Prince-Mendiratta))
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok)) - Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
- Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git)) - Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git))

View file

@ -9,7 +9,6 @@ status: published
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.17) [View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.17)
#### Firefox, Touch: Fix not being able to open style dropdowns ([#2092](https://github.com/tldraw/tldraw/pull/2092)) #### Firefox, Touch: Fix not being able to open style dropdowns ([#2092](https://github.com/tldraw/tldraw/pull/2092))
- Firefox Mobile: Fixed a bug where you couldn't open some style dropdown options. - Firefox Mobile: Fixed a bug where you couldn't open some style dropdown options.
@ -49,8 +48,8 @@ status: published
#### [fix] locked shape of opacity problem with eraser.pointing ([#2073](https://github.com/tldraw/tldraw/pull/2073)) #### [fix] locked shape of opacity problem with eraser.pointing ([#2073](https://github.com/tldraw/tldraw/pull/2073))
- locked shape of opacity problem with eraser.pointing - locked shape of opacity problem with eraser.pointing
Before/after: Before/after:
![A](https://github.com/tldraw/tldraw/assets/59823089/7483506c-72ac-45cc-93aa-f2a794ea8ff0) ![B](https://github.com/tldraw/tldraw/assets/59823089/ef0f988c-83f5-46a2-b891-0a391bca2f87) ![A](https://github.com/tldraw/tldraw/assets/59823089/7483506c-72ac-45cc-93aa-f2a794ea8ff0) ![B](https://github.com/tldraw/tldraw/assets/59823089/ef0f988c-83f5-46a2-b891-0a391bca2f87)
--- ---
@ -107,4 +106,4 @@ Before/after:
- Lu Wilson ([@TodePond](https://github.com/TodePond)) - Lu Wilson ([@TodePond](https://github.com/TodePond))
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek)) - Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok)) - Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
- Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git)) - Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git))

View file

@ -9,7 +9,6 @@ status: published
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.18) [View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.18)
#### Fix an error when using context menu. ([#2186](https://github.com/tldraw/tldraw/pull/2186)) #### Fix an error when using context menu. ([#2186](https://github.com/tldraw/tldraw/pull/2186))
- Fixes the console error when opening the context menu for the first time. - Fixes the console error when opening the context menu for the first time.
@ -184,4 +183,4 @@ status: published
- Lu Wilson ([@TodePond](https://github.com/TodePond)) - Lu Wilson ([@TodePond](https://github.com/TodePond))
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek)) - Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok)) - Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
- Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git)) - Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git))

View file

@ -9,7 +9,6 @@ status: published
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.19) [View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-alpha.19)
#### zoom to affected shapes after undo/redo ([#2293](https://github.com/tldraw/tldraw/pull/2293)) #### zoom to affected shapes after undo/redo ([#2293](https://github.com/tldraw/tldraw/pull/2293))
- Make sure affected shapes are visible after undo/redo - Make sure affected shapes are visible after undo/redo
@ -248,4 +247,4 @@ status: published
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek)) - Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok)) - Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
- Sugit ([@sugitlab](https://github.com/sugitlab)) - Sugit ([@sugitlab](https://github.com/sugitlab))
- Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git)) - Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git))

View file

@ -9,7 +9,6 @@ status: published
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-beta.1) [View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-beta.1)
#### add speech bubble example ([#2362](https://github.com/tldraw/tldraw/pull/2362)) #### add speech bubble example ([#2362](https://github.com/tldraw/tldraw/pull/2362))
- Add an example for making a custom shape with handles, this one is a speech bubble with a movable tail. - Add an example for making a custom shape with handles, this one is a speech bubble with a movable tail.
@ -149,4 +148,4 @@ Updated translations for German, Korean, Russian, Ukrainian, Traditional Chinese
- MinhoPark ([@Lennon57](https://github.com/Lennon57)) - MinhoPark ([@Lennon57](https://github.com/Lennon57))
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek)) - Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok)) - Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
- Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git)) - Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git))

View file

@ -9,7 +9,6 @@ status: published
[View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-beta.2) [View on GitHub](https://github.com/tldraw/tldraw/releases/tag/v2.0.0-beta.2)
#### Fix validation when pasting images. ([#2436](https://github.com/tldraw/tldraw/pull/2436)) #### Fix validation when pasting images. ([#2436](https://github.com/tldraw/tldraw/pull/2436))
- Fixes url validations. - Fixes url validations.
@ -164,4 +163,4 @@ status: published
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek)) - Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
- Stan Flint ([@StanFlint](https://github.com/StanFlint)) - Stan Flint ([@StanFlint](https://github.com/StanFlint))
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok)) - Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
- Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git)) - Taha ([@Taha-Hassan-Git](https://github.com/Taha-Hassan-Git))

View file

@ -34,7 +34,13 @@ const nextConfig = {
{ {
// For reverse compatibility with old links // For reverse compatibility with old links
source: '/docs/usage', source: '/docs/usage',
destination: '/usage', destination: '/installation',
permanent: true,
},
{
// For reverse compatibility with old links
source: '/usage',
destination: '/installation',
permanent: true, permanent: true,
}, },
{ {

View file

@ -43,6 +43,7 @@
"watch-content": "tsx ./watcher.ts" "watch-content": "tsx ./watcher.ts"
}, },
"dependencies": { "dependencies": {
"@ariakit/react": "^0.4.1",
"@codesandbox/sandpack-react": "^2.11.3", "@codesandbox/sandpack-react": "^2.11.3",
"@microsoft/api-extractor-model": "^7.26.4", "@microsoft/api-extractor-model": "^7.26.4",
"@microsoft/tsdoc": "^0.14.2", "@microsoft/tsdoc": "^0.14.2",

View file

@ -959,157 +959,52 @@ body {
box-shadow: var(--shadow-small); box-shadow: var(--shadow-small);
} }
/* --------------------- Search --------------------- */
.sidebar .search__wrapper { .sidebar .search__wrapper {
display: none; display: none;
} }
.search__wrapper { .search__wrapper {
display: flex;
flex-direction: row;
}
.search {
position: relative; position: relative;
z-index: 200;
height: 40px;
padding: 4px;
flex-grow: 2;
} }
.search__icon { .search__wrapper:focus-within .search__keyboard {
display: none;
}
.search__keyboard {
display: block;
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 12px;
z-index: 2;
transform: translateY(-50%); transform: translateY(-50%);
color: var(--color-tint-5); color: var(--color-tint-5);
left: 0px;
z-index: 2;
pointer-events: none; pointer-events: none;
transition: color 0.12s; transition: color 0.12s;
} }
.search:focus-within .search__icon { .search__keyboard kbd {
display: inline-block;
height: 24px;
min-width: 24px;
line-height: 26px;
padding: 0 4px;
text-align: center;
margin-right: 4px;
font-size: 12px;
color: var(--color-tint-6);
background-color: var(--color-tint-2);
border-radius: 2px;
}
.search__ai-toggle {
border: 0;
color: var(--color-text); color: var(--color-text);
}
.search__input {
position: relative;
padding-left: 20px;
height: 100%;
width: 100%;
border-radius: var(--border-radius-menu);
border: none;
background-color: var(--color-background);
font-family: var(--font-body);
font-size: 14px; font-size: 14px;
background-color: none; text-align: left;
background: none; cursor: pointer;
} margin: 4px 0;
background: transparent;
.search__input:disabled {
/* background-color: var(--color-tint-1); */
color: var(--color-tint-5);
}
.search__input::placeholder {
color: var(--color-tint-5);
}
.search__input:focus {
outline: none;
}
.search__check {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
right: 0px;
gap: 6px;
font-size: 12px;
text-transform: uppercase;
}
/* Search Results */
.search__results__wrapper {
position: relative;
height: 0px;
width: 100%;
z-index: 100;
}
.search__results {
padding: 0px;
position: absolute;
top: 0px;
left: 0px;
background-color: var(--color-background);
width: 320px;
border-radius: 8px;
box-shadow: var(--shadow-big);
max-height: 80vh;
overflow-y: auto;
}
.search__results__switcher {
margin-top: 16px;
font-size: 14px;
}
.article ul.search__results__list {
list-style-type: none;
padding-left: 0px;
margin-left: 0px;
}
.search__results__article {
flex-direction: column;
align-items: flex-start;
justify-content: center;
font-size: 16px;
padding-bottom: 12px;
height: fit-content;
border-radius: 12px;
margin: 0px;
}
.search__results__article h4 {
font-size: 12px;
font-weight: 500;
color: var(--color-text);
}
.search__results__article h3 {
font-size: 16px;
font-weight: 500;
}
.search__results__article__details {
display: flex;
flex-direction: row;
align-items: center;
}
.search__results__article__score {
font-size: 12px;
padding: 0px 8px;
border-left: 1px solid var(--color-tint-4);
line-height: 1;
height: min-content;
color: var(--color-tint-5);
margin-left: 8px;
text-decoration: none;
}
@media (hover: hover) {
.search__results__list a:hover {
text-decoration: none;
}
.search__results__list a:hover h3 {
text-decoration: underline;
}
} }
/* --------------------- Desktop M --------------------- */ /* --------------------- Desktop M --------------------- */

View file

@ -185,8 +185,6 @@ export type SidebarContentList = {
articleId: string | null articleId: string | null
links: SidebarContentLink[] links: SidebarContentLink[]
activeId?: string | null activeId?: string | null
searchQuery?: string
searchType?: string
} }
/* ---------- Finished / generated content ---------- */ /* ---------- Finished / generated content ---------- */

View file

@ -2,7 +2,13 @@ export type SearchResult = {
type: 'article' | 'category' | 'section' | 'heading' type: 'article' | 'category' | 'section' | 'heading'
id: string id: string
subtitle: string subtitle: string
sectionType: string
title: string title: string
url: string url: string
score: number score: number
} }
export enum SEARCH_TYPE {
AI = 'ai',
NORMAL = 'n',
}

View file

@ -0,0 +1,9 @@
export const SEARCH_RESULTS = {
articles: [],
apiDocs: [],
examples: [],
}
export const searchBucket = (sectionId: string) =>
sectionId === 'examples' ? 'examples' : sectionId === 'reference' ? 'apiDocs' : 'articles'
export const sectionTypeBucket = (sectionId: string) =>
['examples', 'reference'].includes(sectionId) ? sectionId : 'docs'

View file

@ -51,6 +51,39 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@ariakit/core@npm:0.4.1":
version: 0.4.1
resolution: "@ariakit/core@npm:0.4.1"
checksum: 536697f9608c1c0694c76e797a0c54a107fda14dec779a5ebbf8fe7684f2fb3f315e0cd337d4e8cc97956377b061b7a8b2046d70024cb5db45667852f4c40960
languageName: node
linkType: hard
"@ariakit/react-core@npm:0.4.1":
version: 0.4.1
resolution: "@ariakit/react-core@npm:0.4.1"
dependencies:
"@ariakit/core": "npm:0.4.1"
"@floating-ui/dom": "npm:^1.0.0"
use-sync-external-store: "npm:^1.2.0"
peerDependencies:
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
checksum: 83731f8be4301555b00d68825c9b195084961dc944affbead636dd7125c95bb5abec6d40646fb051ef8e20108fe641654207da8f7329059d74019a4b29ed28d5
languageName: node
linkType: hard
"@ariakit/react@npm:^0.4.1":
version: 0.4.1
resolution: "@ariakit/react@npm:0.4.1"
dependencies:
"@ariakit/react-core": "npm:0.4.1"
peerDependencies:
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
checksum: 1414b8aac17efea15793c2bcade85a32bdeb9b8cad8355a9f889a16e45ce97ad00699001bc2edd181b003fac7e18cd5f4e96792a581d1107d87a403386eb11e8
languageName: node
linkType: hard
"@auto-it/bot-list@npm:10.46.0": "@auto-it/bot-list@npm:10.46.0":
version: 10.46.0 version: 10.46.0
resolution: "@auto-it/bot-list@npm:10.46.0" resolution: "@auto-it/bot-list@npm:10.46.0"
@ -3455,22 +3488,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@floating-ui/core@npm:^1.5.3": "@floating-ui/core@npm:^1.6.0":
version: 1.5.3 version: 1.6.0
resolution: "@floating-ui/core@npm:1.5.3" resolution: "@floating-ui/core@npm:1.6.0"
dependencies: dependencies:
"@floating-ui/utils": "npm:^0.2.0" "@floating-ui/utils": "npm:^0.2.1"
checksum: 7d9feaca2565a2a71bf03d23cd292c03def63097d7fde7d62909cdb8ddb84664781f3922086bcf10443f3310cb92381a0ecf745b2774edb917fa74fe61015c56 checksum: d6a47cacde193cd8ccb4c268b91ccc4ca254dffaec6242b07fd9bcde526044cc976d27933a7917f9a671de0a0e27f8d358f46400677dbd0c8199de293e9746e1
languageName: node languageName: node
linkType: hard linkType: hard
"@floating-ui/dom@npm:^1.5.4": "@floating-ui/dom@npm:^1.0.0, @floating-ui/dom@npm:^1.5.4":
version: 1.5.4 version: 1.6.1
resolution: "@floating-ui/dom@npm:1.5.4" resolution: "@floating-ui/dom@npm:1.6.1"
dependencies: dependencies:
"@floating-ui/core": "npm:^1.5.3" "@floating-ui/core": "npm:^1.6.0"
"@floating-ui/utils": "npm:^0.2.0" "@floating-ui/utils": "npm:^0.2.1"
checksum: 3ba02ba2b4227c1e18df6ccdd029a1c100058db2e76ca1dac60a593ec72b2d4d995fa5c2d1639a5c38adb17e12398fbfe4f6cf5fd45f2ee6170ed0cf64acea06 checksum: c010feb55be37662eb4cc8d0a22e21359c25247bbdcd9557617fd305cf08c8f020435b17e4b4f410201ba9abe3a0dd96b5c42d56e85f7a5e11e7d30b85afc116
languageName: node languageName: node
linkType: hard linkType: hard
@ -3486,7 +3519,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@floating-ui/utils@npm:^0.2.0": "@floating-ui/utils@npm:^0.2.1":
version: 0.2.1 version: 0.2.1
resolution: "@floating-ui/utils@npm:0.2.1" resolution: "@floating-ui/utils@npm:0.2.1"
checksum: 33c9ab346e7b05c5a1e6a95bc902aafcfc2c9d513a147e2491468843bd5607531b06d0b9aa56aa491cbf22a6c2495c18ccfc4c0344baec54a689a7bb8e4898d6 checksum: 33c9ab346e7b05c5a1e6a95bc902aafcfc2c9d513a147e2491468843bd5607531b06d0b9aa56aa491cbf22a6c2495c18ccfc4c0344baec54a689a7bb8e4898d6
@ -7220,6 +7253,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@tldraw/docs@workspace:apps/docs" resolution: "@tldraw/docs@workspace:apps/docs"
dependencies: dependencies:
"@ariakit/react": "npm:^0.4.1"
"@codesandbox/sandpack-react": "npm:^2.11.3" "@codesandbox/sandpack-react": "npm:^2.11.3"
"@microsoft/api-extractor-model": "npm:^7.26.4" "@microsoft/api-extractor-model": "npm:^7.26.4"
"@microsoft/tsdoc": "npm:^0.14.2" "@microsoft/tsdoc": "npm:^0.14.2"
@ -24611,6 +24645,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"use-sync-external-store@npm:^1.2.0":
version: 1.2.0
resolution: "use-sync-external-store@npm:1.2.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: a676216affc203876bd47981103f201f28c2731361bb186367e12d287a7566763213a8816910c6eb88265eccd4c230426eb783d64c373c4a180905be8820ed8e
languageName: node
linkType: hard
"user-home@npm:^2.0.0": "user-home@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "user-home@npm:2.0.0" resolution: "user-home@npm:2.0.0"