snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
139
apps/docs/app/(docs)/[...slug]/page.tsx
Normal file
139
apps/docs/app/(docs)/[...slug]/page.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { Content } from '@/components/content'
|
||||
import { StarterKitEmbed } from '@/components/content/embed'
|
||||
import { Example } from '@/components/content/example'
|
||||
import { DocsFeedbackWidget } from '@/components/docs/docs-feedback-widget'
|
||||
import { DocsFooter } from '@/components/docs/docs-footer'
|
||||
import { DocsHeader } from '@/components/docs/docs-header'
|
||||
import { DocsMobileSidebar } from '@/components/docs/docs-mobile-sidebar'
|
||||
import { DocsSidebar } from '@/components/docs/docs-sidebar'
|
||||
import { DocsStarterSidebar } from '@/components/docs/docs-starter-sidebar'
|
||||
import { DocsTableOfContents } from '@/components/docs/docs-table-of-contents'
|
||||
import { SearchButton } from '@/components/search/SearchButton'
|
||||
import { cn } from '@/utils/cn'
|
||||
import { db } from '@/utils/ContentDatabase'
|
||||
import { parseMarkdown } from '@/utils/parse-markdown'
|
||||
import { canonicalDocsPageUrl } from '@/utils/sitemap-canonical-paths'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const paths = await db.getAllPaths()
|
||||
return paths.map((path) => ({ slug: path.replace(/^\//, '').split('/') }))
|
||||
}
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ slug: string | string[] }>
|
||||
}): Promise<Metadata> {
|
||||
const params = await props.params
|
||||
const path = typeof params.slug === 'string' ? [params.slug] : params.slug
|
||||
const pagePath = `/${path.join('/')}`
|
||||
const content = await db.getPageContent(pagePath)
|
||||
if (!content || content.type !== 'article') notFound()
|
||||
const metadata: Metadata = {
|
||||
title: content.article.title,
|
||||
alternates: { canonical: canonicalDocsPageUrl(pagePath) },
|
||||
}
|
||||
if (content.article.description) {
|
||||
metadata.description = content.article.description
|
||||
} else {
|
||||
const parsed = parseMarkdown(content.article.content ?? '', content.article.id)
|
||||
const initialContentText = parsed.initialContentText.trim()
|
||||
if (initialContentText) metadata.description = initialContentText
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
export default async function Page(props: { params: Promise<{ slug: string | string[] }> }) {
|
||||
const params = await props.params
|
||||
const path = typeof params.slug === 'string' ? [params.slug] : params.slug
|
||||
const content = await db.getPageContent(`/${path.join('/')}`)
|
||||
if (!content || content.type !== 'article') notFound()
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-screen-2xl mx-auto md:px-5 md:flex md:items-start md:pt-12 isolate">
|
||||
<DocsSidebar
|
||||
sectionId={content.article.sectionId}
|
||||
categoryId={content.article.categoryId}
|
||||
articleId={content.article.id}
|
||||
/>
|
||||
<div className="sticky top-14 z-10 flex items-center justify-between w-full h-12 px-5 bg-white border-b border-zinc-100 dark:border-zinc-800 dark:bg-zinc-950 backdrop-blur md:hidden">
|
||||
<DocsMobileSidebar
|
||||
sectionId={content.article.sectionId}
|
||||
categoryId={content.article.categoryId}
|
||||
articleId={content.article.id}
|
||||
/>
|
||||
<SearchButton type="docs" layout="mobile" className="hidden -mr-2 sm:block" />
|
||||
</div>
|
||||
{content.article.sectionId === 'examples' ? (
|
||||
<main className="relative w-full px-5 pt-12 shrink md:pt-0 min-w-[1px]">
|
||||
<DocsHeader article={content.article} />
|
||||
{content.article.description && (
|
||||
<Content mdx={content.article.description} type={content.article.sectionId} />
|
||||
)}
|
||||
<Example article={content.article} />
|
||||
{content.article.content && (
|
||||
<Content mdx={content.article.content} type={content.article.sectionId} />
|
||||
)}
|
||||
<div className="mx-auto w-full max-w-sm mt-8 mb-16">
|
||||
<DocsFeedbackWidget />
|
||||
</div>
|
||||
<DocsFooter article={content.article} />
|
||||
</main>
|
||||
) : content.article.sectionId === 'starter-kits' &&
|
||||
content.article.id !== 'starter-kits/starter-kits_ucg/overview' ? (
|
||||
<main className="relative w-full px-5 pt-12 shrink md:pt-0 min-w-[1px]">
|
||||
<DocsHeader article={content.article} />
|
||||
<div
|
||||
className={cn(
|
||||
'mb-8',
|
||||
'prose dark:prose-invert prose-sm prose-zinc text-zinc-800 dark:text-zinc-200 sm:prose-base w-full max-w-full',
|
||||
'prose-code:before:content-none prose-code:after:content-none prose-code:bg-zinc-100 dark:prose-code:!bg-zinc-800 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:font-normal',
|
||||
'prose-a:no-underline prose-a:text-blue-500 hover:prose-a:text-blue-600 dark:hover:prose-a:text-blue-400 prose-a:font-normal',
|
||||
'prose-blockquote:text-zinc-800 dark:prose-blockquote:text-zinc-200 prose-blockquote:font-normal prose-blockquote:border-none prose-blockquote:px-4 prose-blockquote:leading-normal prose-blockquote:bg-zinc-50 dark:prose-blockquote:bg-zinc-900 prose-blockquote:py-3 prose-blockquote:rounded-xl',
|
||||
'prose-table:bg-zinc-50 dark:prose-table:bg-zinc-900 prose-table:rounded-xl prose-table:text-sm',
|
||||
'prose-th:text-left prose-th:font-semibold prose-th:uppercase prose-th:text-xs prose-th:border-l prose-th:border-white dark:prose-th:border-zinc-950 prose-th:py-3 prose-th:px-4',
|
||||
'prose-tr:border-t prose-tr:border-white dark:prose-tr:border-zinc-950',
|
||||
'prose-td:border-l first:prose-td:border-l-0 prose-td:border-white dark:prose-td:border-zinc-950 prose-td:py-3 prose-td:px-4',
|
||||
'prose-hr:border-zinc-100 dark:prose-hr:border-zinc-800',
|
||||
'prose-h1:scroll-mt-20 prose-h2:scroll-mt-20 prose-h3:scroll-mt-20 prose-h4:scroll-mt-20 prose-h5:scroll-mt-20'
|
||||
)}
|
||||
>
|
||||
{content.article.description && (
|
||||
<Content
|
||||
mdx={content.article.description}
|
||||
type={content.article.sectionId}
|
||||
className="mb-6"
|
||||
/>
|
||||
)}
|
||||
{content.article.embed && <StarterKitEmbed id={content.article.embed} />}
|
||||
</div>
|
||||
<div className="flex flex-col xl:flex-row gap-6 relative">
|
||||
<DocsStarterSidebar article={content.article} />
|
||||
<Content mdx={content.article.content ?? ''} type={content.article.sectionId} />
|
||||
</div>
|
||||
<DocsFooter article={content.article} />
|
||||
</main>
|
||||
) : content.article.sectionId === 'starter-kits' ? (
|
||||
<main className="relative w-full px-5 pt-12 shrink md:pt-0 min-w-[1px]">
|
||||
<DocsHeader article={content.article} />
|
||||
<Content mdx={content.article.content ?? ''} type={content.article.sectionId} />
|
||||
<DocsFooter article={content.article} />
|
||||
<div className="mx-auto w-full max-w-sm mt-8 mb-16">
|
||||
<DocsFeedbackWidget />
|
||||
</div>
|
||||
</main>
|
||||
) : (
|
||||
<>
|
||||
<main className="relative grow px-5 pt-12 md:pr-0 lg:pl-12 xl:pr-12 md:pt-0 min-w-[1px]">
|
||||
<DocsHeader article={content.article} />
|
||||
<Content mdx={content.article.content ?? ''} type={content.article.sectionId} />
|
||||
<DocsFooter article={content.article} />
|
||||
</main>
|
||||
<DocsTableOfContents article={content.article} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
20
apps/docs/app/(docs)/layout.tsx
Normal file
20
apps/docs/app/(docs)/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Metadata } from 'next'
|
||||
import { DocsSearchBar } from '@/components/docs/docs-search-bar'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL('https://tldraw.dev'),
|
||||
title: {
|
||||
default: 'Docs',
|
||||
template: `%s • tldraw Docs`,
|
||||
},
|
||||
description: 'Documentation for the tldraw SDK.',
|
||||
}
|
||||
|
||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<DocsSearchBar />
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
31
apps/docs/app/.well-known/agent-skills/index.json/route.ts
Normal file
31
apps/docs/app/.well-known/agent-skills/index.json/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
AGENT_SKILLS_INDEX_SCHEMA,
|
||||
PUBLISHED_SKILLS,
|
||||
readPublishedSkill,
|
||||
} from '@/utils/agent-skills'
|
||||
|
||||
// Read at build time, so a skill whose frontmatter has gone missing fails the build rather than
|
||||
// serving a broken index.
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
export function GET() {
|
||||
const skills = Object.keys(PUBLISHED_SKILLS).map((key) => {
|
||||
const skill = readPublishedSkill(key as keyof typeof PUBLISHED_SKILLS)
|
||||
return {
|
||||
name: skill.name,
|
||||
// An archive rather than `skill-md`, because these skills run their own helper scripts.
|
||||
// See `SkillDocument.archive`.
|
||||
type: 'archive',
|
||||
description: skill.description,
|
||||
url: skill.url,
|
||||
digest: skill.digest,
|
||||
}
|
||||
})
|
||||
|
||||
return Response.json(
|
||||
{ $schema: AGENT_SKILLS_INDEX_SCHEMA, skills },
|
||||
// Readable cross-origin: an agent running in a browser is a normal consumer of this, and
|
||||
// everything here is already public.
|
||||
{ headers: { 'access-control-allow-origin': '*' } }
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { readPublishedSkill } from '@/utils/agent-skills'
|
||||
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
// The installable form of the skill: the whole directory, which the index publishes a digest over.
|
||||
// A client that unpacks this and one that reads the `SKILL.md` route next door see the same
|
||||
// instructions; only this one carries the helper scripts those instructions run.
|
||||
export function GET() {
|
||||
return new Response(new Uint8Array(readPublishedSkill('tldraw-migrate').archive), {
|
||||
headers: {
|
||||
'content-type': 'application/gzip',
|
||||
'access-control-allow-origin': '*',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { readPublishedSkill } from '@/utils/agent-skills'
|
||||
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
// Served verbatim from `skills/tldraw-migrate/SKILL.md` so the instructions can be read without
|
||||
// unpacking anything. The index points at the archive instead — this file alone cannot run.
|
||||
export function GET() {
|
||||
return new Response(readPublishedSkill('tldraw-migrate').markdown, {
|
||||
headers: {
|
||||
'content-type': 'text/markdown; charset=utf-8',
|
||||
'access-control-allow-origin': '*',
|
||||
},
|
||||
})
|
||||
}
|
||||
114
apps/docs/app/analytics.tsx
Normal file
114
apps/docs/app/analytics.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
'use client'
|
||||
|
||||
import Script from 'next/script'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Analytics() {
|
||||
useEffect(() => {
|
||||
window.TL_GA4_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA4_MEASUREMENT_ID
|
||||
window.TL_GOOGLE_ADS_ID = process.env.NEXT_PUBLIC_GOOGLE_ADS_ID
|
||||
window.TL_GTM_CONTAINER_ID = process.env.NEXT_PUBLIC_GTM_CONTAINER_ID
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleCopy = (copyEvent: ClipboardEvent) => {
|
||||
const isWithinCodeBlock = (copyEvent.target as HTMLElement | null)?.closest('pre, code')
|
||||
if (isWithinCodeBlock) {
|
||||
const copiedText = window.getSelection()?.toString() || ''
|
||||
|
||||
if (window.tlanalytics?.trackCopyCode) {
|
||||
window.tlanalytics.trackCopyCode({
|
||||
page_category: 'docs',
|
||||
text_snippet: copiedText,
|
||||
})
|
||||
}
|
||||
|
||||
// Track Google Ads conversion for code block copies
|
||||
if (window.tlanalytics?.gtag) {
|
||||
window.tlanalytics.gtag('event', 'conversion', {
|
||||
send_to: 'AW-17268182782/qIuDCMnhl_EaEP6djqpA',
|
||||
value: 1.0,
|
||||
currency: 'USD',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('copy', handleCopy)
|
||||
return () => {
|
||||
document.removeEventListener('copy', handleCopy)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const analyticsScriptSrc =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:5173/tl-analytics.js'
|
||||
: 'https://analytics.tldraw.com/tl-analytics.js'
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
id="tldraw-analytics"
|
||||
type="text/javascript"
|
||||
strategy="afterInteractive"
|
||||
async
|
||||
defer
|
||||
src={analyticsScriptSrc}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface ConsentPreferences {
|
||||
analytics: 'granted' | 'denied'
|
||||
marketing: 'granted' | 'denied'
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
tlanalytics: {
|
||||
openPrivacySettings(): void
|
||||
track(name: string, data?: { [key: string]: any }): void
|
||||
gtag(...args: any[]): void
|
||||
getConsentState(): ConsentPreferences
|
||||
onConsentUpdate(callback: (preferences: ConsentPreferences) => void): () => void
|
||||
trackCopyCode(data: {
|
||||
page_category: string
|
||||
text_snippet: string
|
||||
user_email?: string
|
||||
user_email_sha256?: string
|
||||
user_first_name?: string
|
||||
user_last_name?: string
|
||||
user_phone_number?: string
|
||||
}): void
|
||||
trackFormSubmission(data: {
|
||||
enquiry_type: string
|
||||
page_category?: string
|
||||
company_size?: string
|
||||
company_website?: string
|
||||
user_email?: string
|
||||
user_email_sha256?: string
|
||||
user_first_name?: string
|
||||
user_last_name?: string
|
||||
user_phone_number?: string
|
||||
}): void
|
||||
}
|
||||
TL_GA4_MEASUREMENT_ID: string | undefined
|
||||
TL_GOOGLE_ADS_ID?: string
|
||||
TL_GTM_CONTAINER_ID?: string
|
||||
posthog: any
|
||||
}
|
||||
}
|
||||
|
||||
export function PrivacySettingsLink() {
|
||||
const onCookieClick = () => window.tlanalytics.openPrivacySettings()
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={onCookieClick}>Privacy settings</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function track(name: string, data?: { [key: string]: any }) {
|
||||
window.tlanalytics?.track(name, data)
|
||||
}
|
||||
138
apps/docs/app/globals.css
Normal file
138
apps/docs/app/globals.css
Normal file
@@ -0,0 +1,138 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
.prose h2,
|
||||
.prose h3,
|
||||
.prose h4,
|
||||
.prose h5,
|
||||
.prose h6 {
|
||||
scroll-margin-top: 90px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.prose h2,
|
||||
.prose h3,
|
||||
.prose h4,
|
||||
.prose h5,
|
||||
.prose h6 {
|
||||
scroll-margin-top: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
.prose h2 a,
|
||||
.prose h3 a,
|
||||
.prose h4 a {
|
||||
@apply font-bold;
|
||||
}
|
||||
.prose blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
.prose blockquote p:before,
|
||||
.prose blockquote p:after {
|
||||
content: none;
|
||||
}
|
||||
.prose details.article__table-of-contents {
|
||||
display: none !important;
|
||||
}
|
||||
input[type='search']::-webkit-search-decoration,
|
||||
input[type='search']::-webkit-search-cancel-button,
|
||||
input[type='search']::-webkit-search-results-button,
|
||||
input[type='search']::-webkit-search-results-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.prose table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.prose td {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hs-form-field.hs-fieldtype-text label,
|
||||
.hs-form-field.hs-fieldtype-select label,
|
||||
.hs-form-field.hs-fieldtype-textarea label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* We don't show any descriptions or lables */
|
||||
.hs-field-desc {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Make text areas span both columns */
|
||||
div.hs-fieldtype-textarea {
|
||||
@apply sm:col-span-2;
|
||||
}
|
||||
|
||||
[data-hs-forms-root] form {
|
||||
@apply flex flex-col sm:grid-cols-2 sm:grid gap-4;
|
||||
}
|
||||
|
||||
[data-hs-forms-root] select {
|
||||
@apply w-full h-9 px-4 appearance-none rounded-lg text-white bg-zinc-700/50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:ring-offset-zinc-900;
|
||||
}
|
||||
[data-hs-forms-root] select.is-placeholder {
|
||||
@apply text-white/50;
|
||||
}
|
||||
|
||||
[data-hs-forms-root] input {
|
||||
@apply w-full h-9 px-4 rounded-lg text-white bg-zinc-700/50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:ring-offset-zinc-900;
|
||||
}
|
||||
|
||||
[data-hs-forms-root] textarea {
|
||||
@apply -mb-1.5 w-full px-4 py-2 rounded-lg resize-none text-white bg-zinc-700/50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:ring-offset-zinc-900;
|
||||
height: 112px;
|
||||
}
|
||||
|
||||
.hs-submit {
|
||||
@apply overflow-hidden items-center col-start-1 col-span-2 p-2;
|
||||
}
|
||||
|
||||
.hs-submit input {
|
||||
@apply overflow-hidden items-center h-9 rounded-lg text-base !bg-blue-500 !text-white hover:!bg-blue-600 dark:hover:!bg-blue-400;
|
||||
}
|
||||
|
||||
.hs_error_rollup {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.masked-logo {
|
||||
position: relative;
|
||||
background-color: currentColor;
|
||||
z-index: 1;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
mark {
|
||||
margin: -1px -1px;
|
||||
padding: 1px 1px;
|
||||
border-radius: 3px;
|
||||
background-color: #ffc80046;
|
||||
color: inherit;
|
||||
-webkit-box-decoration-break: clone;
|
||||
box-decoration-break: clone;
|
||||
}
|
||||
|
||||
.special {
|
||||
--r: 18px;
|
||||
--rl: -1;
|
||||
--b: 4px;
|
||||
background: none;
|
||||
text-shadow:
|
||||
calc(var(--r) * 1) 0px var(--b) rgba(255, 208, 0, 0.03),
|
||||
calc(var(--r) * 0.75) 0px var(--b) rgba(255, 140, 0, 0.04),
|
||||
calc(var(--r) * 0.5) 0px var(--b) rgba(255, 187, 0, 0.09),
|
||||
calc(var(--r) * 0.25) 0px var(--b) rgba(255, 187, 0, 0.12),
|
||||
calc(var(--r) * 0.12) 0px var(--b) rgba(255, 217, 0, 0.32),
|
||||
calc(var(--r) * 1 * var(--rl)) 0px rgba(255, 140, 0, 0.02),
|
||||
calc(var(--r) * 0.75 * var(--rl)) 0px var(--b) rgba(255, 208, 0, 0.04),
|
||||
calc(var(--r) * 0.5 * var(--rl)) 0px var(--b) rgba(255, 187, 0, 0.08),
|
||||
calc(var(--r) * 0.25 * var(--rl)) 0px var(--b) rgba(255, 187, 0, 0.12),
|
||||
calc(var(--r) * 0.12 * var(--rl)) 0px var(--b) rgba(255, 217, 0, 0.32);
|
||||
}
|
||||
102
apps/docs/app/layout.tsx
Normal file
102
apps/docs/app/layout.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { GeistSans } from 'geist/font/sans'
|
||||
import { Metadata, Viewport } from 'next'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import localFont from 'next/font/local'
|
||||
import AutoRefresh from '@/components/common/autorefresh'
|
||||
import { Footer } from '@/components/navigation/footer'
|
||||
import { Header } from '@/components/navigation/header'
|
||||
import { assetUrl } from '@/utils/asset-url'
|
||||
import { cn } from '@/utils/cn'
|
||||
import Analytics from './analytics'
|
||||
import './globals.css'
|
||||
|
||||
function Wrapper({ children }: { children: React.ReactNode }) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return <AutoRefresh>{children}</AutoRefresh>
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL('https://tldraw.dev'),
|
||||
title: {
|
||||
default: 'tldraw: Build whiteboards in React with the tldraw SDK',
|
||||
template: `%s • tldraw`,
|
||||
},
|
||||
description:
|
||||
'The tldraw SDK provides tools, services, and APIs to build beautiful whiteboards and infinite canvas applications with real-time collaboration and a powerful React-based canvas.',
|
||||
twitter: {
|
||||
creator: '@tldraw',
|
||||
},
|
||||
applicationName: 'tldraw SDK',
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
title: 'tldraw SDK',
|
||||
statusBarStyle: 'black',
|
||||
},
|
||||
icons: [
|
||||
{ rel: 'shortcut icon', url: assetUrl('/favicon.svg') },
|
||||
{ rel: 'icon', url: assetUrl('/favicon-32x32.svg'), sizes: '32x32' },
|
||||
{ rel: 'icon', url: assetUrl('/favicon-16x16.svg'), sizes: '16x16' },
|
||||
{ rel: 'apple-touch-icon', url: assetUrl('/apple-touch-icon.png') },
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
url: assetUrl('/apple-touch-icon-152x152.svg'),
|
||||
sizes: '152x152',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
url: assetUrl('/apple-touch-icon-180x180.svg'),
|
||||
sizes: '180x180',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
url: assetUrl('/apple-touch-icon-167x167.svg'),
|
||||
sizes: '167x167',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
width: 'device-width',
|
||||
height: 'device-height',
|
||||
themeColor: '#ffffff',
|
||||
}
|
||||
|
||||
const ShantellSans = localFont({
|
||||
src: './shantell-sans.woff2',
|
||||
weight: '400',
|
||||
display: 'swap',
|
||||
variable: '--font-shantell-sans',
|
||||
})
|
||||
|
||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<html
|
||||
lang="en"
|
||||
className={cn(GeistSans.variable, ShantellSans.variable, 'font-sans antialiased')}
|
||||
>
|
||||
<body className="overflow-x-hidden bg-white text-zinc-800 dark:bg-zinc-950 dark:text-zinc-300">
|
||||
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}>
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
<Analytics />
|
||||
</ThemeProvider>
|
||||
<noscript>
|
||||
<iframe
|
||||
src="https://www.googletagmanager.com/ns.html?id=GTM-KMMLPMMN"
|
||||
height="0"
|
||||
width="0"
|
||||
style={{ display: 'none', visibility: 'hidden' }}
|
||||
/>
|
||||
</noscript>
|
||||
</body>
|
||||
</html>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
72
apps/docs/app/not-found.tsx
Normal file
72
apps/docs/app/not-found.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
/* eslint-disable react/no-unescaped-entities */
|
||||
'use client'
|
||||
|
||||
import { RocketLaunchIcon } from '@heroicons/react/16/solid'
|
||||
import { AcademicCapIcon, BookOpenIcon, NewspaperIcon, PlayIcon } from '@heroicons/react/20/solid'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { DiscordIcon } from '@/components/common/icon/discord'
|
||||
import { DocsSearchBar } from '@/components/docs/docs-search-bar'
|
||||
import { NavigationLink } from '@/components/navigation/link'
|
||||
import Handwritten404 from '@/public/images/404.svg'
|
||||
|
||||
const links = [
|
||||
{
|
||||
caption: 'Quick start',
|
||||
icon: RocketLaunchIcon,
|
||||
href: '/quick-start',
|
||||
},
|
||||
{
|
||||
caption: 'Guides',
|
||||
icon: AcademicCapIcon,
|
||||
href: '/docs/editor',
|
||||
},
|
||||
{
|
||||
caption: 'Reference',
|
||||
icon: BookOpenIcon,
|
||||
href: '/reference/editor/Editor',
|
||||
},
|
||||
{
|
||||
caption: 'Examples',
|
||||
icon: PlayIcon,
|
||||
href: '/examples/basic',
|
||||
},
|
||||
{
|
||||
caption: 'Blog',
|
||||
icon: NewspaperIcon,
|
||||
href: '/blog',
|
||||
},
|
||||
]
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<DocsSearchBar />
|
||||
<section className="flex flex-col items-center justify-center py-16 md:py-32 px-5">
|
||||
<Image src={Handwritten404} alt="404 - Page not found" className="w-64" />
|
||||
<h1 className="font-hand text-black dark:text-white text-2xl">page not found :(</h1>
|
||||
<p className="mt-8 max-w-md text-center text-balance">
|
||||
There's nothing here. You may want to look at these pages to find what you're looking for:
|
||||
</p>
|
||||
<ul className="mt-8 flex flex-wrap justify-center gap-8">
|
||||
{links.map((item, index) => (
|
||||
<li key={index}>
|
||||
<NavigationLink {...item} active={false} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="mt-32 max-w-md text-center text-balance text-sm">
|
||||
You can always ask for help in our welcoming community on{' '}
|
||||
<Link
|
||||
href="https://discord.tldraw.com/?utm_source=docs&utm_medium=organic&utm_campaign=sociallink"
|
||||
className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>
|
||||
<DiscordIcon className="h-3.5 mb-0.5 ml-0.5 inline" />
|
||||
<span> Discord</span>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
4
apps/docs/app/robots.txt
Normal file
4
apps/docs/app/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://www.tldraw.dev/sitemap.xml
|
||||
14
apps/docs/app/search/layout.tsx
Normal file
14
apps/docs/app/search/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL('https://tldraw.dev'),
|
||||
title: {
|
||||
default: 'Search',
|
||||
template: `%s • tldraw SDK`,
|
||||
},
|
||||
description: 'Documentation for the tldraw SDK.',
|
||||
}
|
||||
|
||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children
|
||||
}
|
||||
19
apps/docs/app/search/page.tsx
Normal file
19
apps/docs/app/search/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { DocsMobileSidebar } from '@/components/docs/docs-mobile-sidebar'
|
||||
import { DocsSidebar } from '@/components/docs/docs-sidebar'
|
||||
import { FullPageSearch } from '@/components/search/FullPageSearch'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="w-full max-w-screen-2xl mx-auto md:px-5 md:flex md:pt-8 isolate md:isolation-auto">
|
||||
<DocsSidebar />
|
||||
<div className="sticky top-14 z-10 flex items-center justify-between w-full h-12 px-5 bg-white border-b border-zinc-100 dark:border-zinc-800 dark:bg-zinc-950 backdrop-blur md:hidden">
|
||||
<DocsMobileSidebar />
|
||||
</div>
|
||||
<main className="relative w-full max-w-3xl px-5 pt-7 shrink md:pr-0 lg:pl-12 xl:pr-12 md:pt-0 min-w-[1px]">
|
||||
<FullPageSearch indexName="docs" />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
20
apps/docs/app/sitemap.ts
Normal file
20
apps/docs/app/sitemap.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MetadataRoute } from 'next'
|
||||
import { db } from '@/utils/ContentDatabase'
|
||||
import { canonicalizeDocsSitemapPaths } from '@/utils/sitemap-canonical-paths'
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const paths = canonicalizeDocsSitemapPaths(await db.getAllPaths())
|
||||
|
||||
// Docs-only sitemap. Marketing pages are now owned by the dotdev app.
|
||||
const docsSitemap: MetadataRoute.Sitemap = [
|
||||
{
|
||||
url: 'https://tldraw.dev/',
|
||||
lastModified: new Date(),
|
||||
},
|
||||
...paths.map((path: string) => ({
|
||||
url: 'https://tldraw.dev' + path,
|
||||
lastModified: new Date(),
|
||||
})),
|
||||
]
|
||||
return docsSitemap
|
||||
}
|
||||
Reference in New Issue
Block a user