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 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,
}),

View file

@ -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,
}),

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 { 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 />

View file

@ -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>
)

View file

@ -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>

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)

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)
#### 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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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,
},
{

View file

@ -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",

View file

@ -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 --------------------- */

View file

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

View file

@ -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',
}

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
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"