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:
parent
591d3129c7
commit
157d24db73
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
|
@ -2,11 +2,13 @@ import { SearchResult } from '@/types/search-types'
|
|||
import { getDb } from '@/utils/ContentDatabase'
|
||||
import assert from 'assert'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
|
||||
|
||||
type Data = {
|
||||
results: {
|
||||
articles: SearchResult[]
|
||||
apiDocs: SearchResult[]
|
||||
examples: SearchResult[]
|
||||
}
|
||||
status: 'success' | 'error' | 'no-query'
|
||||
}
|
||||
|
@ -16,13 +18,11 @@ const BANNED_HEADINGS = ['new', 'constructor', 'properties', 'example', 'methods
|
|||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const query = searchParams.get('q')?.toLowerCase()
|
||||
|
||||
if (!query) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
results: {
|
||||
articles: [],
|
||||
apiDocs: [],
|
||||
},
|
||||
results: structuredClone(SEARCH_RESULTS),
|
||||
status: 'error',
|
||||
error: 'No query',
|
||||
}),
|
||||
|
@ -33,10 +33,7 @@ export async function GET(req: NextRequest) {
|
|||
}
|
||||
|
||||
try {
|
||||
const results: Data['results'] = {
|
||||
articles: [],
|
||||
apiDocs: [],
|
||||
}
|
||||
const results: Data['results'] = structuredClone(SEARCH_RESULTS)
|
||||
const db = await getDb()
|
||||
|
||||
const getVectorDb = (await import('@/utils/ContentVectorDatabase')).getVectorDb
|
||||
|
@ -44,9 +41,11 @@ export async function GET(req: NextRequest) {
|
|||
const vdb = await getVectorDb()
|
||||
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 article = await db.db.get(
|
||||
`SELECT id, title, description, categoryId, sectionId, keywords FROM articles WHERE 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)
|
||||
assert(heading, `No heading found for ${result.id} ${result.slug}`)
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
article,
|
||||
|
@ -72,18 +72,23 @@ export async function GET(req: NextRequest) {
|
|||
}
|
||||
})
|
||||
)
|
||||
|
||||
const visited = new Set<string>()
|
||||
for (const result of headings) {
|
||||
if (!result) continue
|
||||
if (visited.has(result.id)) continue
|
||||
|
||||
visited.add(result.id)
|
||||
const { category, section, article, heading, score } = result
|
||||
const isUncategorized = category.id === section.id + '_ucg'
|
||||
|
||||
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,
|
||||
type: 'heading',
|
||||
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
|
||||
sectionType: sectionTypeBucket(section.id),
|
||||
title:
|
||||
section.id === 'reference'
|
||||
? article.title + '.' + heading.title
|
||||
|
@ -94,6 +99,7 @@ export async function GET(req: NextRequest) {
|
|||
score,
|
||||
})
|
||||
}
|
||||
|
||||
const articles = await Promise.all(
|
||||
queryResults.map(async (result) => ({
|
||||
score: result.score,
|
||||
|
@ -103,8 +109,10 @@ export async function GET(req: NextRequest) {
|
|||
),
|
||||
}))
|
||||
)
|
||||
|
||||
for (const { score, article } of articles.filter(Boolean)) {
|
||||
if (visited.has(article.id)) continue
|
||||
|
||||
visited.add(article.id)
|
||||
const category = await db.db.get(
|
||||
`SELECT id, title FROM categories WHERE categories.id = ?`,
|
||||
|
@ -115,10 +123,12 @@ export async function GET(req: NextRequest) {
|
|||
article.sectionId
|
||||
)
|
||||
const isUncategorized = category.id === section.id + '_ucg'
|
||||
results[section.id === 'reference' ? 'apiDocs' : 'articles'].push({
|
||||
|
||||
results[searchBucket(section.id)].push({
|
||||
id: article.id,
|
||||
type: 'article',
|
||||
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
|
||||
sectionType: sectionTypeBucket(section.id),
|
||||
title: article.title,
|
||||
url: isUncategorized
|
||||
? `${section.id}/${article.id}`
|
||||
|
@ -126,23 +136,18 @@ export async function GET(req: NextRequest) {
|
|||
score,
|
||||
})
|
||||
}
|
||||
const apiDocsScores = results.apiDocs.map((a) => a.score)
|
||||
const maxScoreApiDocs = Math.max(...apiDocsScores)
|
||||
const minScoreApiDocs = Math.min(...apiDocsScores)
|
||||
const apiDocsBottom = minScoreApiDocs + (maxScoreApiDocs - minScoreApiDocs) * 0.75
|
||||
results.apiDocs
|
||||
.filter((a) => a.score > apiDocsBottom)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.sort((a, b) => (b.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1))
|
||||
.slice(0, 10)
|
||||
const articleScores = results.articles.map((a) => a.score)
|
||||
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))
|
||||
|
||||
Object.keys(results).forEach((section: string) => {
|
||||
const scores = results[section as keyof Data['results']].map((a) => a.score)
|
||||
const maxScore = Math.max(...scores)
|
||||
const minScore = Math.min(...scores)
|
||||
const bottomScore = minScore + (maxScore - minScore) * (section === 'apiDocs' ? 0.75 : 0.5)
|
||||
results[section as keyof Data['results']]
|
||||
.filter((a) => a.score > bottomScore)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.sort((a, b) => (b.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1))
|
||||
})
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
results,
|
||||
|
@ -155,10 +160,7 @@ export async function GET(req: NextRequest) {
|
|||
} catch (e: any) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
results: {
|
||||
articles: [],
|
||||
apiDocs: [],
|
||||
},
|
||||
results: structuredClone(SEARCH_RESULTS),
|
||||
status: 'error',
|
||||
error: e.message,
|
||||
}),
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
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'
|
||||
|
||||
type Data = {
|
||||
results: {
|
||||
articles: SearchResult[]
|
||||
apiDocs: SearchResult[]
|
||||
examples: SearchResult[]
|
||||
}
|
||||
status: 'success' | 'error' | 'no-query'
|
||||
}
|
||||
|
@ -23,10 +25,7 @@ export async function GET(req: NextRequest) {
|
|||
if (!query) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
results: {
|
||||
articles: [],
|
||||
apiDocs: [],
|
||||
},
|
||||
results: structuredClone(SEARCH_RESULTS),
|
||||
status: 'no-query',
|
||||
}),
|
||||
{
|
||||
|
@ -36,15 +35,9 @@ export async function GET(req: NextRequest) {
|
|||
}
|
||||
|
||||
try {
|
||||
const results: Data['results'] = {
|
||||
articles: [],
|
||||
apiDocs: [],
|
||||
}
|
||||
|
||||
const results: Data['results'] = structuredClone(SEARCH_RESULTS)
|
||||
const db = await getDb()
|
||||
|
||||
const queryWithoutSpaces = query.replace(/\s/g, '')
|
||||
|
||||
const searchForArticle = await db.db.prepare(
|
||||
`
|
||||
SELECT id, title, sectionId, categoryId, content
|
||||
|
@ -61,16 +54,16 @@ export async function GET(req: NextRequest) {
|
|||
|
||||
await searchForArticle.all(query).then(async (queryResults) => {
|
||||
for (const article of queryResults) {
|
||||
const isApiDoc = article.sectionId === 'reference'
|
||||
const section = await db.getSection(article.sectionId)
|
||||
const category = await db.getCategory(article.categoryId)
|
||||
const isUncategorized = category.id === section.id + '_ucg'
|
||||
|
||||
results[isApiDoc ? 'apiDocs' : 'articles'].push({
|
||||
results[searchBucket(article.sectionId)].push({
|
||||
id: article.id,
|
||||
type: 'article',
|
||||
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
|
||||
title: article.title,
|
||||
sectionType: sectionTypeBucket(section.id),
|
||||
url: isUncategorized
|
||||
? `${section.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) => {
|
||||
for (const heading of queryResults) {
|
||||
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 category = await db.getCategory(article.categoryId)
|
||||
const isUncategorized = category.id === section.id + '_ucg'
|
||||
|
||||
results[isApiDoc ? 'apiDocs' : 'articles'].push({
|
||||
results[searchBucket(article.sectionId)].push({
|
||||
id: article.id + '#' + heading.slug,
|
||||
type: 'heading',
|
||||
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
|
||||
sectionType: sectionTypeBucket(section.id),
|
||||
title:
|
||||
section.id === 'reference'
|
||||
? article.title + '.' + heading.title
|
||||
|
@ -116,8 +109,8 @@ export async function GET(req: NextRequest) {
|
|||
}
|
||||
})
|
||||
|
||||
results.apiDocs.sort((a, b) => b.score - a.score)
|
||||
results.articles.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.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1)
|
||||
)
|
||||
|
@ -128,10 +121,7 @@ export async function GET(req: NextRequest) {
|
|||
} catch (e: any) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
results: {
|
||||
articles: [],
|
||||
apiDocs: [],
|
||||
},
|
||||
results: structuredClone(SEARCH_RESULTS),
|
||||
status: 'error',
|
||||
error: e.message,
|
||||
}),
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
193
apps/docs/components/Autocomplete.css
Normal file
193
apps/docs/components/Autocomplete.css
Normal 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%);
|
||||
}
|
123
apps/docs/components/Autocomplete.tsx
Normal file
123
apps/docs/components/Autocomplete.tsx
Normal 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
|
|
@ -7,15 +7,7 @@ import { Chevron } from './Icons'
|
|||
import { Search } from './Search'
|
||||
import { ThemeSwitcher } from './ThemeSwitcher'
|
||||
|
||||
export function Header({
|
||||
searchQuery,
|
||||
searchType,
|
||||
sectionId,
|
||||
}: {
|
||||
searchQuery?: string
|
||||
searchType?: string
|
||||
sectionId?: string
|
||||
}) {
|
||||
export function Header({ sectionId }: { sectionId?: string }) {
|
||||
return (
|
||||
<div className="layout__header">
|
||||
<Link href="/quick-start">
|
||||
|
@ -27,7 +19,7 @@ export function Header({
|
|||
}}
|
||||
/>
|
||||
</Link>
|
||||
<Search prevQuery={searchQuery} prevType={searchType} />
|
||||
<Search />
|
||||
<div className="layout__header__sections_and_socials">
|
||||
<SectionLinks sectionId={sectionId} />
|
||||
<ThemeSwitcher />
|
||||
|
|
|
@ -1,77 +1,162 @@
|
|||
'use client'
|
||||
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { SEARCH_TYPE, SearchResult } from '@/types/search-types'
|
||||
import { debounce } from '@/utils/debounce'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { Icon } from './Icon'
|
||||
import Autocomplete, { DropdownOption } from './Autocomplete'
|
||||
|
||||
export function Search({
|
||||
prevType = 'n',
|
||||
prevQuery = '',
|
||||
}: {
|
||||
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)
|
||||
}, [])
|
||||
const HOST_URL =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:3001'
|
||||
: process.env.NEXT_PUBLIC_SITE_URL ?? 'https://www.tldraw.dev'
|
||||
|
||||
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 pathName = usePathname()
|
||||
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) => {
|
||||
e.preventDefault()
|
||||
rInput.current?.focus()
|
||||
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 (
|
||||
<div className="search__wrapper">
|
||||
<div className="search">
|
||||
<Icon className="search__icon" icon="search" small />
|
||||
<input
|
||||
ref={rInput}
|
||||
type="text"
|
||||
className="search__input"
|
||||
placeholder="Search..."
|
||||
value={query}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
<Autocomplete
|
||||
ref={rInput}
|
||||
customUI={
|
||||
<button className="search__ai-toggle" onClick={handleSearchTypeChange}>
|
||||
{searchType === SEARCH_TYPE.NORMAL ? '✨ Search using AI' : '⭐ Search without AI'}
|
||||
</button>
|
||||
}
|
||||
groups={['examples', 'docs', 'reference']}
|
||||
groupsToLabel={{ examples: 'Examples', docs: 'Articles', reference: 'Reference' }}
|
||||
groupsToIcon={{ examples: CodeIcon, docs: DocIcon, reference: ReferenceIcon }}
|
||||
options={searchResults}
|
||||
isLoading={isLoading}
|
||||
onInputChange={handleInputChange}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{platform && (
|
||||
<span className="search__keyboard">
|
||||
{platform === 'mac' && <kbd data-platform="mac">⌘</kbd>}
|
||||
{platform === 'nonMac' && <kbd data-platform="win">Ctrl</kbd>}
|
||||
<kbd>K</kbd>
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
|
|
|
@ -28,15 +28,7 @@ const linkContext = createContext<{
|
|||
sectionId: string | null
|
||||
} | null>(null)
|
||||
|
||||
export function Sidebar({
|
||||
headings,
|
||||
links,
|
||||
sectionId,
|
||||
categoryId,
|
||||
articleId,
|
||||
searchQuery,
|
||||
searchType,
|
||||
}: SidebarProps) {
|
||||
export function Sidebar({ headings, links, sectionId, categoryId, articleId }: SidebarProps) {
|
||||
const activeId = articleId ?? categoryId ?? sectionId
|
||||
|
||||
const pathName = usePathname()
|
||||
|
@ -45,17 +37,13 @@ export function Sidebar({
|
|||
document.body.classList.remove('sidebar-open')
|
||||
|
||||
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])
|
||||
|
||||
return (
|
||||
<>
|
||||
<linkContext.Provider value={{ activeId, articleId, categoryId, sectionId }}>
|
||||
<div className="sidebar" onScroll={(e) => e.stopPropagation()}>
|
||||
<Search prevQuery={searchQuery} prevType={searchType} />
|
||||
<Search />
|
||||
<div className="sidebar__section__links">
|
||||
<SectionLinks sectionId={sectionId} />
|
||||
</div>
|
||||
|
|
20
apps/docs/components/Spinner.tsx
Normal file
20
apps/docs/components/Spinner.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -8,4 +8,3 @@ status: published
|
|||
---
|
||||
|
||||
[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
|
@ -9,7 +9,6 @@ status: published
|
|||
|
||||
[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 the styles panel button for laser tool on mobile.
|
||||
|
@ -112,4 +111,4 @@ status: published
|
|||
- David Sheldrick ([@ds300](https://github.com/ds300))
|
||||
- Lu Wilson ([@TodePond](https://github.com/TodePond))
|
||||
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
|
||||
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
|
||||
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
|
||||
|
|
|
@ -9,7 +9,6 @@ status: published
|
|||
|
||||
[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))
|
||||
|
||||
- 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
|
||||
|
||||
<img 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" />
|
||||
<img
|
||||
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))
|
||||
|
||||
|
@ -49,8 +54,14 @@ Before/After
|
|||
|
||||
Before/After
|
||||
|
||||
<image 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" />
|
||||
<image
|
||||
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))
|
||||
|
||||
|
@ -71,8 +82,14 @@ Before/After
|
|||
|
||||
Before & After:
|
||||
|
||||
<image 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" />
|
||||
<image
|
||||
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))
|
||||
|
||||
|
@ -88,7 +105,7 @@ Before & After:
|
|||
|
||||
#### 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))
|
||||
|
||||
|
@ -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))
|
||||
|
||||
- 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))
|
||||
|
||||
|
@ -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`
|
||||
- Migrate snapshot [#1843](https://github.com/tldraw/tldraw/pull/1843) ([@steveruizok](https://github.com/steveruizok))
|
||||
- `@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] 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`
|
||||
- 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))
|
||||
|
@ -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))
|
||||
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
|
||||
- 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))
|
||||
|
|
|
@ -9,7 +9,6 @@ status: published
|
|||
|
||||
[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))
|
||||
|
||||
- Fixes opacity of shapes while erasing in a group or frame.
|
||||
|
@ -24,9 +23,15 @@ status: published
|
|||
|
||||
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))
|
||||
|
||||
|
@ -97,4 +102,4 @@ Before/after:
|
|||
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
|
||||
- Prince Mendiratta ([@Prince-Mendiratta](https://github.com/Prince-Mendiratta))
|
||||
- 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))
|
||||
|
|
|
@ -9,7 +9,6 @@ status: published
|
|||
|
||||
[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 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))
|
||||
|
||||
- locked shape of opacity problem with eraser.pointing
|
||||
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)
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
|
@ -107,4 +106,4 @@ Before/after:
|
|||
- Lu Wilson ([@TodePond](https://github.com/TodePond))
|
||||
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
|
||||
- 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))
|
||||
|
|
|
@ -9,7 +9,6 @@ status: published
|
|||
|
||||
[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))
|
||||
|
||||
- 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))
|
||||
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
|
||||
- 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))
|
||||
|
|
|
@ -9,7 +9,6 @@ status: published
|
|||
|
||||
[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))
|
||||
|
||||
- Make sure affected shapes are visible after undo/redo
|
||||
|
@ -248,4 +247,4 @@ status: published
|
|||
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
|
||||
- Steve Ruiz ([@steveruizok](https://github.com/steveruizok))
|
||||
- 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))
|
||||
|
|
|
@ -9,7 +9,6 @@ status: published
|
|||
|
||||
[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 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))
|
||||
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
|
||||
- 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))
|
||||
|
|
|
@ -9,7 +9,6 @@ status: published
|
|||
|
||||
[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))
|
||||
|
||||
- Fixes url validations.
|
||||
|
@ -164,4 +163,4 @@ status: published
|
|||
- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek))
|
||||
- Stan Flint ([@StanFlint](https://github.com/StanFlint))
|
||||
- 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))
|
||||
|
|
|
@ -34,7 +34,13 @@ const nextConfig = {
|
|||
{
|
||||
// For reverse compatibility with old links
|
||||
source: '/docs/usage',
|
||||
destination: '/usage',
|
||||
destination: '/installation',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
// For reverse compatibility with old links
|
||||
source: '/usage',
|
||||
destination: '/installation',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
"watch-content": "tsx ./watcher.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.4.1",
|
||||
"@codesandbox/sandpack-react": "^2.11.3",
|
||||
"@microsoft/api-extractor-model": "^7.26.4",
|
||||
"@microsoft/tsdoc": "^0.14.2",
|
||||
|
|
|
@ -959,157 +959,52 @@ body {
|
|||
box-shadow: var(--shadow-small);
|
||||
}
|
||||
|
||||
/* --------------------- Search --------------------- */
|
||||
|
||||
.sidebar .search__wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search__wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.search {
|
||||
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;
|
||||
top: 50%;
|
||||
right: 12px;
|
||||
z-index: 2;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-tint-5);
|
||||
left: 0px;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
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);
|
||||
}
|
||||
|
||||
.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;
|
||||
background-color: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
margin: 4px 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* --------------------- Desktop M --------------------- */
|
||||
|
|
|
@ -185,8 +185,6 @@ export type SidebarContentList = {
|
|||
articleId: string | null
|
||||
links: SidebarContentLink[]
|
||||
activeId?: string | null
|
||||
searchQuery?: string
|
||||
searchType?: string
|
||||
}
|
||||
|
||||
/* ---------- Finished / generated content ---------- */
|
||||
|
|
|
@ -2,7 +2,13 @@ export type SearchResult = {
|
|||
type: 'article' | 'category' | 'section' | 'heading'
|
||||
id: string
|
||||
subtitle: string
|
||||
sectionType: string
|
||||
title: string
|
||||
url: string
|
||||
score: number
|
||||
}
|
||||
|
||||
export enum SEARCH_TYPE {
|
||||
AI = 'ai',
|
||||
NORMAL = 'n',
|
||||
}
|
||||
|
|
9
apps/docs/utils/search-api.ts
Normal file
9
apps/docs/utils/search-api.ts
Normal 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'
|
67
yarn.lock
67
yarn.lock
|
@ -51,6 +51,39 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 10.46.0
|
||||
resolution: "@auto-it/bot-list@npm:10.46.0"
|
||||
|
@ -3455,22 +3488,22 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@floating-ui/core@npm:^1.5.3":
|
||||
version: 1.5.3
|
||||
resolution: "@floating-ui/core@npm:1.5.3"
|
||||
"@floating-ui/core@npm:^1.6.0":
|
||||
version: 1.6.0
|
||||
resolution: "@floating-ui/core@npm:1.6.0"
|
||||
dependencies:
|
||||
"@floating-ui/utils": "npm:^0.2.0"
|
||||
checksum: 7d9feaca2565a2a71bf03d23cd292c03def63097d7fde7d62909cdb8ddb84664781f3922086bcf10443f3310cb92381a0ecf745b2774edb917fa74fe61015c56
|
||||
"@floating-ui/utils": "npm:^0.2.1"
|
||||
checksum: d6a47cacde193cd8ccb4c268b91ccc4ca254dffaec6242b07fd9bcde526044cc976d27933a7917f9a671de0a0e27f8d358f46400677dbd0c8199de293e9746e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@floating-ui/dom@npm:^1.5.4":
|
||||
version: 1.5.4
|
||||
resolution: "@floating-ui/dom@npm:1.5.4"
|
||||
"@floating-ui/dom@npm:^1.0.0, @floating-ui/dom@npm:^1.5.4":
|
||||
version: 1.6.1
|
||||
resolution: "@floating-ui/dom@npm:1.6.1"
|
||||
dependencies:
|
||||
"@floating-ui/core": "npm:^1.5.3"
|
||||
"@floating-ui/utils": "npm:^0.2.0"
|
||||
checksum: 3ba02ba2b4227c1e18df6ccdd029a1c100058db2e76ca1dac60a593ec72b2d4d995fa5c2d1639a5c38adb17e12398fbfe4f6cf5fd45f2ee6170ed0cf64acea06
|
||||
"@floating-ui/core": "npm:^1.6.0"
|
||||
"@floating-ui/utils": "npm:^0.2.1"
|
||||
checksum: c010feb55be37662eb4cc8d0a22e21359c25247bbdcd9557617fd305cf08c8f020435b17e4b4f410201ba9abe3a0dd96b5c42d56e85f7a5e11e7d30b85afc116
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -3486,7 +3519,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@floating-ui/utils@npm:^0.2.0":
|
||||
"@floating-ui/utils@npm:^0.2.1":
|
||||
version: 0.2.1
|
||||
resolution: "@floating-ui/utils@npm:0.2.1"
|
||||
checksum: 33c9ab346e7b05c5a1e6a95bc902aafcfc2c9d513a147e2491468843bd5607531b06d0b9aa56aa491cbf22a6c2495c18ccfc4c0344baec54a689a7bb8e4898d6
|
||||
|
@ -7220,6 +7253,7 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "@tldraw/docs@workspace:apps/docs"
|
||||
dependencies:
|
||||
"@ariakit/react": "npm:^0.4.1"
|
||||
"@codesandbox/sandpack-react": "npm:^2.11.3"
|
||||
"@microsoft/api-extractor-model": "npm:^7.26.4"
|
||||
"@microsoft/tsdoc": "npm:^0.14.2"
|
||||
|
@ -24611,6 +24645,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.0.0
|
||||
resolution: "user-home@npm:2.0.0"
|
||||
|
|
Loading…
Reference in a new issue