[automated] Update standalone examples from v2.1.3

huppy-bot[bot] 2024-04-25 14:50:25 +00:00
rodzic 911fd9d14b
commit 27a68808e5
2026 zmienionych plików z 61 dodań i 445147 usunięć

Wyświetl plik

@ -1,75 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
**/node_modules
.git
**/.git
dist
dist-cjs
dist-esm
.tsbuild*
.lazy
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# turborepo
.turbo
coverage
**/*.env
**/*.tsbuildinfo
**/*.css.map
**/*.js.map
apps/webdriver/www/index.js
apps/webdriver/www/index.css
apps/dotcom-worker/.dev.vars
nohup.out
packages/*/package
packages/*/*.tgz
tsconfig.build.json
.vercel
api-json
api-md
apps/webdriver/www
!apps/webdriver/www/index.html
# yarn v2
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
packages/*/api
apps/examples/www/index.css
apps/examples/www/index.js
.tsbuild
packages/dotcom-worker/.dev.vars

Wyświetl plik

@ -1,33 +0,0 @@
**/node_modules/*
**/out/*
**/dist/*
**/dist-cjs/*
**/dist-esm/*
**/.tsbuild*
**/.next/*
*.md
**/_archive/*
**/*.css.map
**/*.js.map
**/*.d.ts
**/api/*
!**/pages/api/*
**/*.json
**/lazy.config.ts
**/next.config.js
**/setupTests.js
**/setupJest.js
**/jestResolver.js
apps/vscode/extension/editor
apps/examples/www
apps/docs/api-content.json
apps/docs/content.json
apps/vscode/extension/editor/index.js
apps/vscode/extension/editor/tldraw-assets.json
**/sentry.server.config.js
**/scripts/upload-sourcemaps.js
**/coverage/**/*
apps/dotcom/public/sw.js
patchedJestJsDom.js

Wyświetl plik

@ -1,2 +0,0 @@
require('ts-node/register')
module.exports = require('./scripts/lib/eslint-plugin.ts')

Wyświetl plik

@ -1,125 +0,0 @@
module.exports = {
extends: [
'prettier',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@next/next/core-web-vitals',
],
ignorePatterns: [],
plugins: [
'@typescript-eslint',
'no-only-tests',
'import',
'local',
'@next/next',
'react-hooks',
'deprecation',
],
settings: {
next: {
rootDir: ['apps/*/', 'packages/*/'],
},
},
rules: {
'deprecation/deprecation': 'error',
'@next/next/no-html-link-for-pages': 'off',
'react/jsx-key': 'off',
'no-non-null-assertion': 'off',
'no-fallthrough': 'off',
'@typescript-eslint/no-fallthrough': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'react/display-name': 'off',
'@next/next/no-img-element': 'off',
'@typescript-eslint/no-extra-semi': 'off',
'no-mixed-spaces-and-tabs': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'no-throw-literal': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
'import/no-extraneous-dependencies': 'error',
'@typescript-eslint/consistent-type-exports': [
'error',
{ fixMixedExportsWithInlineTypeSpecifier: true },
],
'local/no-export-star': 'error',
'local/no-internal-imports': 'error',
'no-only-tests/no-only-tests': 'error',
'no-restricted-syntax': [
'error',
{ selector: "MethodDefinition[kind='set']", message: 'Property setters are not allowed' },
{ selector: "MethodDefinition[kind='get']", message: 'Property getters are not allowed' },
{
selector: 'Identifier[name=localStorage]',
message: 'Use the getFromLocalStorage/setInLocalStorage helpers instead',
},
{
selector: 'Identifier[name=sessionStorage]',
message: 'Use the getFromSessionStorage/setInSessionStorage helpers instead',
},
],
'no-restricted-globals': [
'error',
{ name: 'structuredClone', message: 'Use structuredClone from @tldraw/util instead' },
],
},
parser: '@typescript-eslint/parser',
parserOptions: {
project: true,
},
overrides: [
{
// enable the rule specifically for TypeScript files
files: ['*.ts', '*.tsx'],
rules: {
'@typescript-eslint/explicit-module-boundary-types': [0],
'no-console': ['error', { allow: ['warn', 'error'] }],
},
},
{
files: ['e2e/**/*'],
rules: {
'@typescript-eslint/no-empty-function': 'off',
},
},
{
files: 'scripts/**/*',
rules: {
'import/no-extraneous-dependencies': 'off',
},
},
{
files: ['apps/examples/**/*'],
rules: {
'no-restricted-syntax': 'off',
},
},
{
files: ['apps/huppy/**/*', 'scripts/**/*'],
rules: {
'no-console': 'off',
},
},
{
files: ['apps/dotcom/**/*'],
rules: {
'no-restricted-properties': [
2,
{
object: 'crypto',
property: 'randomUUID',
message: 'Please use the makeUUID util instead.',
},
],
},
},
],
}

Wyświetl plik

@ -1,50 +0,0 @@
name: Bug Report
description: File a bug report
title: '[Bug]: '
labels: ['bug', 'triage']
assignees: []
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: How can we reproduce the bug?
description: If you can make the bug happen again, please share the steps involved. You can [fork this CodeSandbox](https://codesandbox.io/p/sandbox/tldraw-example-n539u) to make a reproduction.
validations:
required: false
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/tldraw/tldraw/blob/main/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true

Wyświetl plik

@ -1,5 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: tldraw docs
url: https://tldraw.dev
about: Looking for help with the tldraw library? Try the tldraw docs!

Wyświetl plik

@ -1,25 +0,0 @@
name: Example Request
description: Submit a request for an example
title: '[Example Request]: '
labels: ['Example Request']
assignees: []
body:
- type: markdown
attributes:
value: |
Need an example of how to do something with tldraw?
- type: textarea
id: description
attributes:
label: What's the example?
description: Please describe the example. What would you like to see?
validations:
required: true
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/tldraw/tldraw/blob/main/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true

Wyświetl plik

@ -1,33 +0,0 @@
name: Feature request
description: File a feature request
title: '[Feature]: '
labels: ['enhancement']
assignees: []
body:
- type: markdown
attributes:
value: |
Have an idea for a feature or change in tldraw?
- type: textarea
id: description
attributes:
label: What's the feature?
description: Describe the feature, who it would help, and link to any examples from other apps.
validations:
required: true
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/tldraw/tldraw/blob/main/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true

Wyświetl plik

@ -1,21 +0,0 @@
name: Setup tldraw/tldraw
description: Set up node & yarn
runs:
using: composite
steps:
# see https://github.com/actions/setup-node/issues/899 for one of the reasons why this
# needs to be done before action/setup-node
- name: Enable corepack
run: corepack enable
shell: bash
- name: Setup Node.js Environment
uses: actions/setup-node@v3
with:
node-version: 20.11.0
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
shell: bash

Wyświetl plik

@ -1,35 +0,0 @@
Describe what your pull request does. If appropriate, add GIFs or images showing the before and after.
### Change Type
<!-- ❗ Please select a 'Scope' label ❗️ -->
- [ ] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff
<!-- ❗ Please select a 'Type' label ❗️ -->
- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc.
- [ ] `dunno` — I don't know
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] End to end tests
### Release Notes
- Add a brief release note for your PR here.

Wyświetl plik

@ -1,79 +0,0 @@
name: Checks
on:
pull_request:
merge_group:
push:
branches: [main]
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
jobs:
test:
name: 'Tests & checks'
timeout-minutes: 15
runs-on: ubuntu-latest-16-cores-open # TODO: this should probably run on multiple OSes
steps:
- name: Check out code
uses: actions/checkout@v3
- uses: ./.github/actions/setup
- name: Check version constraints
run: yarn constraints
- name: Check for duplicate dependencies
run: yarn dedupe --check
# Not the most sophisticated way to check for warnings, but I don't think
# Yarn can be convinced to exit with a non-zero exit code on warnings
# see also https://unix.stackexchange.com/a/433713 for the explanation of the parameters
- name: Check for installation warnings
run: 'yarn | grep -vzq "with warnings"'
- name: Check tsconfigs
run: yarn check-tsconfigs
- name: Typecheck
run: yarn build-types
- name: Check scripts
run: yarn check-scripts
- name: Check PR template
run: yarn update-pr-template --check
- name: Lint
run: yarn lint
- name: Check API declarations and docs work as intended
run: yarn api-check
- name: Test
run: yarn test-ci
build:
name: 'Build all projects'
timeout-minutes: 15
runs-on: ubuntu-latest-16-cores-open
steps:
- name: Check out code
uses: actions/checkout@v3
- uses: ./.github/actions/setup
- name: Build all projects
# the sed pipe makes sure that github annotations come through without
# turbo's prefix
run: "yarn build | sed -E 's/^.*? ::/::/'"
- name: Pack public packages
run: "yarn lazy pack-tarball | sed -E 's/^.*? ::/::/'"

Wyświetl plik

@ -1,37 +0,0 @@
name: Dedupe Dependabot PRs
on:
push:
branches: ['dependabot/**']
permissions:
contents: write
pull-requests: write
repository-projects: write
jobs:
dedupe:
name: Dedupe Dependabot PRs
runs-on: ubuntu-latest-16-cores-open
steps:
- name: Check out code
uses: actions/checkout@v3
- uses: ./.github/actions/setup
- name: Configure Git
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Dedupe dependencies
run: yarn dedupe
- name: Commit changes
run: |
git add .
git commit -m 'Dedupe' || true
- name: Push changes
run: git push

Wyświetl plik

@ -1,78 +0,0 @@
name: Deploy
on:
pull_request:
push:
branches:
- main
- production
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
TLDRAW_ENV: ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
defaults:
run:
shell: bash
jobs:
deploy:
name: Deploy to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
timeout-minutes: 15
runs-on: ubuntu-latest-16-cores-open
environment: ${{ github.ref == 'refs/heads/production' && 'deploy-production' || 'deploy-staging' }}
concurrency: ${{ github.ref == 'refs/heads/production' && 'deploy-production' || github.ref }}
steps:
- name: Notify initial start
uses: MineBartekSA/discord-webhook@v2
if: github.ref == 'refs/heads/production'
with:
webhook: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
content: 'Preparing ${{ env.TLDRAW_ENV }} deploy: ${{ github.event.head_commit.message }} by ${{ github.event.head_commit.author.name }}'
component: |
{
"type": 2,
"style": 5,
"label": "Open in GitHub",
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
- name: Check out code
uses: actions/checkout@v3
with:
submodules: true
- uses: ./.github/actions/setup
- name: Build types
run: yarn build-types
- name: Deploy
run: yarn tsx scripts/deploy.ts
env:
RELEASE_COMMIT_HASH: ${{ github.sha }}
GH_TOKEN: ${{ github.token }}
ASSET_UPLOAD: ${{ vars.ASSET_UPLOAD }}
MULTIPLAYER_SERVER: ${{ vars.MULTIPLAYER_SERVER }}
SUPABASE_LITE_URL: ${{ vars.SUPABASE_LITE_URL }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_DOTCOM_PROJECT_ID }}
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
DISCORD_DEPLOY_WEBHOOK_URL: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
DISCORD_HEALTH_WEBHOOK_URL: ${{ secrets.DISCORD_HEALTH_WEBHOOK_URL }}
HEALTH_WORKER_UPDOWN_WEBHOOK_PATH: ${{ secrets.HEALTH_WORKER_UPDOWN_WEBHOOK_PATH }}
GC_MAPS_API_KEY: ${{ secrets.GC_MAPS_API_KEY }}
WORKER_SENTRY_DSN: ${{ secrets.WORKER_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SUPABASE_LITE_ANON_KEY: ${{ secrets.SUPABASE_LITE_ANON_KEY }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_ACCESS_KEY_SECRET: ${{ secrets.R2_ACCESS_KEY_SECRET }}
APP_ORIGIN: ${{ vars.APP_ORIGIN }}

Wyświetl plik

@ -1,72 +0,0 @@
# Adapted from https://mmazzarolo.com/blog/2022-09-09-visual-regression-testing-with-playwright-and-github-actions/
# This workflow's goal is forcing an update of the reference snapshots used
# by Playwright tests. Add the 'update-snapshots' label to a PR.
# From a high-level perspective, it works like this:
# 1. Because of a GitHub Action limitation, this workflow is triggered on every
# label added to a PR. We manually interrupt it unless the label is
# "update-snapshots".
# 2. Use the GitHub API to grab the information about the branch name and SHA of
# the latest commit of the current pull request.
# 3. Remove the label from the PR.
# 4. Update the Playwright reference snapshots based on the UI of this branch.
# 5. Commit the newly generated Playwright reference snapshots into this branch.
name: Update playwright snapshots
on:
pull_request:
types: [labeled]
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
jobs:
update_snapshots:
name: 'Run'
timeout-minutes: 60
runs-on: ubuntu-latest-16-cores-open
if: github.event.label.name == 'update-snapshots'
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
pull-requests: write
steps:
- name: Remove the update-snapshots label
uses: actions-ecosystem/action-remove-labels@v1
with:
labels: update-snapshots
- name: Check out code
uses: actions/checkout@v3
with:
fetch-depth: 5
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.ref }}
- name: Run our setup
uses: ./.github/actions/setup
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium chrome
- name: Run Playwright tests & update snapshots
run: 'yarn e2e --update-snapshots'
working-directory: 'apps/examples'
- name: Commit and push changes
if: always()
run: |
git config --global user.name 'huppy-bot[bot]'
git config --global user.email '128400622+huppy-bot[bot]@users.noreply.github.com'
git add -A
git commit --no-verify -m '[automated] update snapshots'
git pull --rebase
git push

Wyświetl plik

@ -1,79 +0,0 @@
name: End to end tests
on:
pull_request:
merge_group:
push:
branches: [main]
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
jobs:
build:
name: 'End to end tests'
timeout-minutes: 60
runs-on: ubuntu-latest-16-cores-open
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Run our setup
uses: ./.github/actions/setup
- name: Get installed Playwright version
id: playwright-version
run: |
cd apps/examples
yarn info @playwright/test --json > playwright.json
echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./playwright.json').children.Version)")" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Check for Playwright browsers cache
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium chrome
if: steps.playwright-cache.outputs.cache-hit != 'true'
- name: Run Playwright tests
run: 'yarn e2e'
working-directory: apps/examples
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: apps/examples/playwright-report
retention-days: 30
- uses: shallwefootball/s3-upload-action@master
# only upload if we have AWS credentials:
env:
HAS_AWS_KEY: ${{ secrets.AWS_S3_SECRET_KEY && '1' || '' }}
if: always() && env.HAS_AWS_KEY == '1'
name: Upload S3
id: s3
with:
aws_key_id: ${{ secrets.AWS_S3_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_S3_SECRET_KEY}}
aws_bucket: playwright-reports.tldraw.xyz
source_dir: apps/examples/playwright-report
- name: Log report to summary
if: always()
run: |
report_url="https://playwright-reports.tldraw.xyz/${{ steps.s3.outputs.object_key }}/index.html"
echo "Report: $report_url"
echo "## Playwright report" >> $GITHUB_STEP_SUMMARY
echo "* $report_url" >> $GITHUB_STEP_SUMMARY

Wyświetl plik

@ -1,36 +0,0 @@
name: Prune preview deploys
on:
schedule:
# run once per day at midnight or whatever
- cron: '0 0 * * *'
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
jobs:
deploy:
name: Prune preview deploys
timeout-minutes: 15
runs-on: ubuntu-latest-16-cores-open
environment: deploy-staging
steps:
- name: Check out code
uses: actions/checkout@v3
with:
submodules: true
fetch-depth: 0
- uses: ./.github/actions/setup
- name: Prune preview deploys
run: yarn tsx scripts/prune-preview-deploys.ts
env:
GH_TOKEN: ${{ github.token }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

Wyświetl plik

@ -1,27 +0,0 @@
name: Publish Canary Packages
on:
push:
branches: [main]
jobs:
deploy:
name: 'Publish Canary Packages'
environment: npm deploy
timeout-minutes: 60
runs-on: ubuntu-latest-16-cores-open
steps:
- name: Check out code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Run our setup
uses: ./.github/actions/setup
- name: Publish Canary Packages
run: yarn tsx ./scripts/publish-canary.ts
env:
GH_TOKEN: ${{ github.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Wyświetl plik

@ -1,43 +0,0 @@
name: (Re)Publish public packages manually
# This only attempts to publish the public packages to npm.
# It does not bump the version, it does not update the changelogs, it does not create a github release.
# Prevent more than one non-canary npm publishing job from running at the same time
concurrency:
group: npm-publish
# Package publishing is manually triggered on github actions dashboard
on: workflow_dispatch
jobs:
deploy:
name: '(Re)Publish public packages manually'
environment: npm deploy
timeout-minutes: 60
runs-on: ubuntu-latest-16-cores-open
outputs:
is_latest_version: ${{ steps.publish_step.outputs.is_latest_version }}
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Run our setup
uses: ./.github/actions/setup
- name: Publish
id: publish_step
run: yarn tsx ./scripts/publish-manual.ts
env:
GH_TOKEN: ${{ github.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
publish_templates:
name: Publishes code templates to separate repositories
uses: tldraw/tldraw/.github/workflows/publish-templates.yml@main
if: ${{ needs.deploy.outputs.is_latest_version == 'true' }}
secrets:
VITE_TEMPLATE_REPO_SSH_DEPLOY_KEY: ${{ secrets.VITE_TEMPLATE_REPO_SSH_DEPLOY_KEY }}
NEXTJS_TEMPLATE_REPO_SSH_DEPLOY_KEY: ${{ secrets.NEXTJS_TEMPLATE_REPO_SSH_DEPLOY_KEY }}
needs: deploy

Wyświetl plik

@ -1,79 +0,0 @@
name: Publish new packages from main
# This bumps the version, updates the changelogs, publishes a GitHub release, and publishes the packages to npm.
# Prevent more than one non-canary npm publishing job from running at the same time
concurrency:
group: npm-publish
# Package publishing is manually triggered on github actions dashboard
on:
workflow_dispatch:
inputs:
bump_type:
type: choice
description: Version Bump Type
required: true
default: 'minor'
options:
- minor
- major
- override
version_override:
type: string
description: Version Override
required: false
jobs:
deploy:
name: 'Publish new version of public packages'
environment: npm deploy
timeout-minutes: 60
runs-on: ubuntu-latest-16-cores-open
steps:
- name: Check inputs
run: |
if [[ "${{ inputs.bump_type }}" != "override" ]] && ! [[ -z "${{ inputs.version_override }}" ]]; then
echo "ERROR You must set the Bump Type to 'override' if you supply a custom version number override."
exit 1
fi
- name: Generate GH token
id: generate_token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.HUPPY_APP_ID }}
private_key: ${{ secrets.HUPPY_APP_PRIVATE_KEY }}
- name: Check out code
uses: actions/checkout@v3
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Prepare repository
# Fetch full git history and tags for auto
run: git fetch --unshallow --tags
- name: Run our setup
uses: ./.github/actions/setup
- name: Publish
run: |
git config --global user.name 'huppy-bot[bot]'
git config --global user.email '128400622+huppy-bot[bot]@users.noreply.github.com'
if [[ "${{ inputs.bump_type }}" == "override" ]]; then
yarn tsx ./scripts/publish-new.ts --bump ${{ inputs.version_override }}
else
yarn tsx ./scripts/publish-new.ts --bump ${{ inputs.bump_type }}
fi
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
HUPPY_TOKEN: ${{ secrets.HUPPY_TOKEN }}
publish_templates:
name: Publishes code templates to separate repositories
uses: tldraw/tldraw/.github/workflows/publish-templates.yml@main
secrets:
VITE_TEMPLATE_REPO_SSH_DEPLOY_KEY: ${{ secrets.VITE_TEMPLATE_REPO_SSH_DEPLOY_KEY }}
NEXTJS_TEMPLATE_REPO_SSH_DEPLOY_KEY: ${{ secrets.NEXTJS_TEMPLATE_REPO_SSH_DEPLOY_KEY }}
needs: [deploy]

Wyświetl plik

@ -1,61 +0,0 @@
name: Publish patch release
# This bumps the patch version, updates the changelogs in the release branch only, publishes a GitHub release, and publishes the packages to npm.
# Prevent more than one non-canary npm publishing job from running at the same time
concurrency:
group: npm-publish
# Package publishing is manually triggered on github actions dashboard
on:
push:
branches:
- 'v*.*.x'
jobs:
deploy:
name: Publish patch release
environment: npm deploy
timeout-minutes: 60
runs-on: ubuntu-latest-16-cores-open
outputs:
is_latest_version: ${{ steps.publish_step.outputs.is_latest_version }}
steps:
- name: Generate GH token
id: generate_token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.HUPPY_APP_ID }}
private_key: ${{ secrets.HUPPY_APP_PRIVATE_KEY }}
- name: Check out code
uses: actions/checkout@v3
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Prepare repository
# Fetch full git history and tags for auto
run: git fetch --unshallow --tags
- name: Run our setup
uses: ./.github/actions/setup
- name: Publish
id: publish_step
run: |
git config --global user.name 'huppy-bot[bot]'
git config --global user.email '128400622+huppy-bot[bot]@users.noreply.github.com'
yarn tsx ./scripts/publish-patch.ts
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
HUPPY_TOKEN: ${{ secrets.HUPPY_TOKEN }}
publish_templates:
name: Publishes code templates to separate repositories
uses: tldraw/tldraw/.github/workflows/publish-templates.yml@main
if: ${{ needs.deploy.outputs.is_latest_version == 'true' }}
secrets:
VITE_TEMPLATE_REPO_SSH_DEPLOY_KEY: ${{ secrets.VITE_TEMPLATE_REPO_SSH_DEPLOY_KEY }}
NEXTJS_TEMPLATE_REPO_SSH_DEPLOY_KEY: ${{ secrets.NEXTJS_TEMPLATE_REPO_SSH_DEPLOY_KEY }}
needs: deploy

Wyświetl plik

@ -1,72 +0,0 @@
name: Publish templates
on:
workflow_call:
secrets:
VITE_TEMPLATE_REPO_SSH_DEPLOY_KEY:
required: true
NEXTJS_TEMPLATE_REPO_SSH_DEPLOY_KEY:
required: true
env:
CI: 1
PRINT_GITHUB_ANNOTATIONS: 1
defaults:
run:
shell: bash
jobs:
publish_vite:
name: 'Vite'
timeout-minutes: 60
runs-on: ubuntu-latest-16-cores-open
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Substitute the latest published version of tldraw
run: |
export TEMPLATE=vite NPM_TAG=latest && \
npm view tldraw dist-tags --json | jq -r ".$NPM_TAG" > /tmp/latest_tldraw && \
cp "templates/$TEMPLATE/package.json" /tmp/template_package.json && \
jq --tab --arg latest_tldraw "$(< /tmp/latest_tldraw)" '.dependencies."tldraw" |= $latest_tldraw' /tmp/template_package.json > "templates/$TEMPLATE/package.json"
- name: Push vite template to tldraw/vite-template
uses: cpina/github-action-push-to-another-repository@07c4d7b3def0a8ebe788a8f2c843a4e1de4f6900 # v1.7.2
env:
SSH_DEPLOY_KEY: ${{ secrets.VITE_TEMPLATE_REPO_SSH_DEPLOY_KEY }}
with:
source-directory: 'templates/vite'
destination-github-username: 'tldraw'
destination-repository-name: 'vite-template'
user-email: dan+github.actions@tldraw.com
target-branch: main
publish_next:
name: 'Next.js'
timeout-minutes: 15
runs-on: ubuntu-latest-16-cores-open
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Substitute the latest published version of tldraw
run: |
export TEMPLATE=nextjs NPM_TAG=latest && \
npm view tldraw dist-tags --json | jq -r ".$NPM_TAG" > /tmp/latest_tldraw && \
cp "templates/$TEMPLATE/package.json" /tmp/template_package.json && \
jq --tab --arg latest_tldraw "$(< /tmp/latest_tldraw)" '.dependencies."tldraw" |= $latest_tldraw' /tmp/template_package.json > "templates/$TEMPLATE/package.json"
- name: Push next.js template to tldraw/nextjs-template
uses: cpina/github-action-push-to-another-repository@07c4d7b3def0a8ebe788a8f2c843a4e1de4f6900 # v1.7.2
env:
SSH_DEPLOY_KEY: ${{ secrets.NEXTJS_TEMPLATE_REPO_SSH_DEPLOY_KEY }}
with:
source-directory: 'templates/nextjs'
destination-github-username: 'tldraw'
destination-repository-name: 'nextjs-template'
user-email: dan+github.actions@tldraw.com
target-branch: main

Wyświetl plik

@ -1,111 +0,0 @@
name: Trigger production build
on:
push:
branches:
- hotfixes
workflow_dispatch:
inputs:
target:
description: 'Target ref to deploy'
required: true
default: 'main'
defaults:
run:
shell: bash
env:
TARGET: ${{ github.event.inputs.target }}
jobs:
trigger:
name: ${{ github.event_name == 'workflow_dispatch' && 'Manual trigger' || 'Hotfix' }}
runs-on: ubuntu-latest-16-cores-open
concurrency: trigger-production
steps:
- name: Generate a token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.HUPPY_APP_ID }}
private-key: ${{ secrets.HUPPY_PRIVATE_KEY }}
- uses: actions/checkout@v3
with:
token: ${{ steps.generate_token.outputs.token }}
ref: refs/heads/production
fetch-depth: 0
- name: Get target commit hash (manual dispatch)
if: github.event_name == 'workflow_dispatch'
run: |
set -x
# if the target exists on its own use that
if git rev-parse "$TARGET" --quiet; then
target_hash=$(git rev-parse "$TARGET")
fi
# if not try prefixed with origin:
if [ -z "$target_hash" ]; then
target_hash=$(git rev-parse "origin/$TARGET")
fi
echo "SHOULD_DEPLOY=true" >> $GITHUB_ENV
echo "TARGET_HASH=$target_hash" >> $GITHUB_ENV
- name: Get target commit hash (hotfix)
if: github.event_name == 'push'
run: |
set -x
echo "TARGET_HASH=$GITHUB_SHA" >> $GITHUB_ENV
echo "TARGET=hotfix" >> $GITHUB_ENV
# is the hotfix sha already on production?
if git merge-base --is-ancestor "$GITHUB_SHA" production; then
echo "SHOULD_DEPLOY=false" >> $GITHUB_ENV
else
echo "SHOULD_DEPLOY=true" >> $GITHUB_ENV
fi
- name: Author setup (manual dispatch)
if: github.event_name == 'workflow_dispatch'
run: |
set -x
git config --global user.name "${{ github.actor }}"
git config --global user.email 'huppy+${{ github.actor }}@tldraw.com'
- name: Author setup (hotfix)
if: github.event_name == 'push'
run: |
set -x
commit_author_name=$(git log -1 --pretty=format:%cn "$TARGET_HASH")
commit_author_email=$(git log -1 --pretty=format:%ce "$TARGET_HASH")
git config --global user.name "$commit_author_name"
git config --global user.email "$commit_author_email"
- name: Get target tree hash
run: |
set -x
tree_hash=$(git show --quiet --pretty=format:%T "$TARGET_HASH")
echo "TREE_HASH=$tree_hash" >> $GITHUB_ENV
- name: Create commit & update production branch
run: |
set -eux
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
message="Deploy from $TARGET ($TARGET_HASH) at $now"
current_prod_hash=$(git rev-parse HEAD)
commit=$(git commit-tree -m "$message" -p "$current_prod_hash" -p "$TARGET_HASH" "$TREE_HASH")
git update-ref refs/heads/production "$commit"
git checkout production
- name: Push commit to trigger deployment
if: env.SHOULD_DEPLOY == 'true'
run: |
set -x
git push origin production
# reset hotfixes to the latest production
git push origin production:hotfixes --force

Wyświetl plik

@ -1,15 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
set -e
rm -rf *.tsbuildinfo */*.tsbuildinfo */*/*.tsbuildinfo */*/*/*.tsbuildinfo */*/*/*/*.tsbuildinfo
# This notifies the user if the yarn.lock file has changed.
CHANGED=$(git diff "$1" "$2" --stat -- ./yarn.lock | wc -l)
if (( CHANGED > 0 )); then
echo
echo "🚨 🚨 🚨 yarn.lock has changed! 🚨 🚨 🚨 "
echo "run 'yarn' to get the latest!"
echo
fi

Wyświetl plik

@ -1,13 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
set -e
# This notifies the user if the yarn.lock file has changed.
CHANGED=$(git diff HEAD@{1} --stat -- ./yarn.lock | wc -l)
if (( CHANGED > 0 )); then
echo
echo "🚨 🚨 🚨 yarn.lock has changed! 🚨 🚨 🚨 "
echo "run 'yarn' to get the latest!"
echo
fi

Wyświetl plik

@ -1,8 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn install --immutable
npx lazy run build-api
git add packages/*/api-report.md
git add packages/*/api/api.json
npx lint-staged

30
.ignore
Wyświetl plik

@ -1,30 +0,0 @@
dist
.tsbuild-dev
.tsbuild-pub
.tsbuild
node_modules
*.d.ts
**/api-report.md
**/_archive
**/*.tsbuildinfo
yarn.lock
**/*.tldr
**/*.d.ts
**/*.d.ts.map
**/*.js.map
**/*.css.map
apps/example/www/index.css
**/dist/*
*.cjs
apps/docs/.next
packages/tldraw/tldraw.css
**/dist-cjs/**/*
**/dist-esm/**/*
**/*.js.map
**/*.api.json
apps/docs/utils/vector-db
apps/docs/content/releases/**/*
apps/docs/content/reference/**/*
packages/**/api

Wyświetl plik

@ -1,30 +0,0 @@
**/node_modules/*
**/out/*
**/dist/*
**/dist-cjs/*
**/dist-esm/*
**/.next/*
**/api/*
!**/pages/api/*
**/.tsbuild*
**/.next/*
**/_archive/*
apps/docs/api-content.json
apps/docs/content.json
apps/vscode/extension/editor/*
apps/examples/www
content.json
apps/docs/utils/vector-db/index.json
**/gen/**/*.md
.github/pull_request_template.md
**/api-report.md
**/CHANGELOG.md
apps/docs/content/releases/**/*
apps/docs/content/reference/**/*
**/.vercel/*
**/.wrangler/*
**/.out/*
**/.temp/*
apps/dotcom/public/**/*.*

Wyświetl plik

@ -1,13 +0,0 @@
diff --git a/lib/api/ExtractorConfig.js b/lib/api/ExtractorConfig.js
index 31b46f8b51a2a93c2e538d67cd340e3c6897e242..d2cab369fc53cbd35bcb0c465783f851f1fd5f42 100644
--- a/lib/api/ExtractorConfig.js
+++ b/lib/api/ExtractorConfig.js
@@ -668,6 +668,6 @@ ExtractorConfig.FILENAME = 'api-extractor.json';
*/
ExtractorConfig._tsdocBaseFilePath = path.resolve(__dirname, '../../extends/tsdoc-base.json');
ExtractorConfig._defaultConfig = node_core_library_1.JsonFile.load(path.join(__dirname, '../schemas/api-extractor-defaults.json'));
-ExtractorConfig._declarationFileExtensionRegExp = /\.d\.ts$/i;
+ExtractorConfig._declarationFileExtensionRegExp = /\.d\.(m|c)?ts$/i;
exports.ExtractorConfig = ExtractorConfig;
//# sourceMappingURL=ExtractorConfig.js.map
\ No newline at end of file

Wyświetl plik

@ -1,32 +0,0 @@
diff --git a/lib/sloppy.js b/lib/sloppy.js
index b5d8950a8ea98d2d58723c1f96eeabb260699e24..81e57113edc1b16d681f157cd9b923dba190567c 100644
--- a/lib/sloppy.js
+++ b/lib/sloppy.js
@@ -1,24 +1,4 @@
-/* Domino uses sloppy-mode features (in particular, `with`) for a few
- * minor things. This file encapsulates all the sloppiness; every
- * other module should be strict. */
-/* jshint strict: false */
-/* jshint evil: true */
-/* jshint -W085 */
module.exports = {
- Window_run: function _run(code, file) {
- if (file) code += '\n//@ sourceURL=' + file;
- with(this) eval(code);
- },
- EventHandlerBuilder_build: function build() {
- try {
- with(this.document.defaultView || Object.create(null))
- with(this.document)
- with(this.form)
- with(this.element)
- return eval("(function(event){" + this.body + "})");
- }
- catch (err) {
- return function() { throw err; };
- }
- }
-};
+ Window_run: function _run(code, file) {},
+ EventHandlerBuilder_build: function build() {},
+}

Wyświetl plik

@ -1,30 +0,0 @@
diff --git a/lib/index.js b/lib/index.js
index 2229f15f097650cf726e2a153b70ca5546696224..08d966f9546acb8caed69e73e06c12ed55e45832 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -14,15 +14,15 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
-__exportStar(require("./FileFetcher"), exports);
-__exportStar(require("./GPT3Tokenizer"), exports);
-__exportStar(require("./ItemSelector"), exports);
+// __exportStar(require("./FileFetcher"), exports);
+// __exportStar(require("./GPT3Tokenizer"), exports);
+// __exportStar(require("./ItemSelector"), exports);
__exportStar(require("./LocalIndex"), exports);
-__exportStar(require("./LocalDocument"), exports);
-__exportStar(require("./LocalDocumentIndex"), exports);
-__exportStar(require("./LocalDocumentResult"), exports);
-__exportStar(require("./OpenAIEmbeddings"), exports);
-__exportStar(require("./TextSplitter"), exports);
+// __exportStar(require("./LocalDocument"), exports);
+// __exportStar(require("./LocalDocumentIndex"), exports);
+// __exportStar(require("./LocalDocumentResult"), exports);
+// __exportStar(require("./OpenAIEmbeddings"), exports);
+// __exportStar(require("./TextSplitter"), exports);
__exportStar(require("./types"), exports);
-__exportStar(require("./WebFetcher"), exports);
+// __exportStar(require("./WebFetcher"), exports);
//# sourceMappingURL=index.js.map
\ No newline at end of file

Wyświetl plik

@ -1,7 +0,0 @@
compressionLevel: mixed
enableGlobalCache: false
enableInlineBuilds: true
nodeLinker: node-modules

Plik diff jest za duży Load Diff

35
CLA.md
Wyświetl plik

@ -1,35 +0,0 @@
# Contributor License Agreement
**Version 1.0 — June 8th 2023**
In order to clarify the intellectual property license granted with Contributions from any person, tldraw, Inc. (“tldraw”) must have a Contributor License Agreement on file that has been signed by each contributor, indicating agreement to the license terms below. This license is for Your protection as a contributor as well as the protection of tldraw; it does not change your rights to use Your own contributions for any other purpose.
You accept and agree to the following terms and conditions for Your Contributions (present and future) that you submit to tldraw. Except for the license granted herein to tldraw, You reserve all right, title, and interest in and to Your Contributions.
1. Definitions.
"You" (or "Your") means the individual identified above.
"Contribution" means any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to tldraw for inclusion in, or documentation of, any of the products owned or managed by tldraw (each, a "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to tldraw or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, tldraw for the purpose of discussing and improving the Works, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
2. Grant of Copyright License. You hereby grant to tldraw a perpetual, worldwide, non-exclusive, sublicensable (through multiple tiers), no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute, and otherwise exploit Your Contributions and such derivative works.
3. Grant of Patent License. You hereby grant to tldraw a perpetual, worldwide, non-exclusive, sublicensable (through multiple tiers), no-charge, royalty-free, irrevocable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer and exploit the Works, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Works to which such Contribution(s) was submitted.
4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived in writing any rights it may have in Your Contributions to tldraw, or that your employer has executed a separate Corporate CLA with tldraw.
5. You represent that each of Your Contributions is Your original creation and does not incorporate any material created by others. You represent that Your Contribution submissions include complete details of any patents or copyrights which are associated with any part of Your Contributions.
6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "as is" basis, without warranties or conditions of any kind, either express or implied, including, without limitation, any warranties or conditions of title, non-infringement, merchantability, or fitness for a particular purpose.
7. You agree to notify tldraw of any facts or circumstances of which you become aware (now or in the future) that would make Your representations in this Agreement inaccurate in any respect.
8. You acknowledge that tldraw owns all right, title, and interest in and to the Works. Notwithstanding the foregoing, tldraws subsidiary, tldraw GB limited (the “Subsidiary”), is the beneficial owner of the Works, and tldraw will sublicense its rights in your Contributions under this Agreement to the Subsidiary in furtherance of the Subsidiarys status as beneficial owner of the Works.
9. This Agreement is governed by the laws of Delaware, and the parties consent to exclusive jurisdiction in the courts sitting in Delaware. The parties waive all defenses of lack of personal jurisdiction and forum non-conveniens.
10. Entire Agreement/Assignment. This Agreement is the entire agreement between the parties, and supersedes any and all prior agreements, understandings or communications, written or oral, between the parties relating to the subject matter hereof. This Agreement may be assigned by tldraw without Your prior consent.
---
Questions or concerns? Email [hello@tldraw.com.](mailto://hello@tldraw.com)

Wyświetl plik

@ -1,48 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@tldraw.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq

Wyświetl plik

@ -1,24 +0,0 @@
# Contributing
Thank you for your interest in contributing to [tldraw](https://github.com/tldraw/tldraw)! We welcome any contributions to the code base and the documentation.
## Create an Issue!
Before submitting a pull request, it is **strongly recommended** to [create an issue](https://github.com/tldraw/tldraw/issues/new/choose) first to discuss your proposed changes. This will help us to make sure that your changes are aligned with the project goals and that you are not duplicating work that is already in progress.
If you are not sure whether your changes are needed, feel free to create an issue anyway and we can discuss it there. Once we have agreed on the changes, you can start working on them.
## Making Changes
To create a pull request:
1. [Fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) and [clone](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) the [repository](https://github.com/tldraw/tldraw)
2. [Create a separate branch](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-branches) for your changes
3. Make your changes, and ensure that it is formatted by [Prettier](https://prettier.io) and type-checks without errors in [TypeScript](https://www.typescriptlang.org/)
4. Write tests that validate your change and/or fix.
5. Run `yarn build` and then run tests with `yarn test-ci`.
6. Push your branch and open a PR. 🚀
Before your code is merged, you will need to sign our [contributor license agreement](https://github.com/tldraw/tldraw/blob/main/CLA.md), which is handled automatically via GitHub comments. Your PR will be reviewed and merged in within a day or two if everything looks good.
Please also see our [Code of Conduct](https://github.com/tldraw/tldraw/blob/main/CODE_OF_CONDUCT.md) for our expectations around contributor culture.

92
README.md 100644 → 100755
Wyświetl plik

@ -1,95 +1,27 @@
# tldraw
Welcome to the public monorepo for [tldraw](https://github.com/tldraw/tldraw). tldraw is a library for creating infinite canvas experiences in React. It's the software behind the digital whiteboard [tldraw.com](https://tldraw.com).
- Read the docs and learn more at [tldraw.dev](https://tldraw.dev).
- Learn about [our license](https://github.com/tldraw/tldraw#License).
## Installation
```bash
npm i tldraw
```
## Usage
```tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function App() {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw />
</div>
)
}
```
Learn more at [tldraw.dev](https://tldraw.dev).
## Local development
The local development server will run our examples app. The basic example will show any changes you've made to the codebase.
To run the local development server, first clone this repo.
Install dependencies:
```bash
yarn
```
Start the local development server:
```bash
yarn dev
```
Open the example project at `localhost:5420`.
## License
tldraw's source code and distributed packages are provided under the non-commercial [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md).
This license does not permit commercial use. If you wish to use tldraw in a commercial product or enterprise, you will need to purchase a commercial license. To obtain a commercial license, please contact us at [sales@tldraw.com](mailto:sales@tldraw.com).
To learn more, see our [license](https://tldraw.dev/community/license) page.
## Trademarks
The tldraw name and logo are trademarks of tldraw. Please see our [trademark guidelines](https://github.com/tldraw/tldraw/blob/main/TRADEMARKS.md) for info on acceptable usage.
# @tldraw/example
## Community
Have questions, comments or feedback? [Join our discord](https://discord.gg/rhsyWMUJxd) or [start a discussion](https://github.com/tldraw/tldraw/discussions/new).
## Distributions
You can find tldraw on npm [here](https://www.npmjs.com/package/@tldraw/tldraw?activeTab=versions).
## Contribution
Please see our [contributing guide](https://github.com/tldraw/tldraw/blob/main/CONTRIBUTING.md). Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
## Contributors
## License
<a href="https://github.com/tldraw/tldraw/graphs/contributors">
<img src="https://contrib.rocks/image?repo=tldraw/tldraw&max=400&columns=20" width="100%"/>
</a>
The tldraw source code and its distributions are provided under the [tldraw license](https://github.com/tldraw/tldraw/blob/master/LICENSE.md). This license does not permit commercial use.
## Star History
If you wish to use this project in commercial product, you need to purchase a commercial license. Please contact us at [hello@tldraw.com](mailto:hello@tldraw.com) for more inforion about obtaining a commercial license.
<a href="https://star-history.com/#tldraw/tldraw">
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="https://api.star-history.com/svg?repos=tldraw/tldraw&type=Date&theme=dark"
/>
<source
media="(prefers-color-scheme: light)"
srcset="https://api.star-history.com/svg?repos=tldraw/tldraw&type=Date"
/>
<img src="https://api.star-history.com/svg?repos=tldraw/tldraw&type=Date" alt="Star History Chart" width="100%" />
</picture>
</a>
## Trademarks
Copyright (c) 2023-present tldraw Inc. The tldraw name and logo are trademarks of tldraw. Please see our [trademark guidelines](https://github.com/tldraw/tldraw/blob/main/TRADEMARKS.md) for info on acceptable usage.
## Contact
Find us on Twitter at [@tldraw](https://twitter.com/tldraw) or email [sales@tldraw.com](mailto://sales@tldraw.com). You can also [join our discord](https://discord.gg/rhsyWMUJxd) for quick help and support.
Find us on Twitter at [@tldraw](https://twitter.com/tldraw) or email [hello@tldraw.com](mailto://hello@tldraw.com). You can also [join our discord](https://discord.gg/rhsyWMUJxd) for quick help and support.

Wyświetl plik

@ -1,73 +0,0 @@
# Releases
## How tldraw is versioned
**We do not follow SemVer**.
- Major version bumps are very rare and we reserve them for special changes that signify a paradigm shift of some kind.
- Minor version bumps are released on a regular cadence. At the time of writing that cadence is monthly. **They may contain breaking changes**. We aim to make breaking changes as minimally disruptive as possible by providing warnings several releases in advance, and by providing tooling to help you migrate your code. We recommend updating tldraw at a similar pace to our release cadence, and be sure to check the release notes.
- Patch version bumps are for bugfixes and hotfixes that can't wait for the next cadence release.
## How to publish a new major or minor release
New cadence releases are published from `main`. You trigger a release manually by running the workflow defined in `publish-new.yml`.
1. Go [here](https://github.com/tldraw/tldraw/actions/workflows/publish-new.yml) and click the 'Run workflow' button.
2. Fill out the form that appears. You can leave the defaults as they are if you want to publish a new 'minor' release. If you want to publish a new 'major' release, select that option from the dropdown.
3. If you need to put the repo in 'prerelease' mode you can select the override option and provide a version number with a prerelease tag, like `3.4.0-rc.1`.
This is useful for providing a period of time for both us and our users to test a new release before it receives the `latest` tag on npm.
After switching into prerelease mode, any further 'minor' or 'major' releases will only increment the prerelease tag, like `3.4.0-rc.2`, `3.4.0-rc.3`, etc.
When you are ready to publish the final release, you can switch back to the `latest` tag by selecting the override option and providing a version number without a prerelease tag, like `3.4.0`.
When you click the 'run' button after selecting how to bump the version number, the github action will do the following things:
- Update the version numbers in package.json files.
- Update the changelog.
- Create a new release on github with the release notes from the changelog entry.
- Publish the new packages to npm.
- Create a new release branch for the new version. e.g. for version `3.4.0` it will create a branch called `v3.4.x`. (this is not done for prerelease versions)
## How to publish a new patch release
1. Make sure your git repo is up-to-date.
`git fetch`
2. Check out the latest release branch.
New major or minor releases will be given their own 'release branch' at publish time, with a name like `v2.0.x`. Every release branch starts with a `v` and ends in `.x`. Patch releases are published from these release branches.
To see the latest tldraw version number run `npm show tldraw version`. Then checkout the release branch for that number by prefixing the `v` and replacing the patch number with `x`. For example, if the latest version is `3.4.3`, you would run
`git checkout v3.4.x`
You can also patch older release branches if you need to. For example, if the latest version is `3.4.3` but you need to patch `2.8.2`, you would run
`git checkout v2.8.x`
3. Create a new branch based on the release branch.
`git checkout -b david/my-helpful-patches`
Replace `david/my-helpful-patches` with a branch name that makes sense for the patches you are about to make.
4. Cherry-pick the commits you want to include in the patch release.
`git cherry-pick <commit-hash>`
You can cherry-pick multiple commits if you want to include multiple bugfixes in the patch release.
5. Push the branch and make a PR targeting the release branch.
6. Merge the PR.
That's it! The patch release will be published automatically after merging. Changelog and version number updates will be committed back to the release branch, and deliberately not to `main`.
## What about documentation?
Our docs site is published in tandem with our npm packages. When you publish a new release, the docs site will be updated automatically so that the docs are always in sync with the latest version of tldraw.
If you make a docs change that you want to publish independently of a new cadence release, you can do so by following the same process as for creating a patch release. This will automatically detect that the packages themselves have not changed and will only update the docs site.

Wyświetl plik

@ -1,66 +0,0 @@
# tldraw Trademark Guidelines
This trademark policy was prepared to help you understand how to use the tldraw trademarks, service marks and logos.
While the copyright to our open source software is licensed under the tldraw license, our trademarks appearing in or on the open source software are the exclusive property of tldraw Inc. This means that our open source license does not include a license to use our trademarks.
Because we make some of our code available to download and modify, proper use of our trademarks is essential to inform people whether or not tldraw stands behind a product or service. When using tldraw trademarks, you must comply with these tldraw Trademark Guidelines.
This policy is intended to explain how to use our trademarks in a way that is consistent with background law and community expectations. This policy covers:
1. Our word trademarks and service marks: tldraw
2. Our logos: The tldraw logos
This policy encompasses all trademarks and service marks, whether they are registered or not.
## General guidelines
Whenever you use one of our marks, you must always do so in a way that does not mislead anyone about what they are getting and from whom.
Do not use the tldraw marks in any way that could mistakenly imply that tldraw has reviewed, approved or guaranteed your goods or services. You also cannot use our logo on your website in a way that suggests that your website is an official website or that we endorse your website. You can, though, say you like the tldraw software, that you use tldraw, that the analytics are powered by tldraw or that you participate in the tldraw community.
Do not use the "tl" prefix in a way that could mistakenly imply that your product is related to tldraw. For example, an analytics product that uses tldraw should not use the name "tlanalytics".
You may not use or register our marks or variations of them as part of your trademark, business, product, service, app, domain name, social media account or business indicator. You may not use our marks as a part of an advertising campaign. You may not display tldraw trademarks more prominently than your product, service or company name. You may not use tldraw trademarks on merchandise for sale (e.g., selling t-shirts, mugs, etc).
Trademark law does not allow your use of names or trademarks that are too similar to ours. You therefore may not use an obvious variation of any of our marks or any phonetic equivalent, foreign language equivalent, takeoff, or abbreviation for a similar or compatible product or service.
## Acceptable uses
You can use the tldraw name to truthfully and accurately refer to or identify tldraw and its products and services in the following instances:
- To refer to tldraw and its products and services in news articles and other content without alteration
- To discuss tldraw and its products in a fair and honest manner that does not suggest sponsorship or endorsement by or affiliation with tldraw
- To refer to and/or to link to the products and services hosted on tldraws servers and website
- To indicate if your product, service or solution integrates, or is interoperable or compatible, with tldraw, for example, “we offer a simple integration with tldraw”, provided that doing so does not create a likelihood of confusion as to the origin of such product, service, or solution
- You may use our word marks as part of a public subdomain solely for the purpose of serving as the URL for your self-managed tldraw instance, for example, tldraw.companyname.com
## Prohibited uses
Unless you have express written permission from tldraw, or your use is permitted pursuant to the acceptable uses set forth above, the use of tldraw trademarks is strictly prohibited. Here is a short, non-exhaustive list of the kinds of uses that are not permitted without tldraws express written permission but that tldraw may consider granting you the right to do should you request permission:
- Use of tldraw trademarks in connection with the provision of a public website that makes tldraw software available for installation and use on a server (rather than directing users to the official tldraw site)
- Use of tldraw trademarks in connection with versions of tldraw products made publicly available or made available in the cloud on a managed service provider, resale or other commercial basis
- Use of tldraw trademarks in connection with tldraw product bundled with other software
In the above cases:
- You must follow the terms of the open source license for tldraw software products and code
- You must remove all of our logos from it and choose your branding, logos and trademarks that denote your unique identity to clearly signal to users that there is no affiliation with or endorsement by tldraw
- You must not use any tldraw trademark in connection with the user-facing name, branding or marketing materials of your project
- You may use word marks, but not our logos, in truthful statements that describe the relationship between your software and ours, for example, “this software is derived from the source code of the tldraw software”, as long as you also include a statement that your project is not officially associated with tldraw or its products
- tldraw reserves the right in its sole discretion to (i) terminate, revoke, modify, or otherwise change permission to use the trademarks at any time and; (ii) object to any use or misuse of the trademarks in any jurisdiction worldwide. All changes to these guidelines are effective immediately when posted and your continued use of the trademarks following the posting of revised guidelines signifies your acceptance of such revision.
## To request the use of the trademarks
Anyone wishing to use any of tldraws trademarks in a manner other than the acceptable uses listed above, including but not limited to marketing, promotion or advertising, or on software derivative of tldraw software, must obtain tldraws express, written permission in advance.
To request the use of the trademarks in a manner or for a purpose not expressly permitted in these guidelines, including use for any purpose of the logos, please email [hello@tldraw.com](mailto://hello@tldraw.com) to discuss. If you need clarification on whether your use qualifies, please ask.
## To report misuse
If you want to report misuse of a tldraw trademark, please email [hello@tldraw.com](mailto://hello@tldraw.com).
Last updated: November 7 2023
These guidelines are based on the Model Trademark Guidelines, available at http://www.modeltrademarkguidelines.org., used under a Creative Commons Attribution 3.0 Unported license: https://creativecommons.org/licenses/by/3.0/deed.en_US

Wyświetl plik

@ -1,6 +0,0 @@
content/reference
api-content.json
content.db
.env
.next
.vercel

Wyświetl plik

@ -1,5 +0,0 @@
.next/*
.lazy/*
content.db
node_modules
utils/vector-db/index.json

Wyświetl plik

@ -1 +0,0 @@
# @tldraw/docs

Wyświetl plik

@ -1 +0,0 @@
See [tldraw license](https://github.com/tldraw/tldraw/blob/master/LICENSE.md)

Wyświetl plik

@ -1,149 +0,0 @@
# tldraw-docs
<div alt style="text-align: center; transform: scale(.5);">
<picture>
<img alt="tldraw" src="https://github.com/tldraw/tldraw-lite/raw/main/docs/public/card_repo.png" />
</picture>
</div>
Welcome to the source for the [tldraw docs site](https://tldraw.dev).
This site is a [Next.js](https://nextjs.org/) app that uses [MDX](https://mdxjs.com/) for content. It contains human-written docs in the `content` folder as well as generated docs in the `api` folder.
We have several scripts that build these files into a SQLite database that is used to generate the site's pages.
To pull the most recent docs from the tldraw repo, create an .env file with a GitHub personal access token and the SHA of the commit or branch that you'd like to pull from.
```
ACCESS_TOKEN=your_github_access_token
SOURCE_SHA=main
```
The files are also provided in this repo.
## Building the content
You can build the markdown and API content using the following scripts:
- `yarn refresh-everything` to reset the database, generate the markdown from the API docs, and populate the database with articles from both the regular content and the generated API content
- `yarn refresh-content` to generate just the regular content
# Content
The docs has two types of content: regular content that is written by the team and auto-generated content that is created using [tsdoc](https://tsdoc.org/) and [API extractor](https://api-extractor.com/).
The `content` folder contains all content in the form of MDX files. All articles belong to a "section" and a "category". The `sections.json` defines each section and any categories belonging to that section.
A section looks like this:
```json
{
"id": "community",
"title": "Community",
"description": "Guides for contributing to tldraw's open source project.",
"categories": []
}
```
The content is organized into folders for each section. The `gen` folder contains auto-generated content.
## Regular Content
The `content` folder contains all "regular" content in the form of MDX files.All articles belong to a "section" and a "category". The content is organized into folders for each "section".
An article's frontmatter looks like this:
```md
---
title: User Interface
description: How to customize the tldraw user interface using overrides.
status: published
author: steveruizok
date: 3/22/2023
order: 8
keywords:
- ui
- interface
- tools
- shapes
- custom
- button
- toolbar
- styles
---
```
### Title
The `title` is displayed in the article's header, in the page title, in the search bar, and in search results. It is used to find an article through the site's search feature.
### Description
The `description` is hidden in the article's frontmatter, but is used to populate the article's meta description tag. It is also used to find an article through the site's search feature.
### Hero
The `hero` is used for the article's social media image. It is not displayed in the article. It should refer to a page in the `public/images` folder.
### Category
An article may declare its `category` in its frontmatter. Any article that does not declare a category will be placed into the "ucg" category for "uncategorized" articles.
### Order
The `order` property defines the article's order in its category. Uncategorized articles are placed at the end of the list of categories sorted by its `order`. For a section without categories, the `order` keyword effectively defines the order that the article will appear in the section list.
### Author
The `author` must refer to an author named in the `content/authors.json` file.
An author looks like this:
```json
"steveruizok": {
"name": "Steve Ruiz",
"email": "steve@tldraw.com",
"twitter": "steveruizok",
"image": "steve_ruiz.jpg"
}
```
The image should refer to an image in `public/avatars`.
### Date
The `date` is formatted as DD/MM/YYYY.
### Status
An article's `status` may be either `draft` or `published`. A `draft` article is hidden in production.
### Keywords
The `keywords` are used to find an article through the site's search feature.
## Auto-generated content
The auto-generated docs content is created using [tsdoc](https://tsdoc.org/) and [API extractor](https://api-extractor.com/). The source is the API documentation created by `yarn build` or `yarn build-api`. The output is placed in the `gen` folder.
## Developing the docs
When developing the docs, any change to the `content` folder will cause the page to refresh. This is a little shitty but it mostly works.
## Contribution
Please see our [contributing guide](https://github.com/tldraw/tldraw/blob/main/CONTRIBUTING.md). Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
## License
The tldraw source code and its distributions are provided under the [tldraw license](https://github.com/tldraw/tldraw/blob/master/LICENSE.md). This license does not permit commercial use.
If you wish to use this project in commercial product, you need to purchase a commercial license. matPlease contact us at [sales@tldraw.com](mailto:sales@tldraw.com) for more inforion about obtaining a commercial license.
## Trademarks
Copyright (c) 2023-present tldraw Inc. The tldraw name and logo are trademarks of tldraw. Please see our [trademark guidelines](https://github.com/tldraw/tldraw/blob/main/TRANDEMARKS.md) for info on acceptable usage.
## Contact
Find us on Twitter at [@tldraw](https://twitter.com/tldraw) or email [sales@tldraw.com](mailto://sales@tldraw.com). You can also [join our discord](https://discord.gg/rhsyWMUJxd) for quick help and support.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -1,158 +0,0 @@
import { ArticleDocsPage } from '@/components/ArticleDocsPage'
import { ArticleReferenceDocsPage } from '@/components/ArticleReferenceDocsPage'
import { CategoryDocsPage } from '@/components/CategoryDocsPage'
import { ExampleDocsPage } from '@/components/ExampleDocsPage'
import { SectionDocsPage } from '@/components/SectionDocsPage'
import { Article, Category, Section } from '@/types/content-types'
import { getDb } from '@/utils/ContentDatabase'
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
async function getContentForPath(
path: string
): Promise<
| { type: 'section'; section: Section }
| { type: 'category'; category: Category }
| { type: 'article'; article: Article }
> {
const db = await getDb()
const section = await db.db.get(`SELECT * FROM sections WHERE sections.path = ?`, path)
if (section) return { type: 'section', section } as const
const category = await db.db.get(`SELECT * FROM categories WHERE categories.path = ?`, path)
if (category) return { type: 'category', category } as const
const article = await db.db.get(`SELECT * FROM articles WHERE articles.path = ?`, path)
if (article) return { type: 'article', article } as const
throw notFound()
}
export async function generateMetadata({ params }: { params: { id: string | string[] } }) {
const path = typeof params.id === 'string' ? [params.id] : params.id
const pathString = '/' + path.join('/')
const content = await getContentForPath(pathString)
if (!content) return {}
let title: string | undefined
let description: string | undefined
let hero: string | undefined
switch (content.type) {
case 'section': {
const { section } = content
title = section.title
description = section.description ?? undefined
hero = section.hero ?? undefined
break
}
case 'category': {
const { category } = content
title = category.title
description = category.description ?? undefined
hero = category.hero ?? undefined
break
}
case 'article': {
const { article } = content
title = article.title
description = article.description ?? undefined
hero = article.hero ?? undefined
break
}
}
const metadata: Metadata = {
title,
description: description,
openGraph: {
title: title,
description: description,
images: hero,
},
twitter: {
description: description,
images: hero,
},
}
return metadata
}
export async function generateStaticParams() {
const db = await getDb()
const sections = await db.db.all(`SELECT * FROM sections`)
const categories = await db.db.all(`SELECT * FROM categories`)
const articles = await db.db.all(`SELECT * FROM articles`)
const paths = [] as string[]
for (const section of sections) {
paths.push(section.path)
}
for (const category of categories) {
paths.push(category.path)
}
for (const article of articles) {
paths.push(article.path)
}
return paths.map((path) => ({ params: { id: path.split('/').filter((p) => p) } }))
}
export default async function ContentPage({ params }: { params: { id: string | string[] } }) {
const path = typeof params.id === 'string' ? [params.id] : params.id
const pathString = '/' + path.join('/')
const content = await getContentForPath(pathString)
if (!content) throw notFound()
switch (content.type) {
case 'section': {
const db = await getDb()
let firstArticleInSection: Article | undefined
const categories = await db.getCategoriesForSection(content.section.id)
for (const category of categories) {
const articles = await db.getCategoryArticles(content.section.id, category.id)
const article = articles[0]
if (article) {
firstArticleInSection = article
break
}
}
if (firstArticleInSection) {
const article = await db.getArticle(firstArticleInSection.id)
if (article?.componentCode) {
return <ExampleDocsPage article={article} />
}
return <ArticleDocsPage article={article} />
}
return <SectionDocsPage section={content.section} />
}
case 'category': {
return <CategoryDocsPage category={content.category} />
}
case 'article': {
if (content.article.componentCode) {
return <ExampleDocsPage article={content.article} />
}
if (content.article.sectionId === 'reference') {
return <ArticleReferenceDocsPage article={content.article} />
}
return <ArticleDocsPage article={content.article} />
}
default: {
throw notFound()
}
}
}

Wyświetl plik

@ -1,111 +0,0 @@
import { Header } from '@/components/Header'
import { Sidebar } from '@/components/Sidebar'
import { getDb } from '@/utils/ContentDatabase'
export default async function ClaPage() {
const db = await getDb()
const sidebar = await db.getSidebarContentList({})
return (
<>
<Header />
<Sidebar {...sidebar} />
<main className="main-content article">
<div className="page-header">
<h1>Contributor License Agreement</h1>
</div>
<h4>Version 1.0 June 8th 2023</h4>
<p>
In order to clarify the intellectual property license granted with Contributions from any
person, tldraw, Inc. (tldraw) must have a Contributor License Agreement on file that has
been signed by each contributor, indicating agreement to the license terms below. This
license is for Your protection as a contributor as well as the protection of tldraw; it
does not change your rights to use Your own contributions for any other purpose.
</p>
<p>
You accept and agree to the following terms and conditions for Your Contributions (present
and future) that you submit to tldraw. Except for the license granted herein to tldraw,
You reserve all right, title, and interest in and to Your Contributions.{' '}
</p>
<p>1. Definitions. </p>
<p>"You" (or "Your") means the individual identified above. </p>
<p>
"Contribution" means any original work of authorship, including any modifications or
additions to an existing work, that is intentionally submitted by You to tldraw for
inclusion in, or documentation of, any of the products owned or managed by tldraw (each, a
"Work"). For the purposes of this definition, "submitted" means any form of electronic,
verbal, or written communication sent to tldraw or its representatives, including but not
limited to communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, tldraw for the purpose of
discussing and improving the Works, but excluding communication that is conspicuously
marked or otherwise designated in writing by You as "Not a Contribution."{' '}
</p>
<p>
2. Grant of Copyright License. You hereby grant to tldraw a perpetual, worldwide,
non-exclusive, sublicensable (through multiple tiers), no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare derivative works of, publicly display,
publicly perform, distribute, and otherwise exploit Your Contributions and such derivative
works.{' '}
</p>
<p>
3. Grant of Patent License. You hereby grant to tldraw a perpetual, worldwide,
non-exclusive, sublicensable (through multiple tiers), no-charge, royalty-free,
irrevocable patent license to make, have made, use, offer to sell, sell, import, and
otherwise transfer and exploit the Works, where such license applies only to those patent
claims licensable by You that are necessarily infringed by Your Contribution(s) alone or
by combination of Your Contribution(s) with the Works to which such Contribution(s) was
submitted.{' '}
</p>
<p>
4. You represent that you are legally entitled to grant the above license. If your
employer(s) has rights to intellectual property that you create that includes your
Contributions, you represent that you have received permission to make Contributions on
behalf of that employer, that your employer has waived in writing any rights it may have
in Your Contributions to tldraw, or that your employer has executed a separate Corporate
CLA with tldraw.{' '}
</p>
<p>
5. You represent that each of Your Contributions is Your original creation and does not
incorporate any material created by others. You represent that Your Contribution
submissions include complete details of any patents or copyrights which are associated
with any part of Your Contributions.{' '}
</p>
<p>
6. You are not expected to provide support for Your Contributions, except to the extent
You desire to provide support. You may provide support for free, for a fee, or not at all.
Unless required by applicable law or agreed to in writing, You provide Your Contributions
on an "as is" basis, without warranties or conditions of any kind, either express or
implied, including, without limitation, any warranties or conditions of title,
non-infringement, merchantability, or fitness for a particular purpose.{' '}
</p>
<p>
7. You agree to notify tldraw of any facts or circumstances of which you become aware (now
or in the future) that would make Your representations in this Agreement inaccurate in any
respect.
</p>
<p>
8. You acknowledge that tldraw owns all right, title, and interest in and to the Works.
Notwithstanding the foregoing, tldraws subsidiary, tldraw GB limited (the Subsidiary),
is the beneficial owner of the Works, and tldraw will sublicense its rights in your
Contributions under this Agreement to the Subsidiary in furtherance of the Subsidiarys
status as beneficial owner of the Works.
</p>
<p>
9. This Agreement is governed by the laws of Delaware, and the parties consent to
exclusive jurisdiction in the courts sitting in Delaware. The parties waive all defenses
of lack of personal jurisdiction and forum non-conveniens.
</p>
<p>
10. Entire Agreement/Assignment. This Agreement is the entire agreement between the
parties, and supersedes any and all prior agreements, understandings or communications,
written or oral, between the parties relating to the subject matter hereof. This Agreement
may be assigned by tldraw without Your prior consent.{' '}
</p>
<hr />
<p>
Questions or concerns? Email <a href="mailto:sales@tldraw.com">sales@tldraw.com.</a>
</p>
</main>
</>
)
}

Wyświetl plik

@ -1,10 +0,0 @@
import { Footer } from '@/components/Footer'
export default async function ContentLayout({ children }: { children: React.ReactNode }) {
return (
<div className="wrapper">
<div className="layout">{children}</div>
<Footer />
</div>
)
}

Wyświetl plik

@ -1,86 +0,0 @@
import { Header } from '@/components/Header'
import { Sidebar } from '@/components/Sidebar'
import { getDb } from '@/utils/ContentDatabase'
export default async function LicensePage() {
const db = await getDb()
const sidebar = await db.getSidebarContentList({})
return (
<>
<Header />
<Sidebar {...sidebar} />
<main className="article">
<div className="page-header">
<h1>tldraw License</h1>
</div>
<p>
This License governs use of the accompanying Software, and your use of the Software
constitutes acceptance of this license.
</p>
<p>
You may use this Software for any non-commercial purpose, subject to the restrictions in
this license. Some purposes which can be non- commercial are teaching, academic research,
and personal experimentation.
</p>
<p>
You may not use or distribute this Software or any derivative works in any form for
commercial purposes. Examples of commercial purposes would be running business operations,
licensing, leasing, or selling the Software, distributing the Software for use with
commercial products or for internal products within commercial entities, or otherwise
using the Software in any way that provides you with a commercial benefit.
</p>
<p>To purchase an alternative license for commercial use, contact sales@tldraw.com.</p>
<p>
Subject to your compliance with the restrictions and obligations in this License, you may
modify this Software and distribute the modified Software for non-commercial purposes,
however, you may not grant rights to the Software or derivative works that are broader
than those provided by this License. For example, you may not distribute modifications of
the Software under terms that provide a commercial benefit to you, permit commercial use,
or under terms that purport to require the Software or derivative works to be sublicensed
to others.
</p>
<p>In return for these conditions of use, you agree:</p>
<p>Not to remove any copyright or other notices from the Software.</p>
<p>
That if you distribute the Software in source or object form, you will include a verbatim
copy of this license.
</p>
<p>
That if you distribute derivative works of the Software in source code form you do so only
under a license that includes all of the provisions of this License, and if you distribute
derivative works of the Software solely in object form you must make the source code form
available upon request and do so only under a license that complies with this License.
</p>
<p>
That that the word "tldraw" shall not be used to refer to any derivative works of the
Software except in the phrase "Based on the tldraw library (https://tldraw.com)", provided
such phrase is not used to promote the derivative works or to imply that tldraw endorses
you or the derivative works.
</p>
<p>
THAT THE SOFTWARE COMES "AS IS", WITH NO WARRANTIES. THIS MEANS NO EXPRESS, IMPLIED OR
STATUTORY WARRANTY, INCLUDING WITHOUT LIMITATION, WARRANTIES OF MERCHANTABILITY OR FITNESS
FOR A PARTICULAR PURPOSE OR ANY WARRANTY OF TITLE OR NON-INFRINGEMENT. ALSO, YOU MUST PASS
THIS DISCLAIMER ON WHENEVER YOU DISTRIBUTE THE SOFTWARE OR DERIVATIVE WORKS.
</p>
<p>
THAT TLDRAW WILL NOT BE LIABLE FOR ANY DAMAGES RELATED TO THE SOFTWARE OR THIS LICENSE,
INCLUDING DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL OR INCIDENTAL DAMAGES, TO THE MAXIMUM
EXTENT THE LAW PERMITS, NO MATTER WHAT LEGAL THEORY IT IS BASED ON. ALSO, YOU MUST PASS
THIS LIMITATION OF LIABILITY ON WHENEVER YOU DISTRIBUTE THE SOFTWARE OR DERIVATIVE WORKS.
</p>
<p>
That if you sue anyone over patents that you think may apply to the Software or anyones
use of the Software, your license to the Software ends automatically.
</p>
<p>That your rights under the License end automatically if you breach it in any way.</p>
<p>tldraw reserves all rights not expressly granted to you in this license.</p>
<hr />
<p>
Questions? Email <a href="mailto:sales@tldraw.com">sales@tldraw.com.</a>
</p>
</main>
</>
)
}

Wyświetl plik

@ -1,10 +0,0 @@
import { Footer } from '@/components/Footer'
export default async function ContentLayout({ children }: { children: React.ReactNode }) {
return (
<div className="wrapper">
<div className="layout">{children}</div>
<Footer />
</div>
)
}

Wyświetl plik

@ -1,10 +0,0 @@
import { ArticleDocsPage } from '@/components/ArticleDocsPage'
import { getDb } from '@/utils/ContentDatabase'
import { notFound } from 'next/navigation'
export default async function HomePage() {
const db = await getDb()
const article = await db.db.get(`SELECT * FROM articles WHERE articles.path = ?`, `/quick-start`)
if (article) return <ArticleDocsPage article={article} />
throw notFound()
}

Wyświetl plik

@ -1,185 +0,0 @@
import { SearchResult } from '@/types/search-types'
import { getDb } from '@/utils/ContentDatabase'
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
import { structuredClone } from '@tldraw/utils'
import assert from 'assert'
import { NextRequest } from 'next/server'
type Data = {
results: {
articles: SearchResult[]
apiDocs: SearchResult[]
examples: SearchResult[]
}
status: 'success' | 'error' | 'no-query'
}
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: structuredClone(SEARCH_RESULTS),
status: 'error',
error: 'No query',
}),
{
status: 400,
}
)
}
try {
const results: Data['results'] = structuredClone(SEARCH_RESULTS)
const db = await getDb()
const getVectorDb = (await import('@/utils/ContentVectorDatabase')).getVectorDb
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) => {
try {
if (result.type !== 'heading') return // bleg
const article = await db.db.get(
`SELECT id, title, description, categoryId, sectionId, keywords FROM articles WHERE id = ?`,
result.id
)
assert(article, `No article found for heading ${result.id}`)
const category = await db.db.get(
`SELECT id, title FROM categories WHERE id = ?`,
article.categoryId
)
const section = await db.db.get(
`SELECT id, title FROM sections WHERE id = ?`,
article.sectionId
)
const heading = await db.db.get(`SELECT * FROM headings WHERE slug = ?`, result.slug)
assert(heading, `No heading found for ${result.id} ${result.slug}`)
return {
id: result.id,
article,
category,
section,
heading,
score: result.score,
}
} catch (e: any) {
console.error(e.message)
// something went wrong
return
}
})
)
).filter(Boolean)
const visited = new Set<string>()
for (const result of headings) {
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[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
: article.title + ': ' + heading.title,
url: isUncategorized
? `${section.id}/${article.id}#${heading.slug}`
: `${section.id}/${category.id}/${article.id}#${heading.slug}`,
score,
})
}
const articles = await Promise.all(
queryResults.map(async (result) => ({
score: result.score,
article: await db.db.get(
`SELECT id, title, description, categoryId, sectionId, keywords FROM articles WHERE id = ?`,
result.id
),
}))
)
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 = ?`,
article.categoryId
)
const section = await db.db.get(
`SELECT id, title FROM sections WHERE sections.id = ?`,
article.sectionId
)
const isUncategorized = category.id === section.id + '_ucg'
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}`
: `${section.id}/${category.id}/${article.id}`,
score,
})
}
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))
results[section as keyof Data['results']] = results[section as keyof Data['results']].slice(
0,
10
)
})
return new Response(
JSON.stringify({
results,
status: 'success',
}),
{
status: 200,
}
)
} catch (e: any) {
return new Response(
JSON.stringify({
results: structuredClone(SEARCH_RESULTS),
status: 'error',
error: e.message,
}),
{
status: 500,
}
)
}
}

Wyświetl plik

@ -1,129 +0,0 @@
import { SearchResult } from '@/types/search-types'
import { getDb } from '@/utils/ContentDatabase'
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
import { structuredClone } from '@tldraw/utils'
import { NextRequest } from 'next/server'
type Data = {
results: {
articles: SearchResult[]
apiDocs: SearchResult[]
examples: SearchResult[]
}
status: 'success' | 'error' | 'no-query'
}
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: structuredClone(SEARCH_RESULTS),
status: 'no-query',
}),
{
status: 400,
}
)
}
try {
const results: Data['results'] = structuredClone(SEARCH_RESULTS)
const db = await getDb()
const searchForArticle = await db.db.prepare(
`
SELECT id, title, sectionId, categoryId, content
FROM ftsArticles
WHERE ftsArticles MATCH ?
ORDER BY bm25(ftsArticles, 1000.0)
`,
query
)
await searchForArticle.all().then(async (queryResults) => {
for (const article of queryResults) {
const section = await db.getSection(article.sectionId)
const category = await db.getCategory(article.categoryId)
const isUncategorized = category.id === section.id + '_ucg'
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}`,
score: 0,
})
}
})
const searchForArticleHeadings = await db.db.prepare(
`
SELECT id, title, articleId, slug
FROM ftsHeadings
WHERE ftsHeadings MATCH ?
ORDER BY bm25(ftsHeadings, 1000.0)
`,
query
)
await searchForArticleHeadings.all().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 section = await db.getSection(article.sectionId)
const category = await db.getCategory(article.categoryId)
const isUncategorized = category.id === section.id + '_ucg'
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
: article.title + ': ' + heading.title,
url: isUncategorized
? `${section.id}/${article.id}#${heading.slug}`
: `${section.id}/${category.id}/${article.id}#${heading.slug}`,
score: 0,
})
}
})
Object.keys(results).forEach((section: string) => {
results[section as keyof Data['results']] = results[section as keyof Data['results']].slice(
0,
20
)
})
results.articles.sort(
(a, b) => (b.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1)
)
return new Response(JSON.stringify({ results, status: 'success' }), {
status: 200,
})
} catch (e: any) {
return new Response(
JSON.stringify({
results: structuredClone(SEARCH_RESULTS),
status: 'error',
error: e.message,
}),
{
status: 500,
}
)
}
}

Wyświetl plik

@ -1,79 +0,0 @@
import { Analytics } from '@vercel/analytics/react'
import { Metadata, Viewport } from 'next'
import AutoRefresh from '../components/AutoRefresh'
import '../styles/globals.css'
import '../styles/hljs.css'
import '../styles/parameters-table.css'
import { Providers } from './providers'
const TITLE = 'tldraw SDK'
const DESCRIPTION =
'Infinite canvas SDK from tldraw. Build whiteboards, design tools, and canvas experiences for the web.'
const TWITTER_HANDLE = '@tldraw'
const TWITTER_CARD = 'social-twitter.png'
const FACEBOOK_CARD = 'social-og.png'
const THEME_COLOR = '#FFFFFF'
export const metadata: Metadata = {
metadataBase: new URL('https://tldraw.dev'),
title: {
default: TITLE,
template: `%s • ${TITLE}`,
},
description: DESCRIPTION,
openGraph: {
title: TITLE,
description: DESCRIPTION,
siteName: TITLE,
type: 'website',
url: 'https://tldraw.dev',
images: FACEBOOK_CARD,
},
twitter: {
creator: TWITTER_HANDLE,
description: DESCRIPTION,
card: 'summary_large_image',
images: TWITTER_CARD,
},
applicationName: TITLE,
appleWebApp: {
capable: true,
title: TITLE,
statusBarStyle: 'black',
},
formatDetection: {
telephone: false,
},
icons: [
{ rel: 'shortcut icon', url: '/favicon.svg' },
{ rel: 'icon', url: 'favicon-32x32.svg', sizes: '32x32' },
{ rel: 'icon', url: 'favicon-16x16.svg', sizes: '16x16' },
{ rel: 'apple-touch-icon', url: 'apple-touch-icon.png' },
{ rel: 'apple-touch-icon', url: 'apple-touch-icon-152x152.svg', sizes: '152x152' },
{ rel: 'apple-touch-icon', url: 'apple-touch-icon-180x180.svg', sizes: '180x180' },
{ rel: 'apple-touch-icon', url: 'apple-touch-icon-167x167.svg', sizes: '167x167' },
],
}
export const viewport: Viewport = {
initialScale: 1,
maximumScale: 1,
width: 'device-width',
height: 'device-height',
themeColor: THEME_COLOR,
}
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<AutoRefresh>
<html suppressHydrationWarning>
<body>
<Providers>
{children}
<Analytics />
</Providers>
</body>
</html>
</AutoRefresh>
)
}

Wyświetl plik

@ -1,23 +0,0 @@
import { Header } from '@/components/Header'
import { Sidebar } from '@/components/Sidebar'
import { getDb } from '@/utils/ContentDatabase'
export default async function NotFound() {
const db = await getDb()
const sidebar = await db.getSidebarContentList({})
return (
<div className="wrapper">
<div className="layout">
<Header />
<Sidebar {...sidebar} />
<main className="main-content article">
<div className="page-header">
<h1>Not found.</h1>
</div>
<p>There's nothing here. :(</p>
</main>
</div>
</div>
)
}

Wyświetl plik

@ -1,8 +0,0 @@
'use client'
import { ThemeProvider } from 'next-themes'
import { ReactNode } from 'react'
export function Providers({ children }: { children: ReactNode }) {
return <ThemeProvider enableSystem>{children}</ThemeProvider>
}

Wyświetl plik

@ -1,31 +0,0 @@
import { Article } from '@/types/content-types'
import { Icon } from './Icon'
type ArticleDetailsProps = {
article: Article
}
const ROOT_CONTENT_URL = `https://github.com/tldraw/tldraw/blob/main/apps/docs/content/`
export function ArticleDetails({ article: { sourceUrl, date } }: ArticleDetailsProps) {
return (
<div className="article__details">
{sourceUrl && (
<a className="article__details__edit" href={`${ROOT_CONTENT_URL}${sourceUrl}`}>
<Icon icon="edit" />
<span>Edit this page</span>
</a>
)}
{date && (
<div className="article__details__timestamp">
Last edited on{' '}
{Intl.DateTimeFormat('en-gb', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(new Date(date))}
</div>
)}
</div>
)
}

Wyświetl plik

@ -1,39 +0,0 @@
import { Article } from '@/types/content-types'
import { getDb } from '@/utils/ContentDatabase'
import { ArticleDetails } from './ArticleDetails'
import { ArticleHeadingLinks } from './ArticleHeadingLinks'
import { ArticleNavLinks } from './ArticleNavLinks'
import { Header } from './Header'
import { Mdx } from './Mdx'
import { Sidebar } from './Sidebar'
import { Image } from './mdx-components/generic'
export async function ArticleDocsPage({ article }: { article: Article }) {
const db = await getDb()
const section = await db.getSection(article.sectionId)
const category = await db.getCategory(article.categoryId)
const headings = await db.getArticleHeadings(article.id)
const links = await db.getArticleLinks(article)
const sidebar = await db.getSidebarContentList({
sectionId: section.id,
categoryId: category.id,
articleId: article.id,
})
return (
<>
<Header sectionId={section.id} />
<Sidebar {...sidebar} />
<main className="main-content article">
<div className="page-header">
<h1>{article.title}</h1>
</div>
{article.hero && <Image alt="hero" title={article.title} src={`images/${article.hero}`} />}
{article.content && <Mdx content={article.content} />}
<ArticleDetails article={article} />
{links && <ArticleNavLinks links={links} />}
</main>
<ArticleHeadingLinks article={article} headingLinks={headings} />
</>
)
}

Wyświetl plik

@ -1,46 +0,0 @@
/* eslint-disable no-useless-escape */
import { Article, ArticleHeading, ArticleHeadings } from '@/types/content-types'
import Link from 'next/link'
export function ArticleHeadingLinks({
headingLinks,
}: {
article: Article
headingLinks: ArticleHeadings
}) {
const linksToShow = headingLinks.filter((heading) => heading.level < 4)
if (linksToShow.length <= 1) return null
return (
<nav className="layout__headings">
<ul className="sidebar__list sidebar__sections__list">
<li className="sidebar__section">
<div className="sidebar__section__title uppercase_title">On this page</div>
<ul className="sidebar__list">
{linksToShow.map((heading) => (
<HeaderLink key={heading.slug} heading={heading} />
))}
</ul>
</li>
</ul>
</nav>
)
}
function HeaderLink({ heading }: { heading: ArticleHeading }) {
return (
<li className="sidebar__article">
<Link href={`#${heading.slug}`} className="sidebar__link">
{heading.level > 2 ? <span className="sidebar__link__indent">{'–'}</span> : null}
<span className="sidebar__link__title">
{heading.isCode ? (
<code>{heading.title.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')}</code>
) : (
heading.title.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
)}
</span>
</Link>
</li>
)
}

Wyświetl plik

@ -1,26 +0,0 @@
import { ArticleLinks } from '@/types/content-types'
import Link from 'next/link'
import { Icon } from './Icon'
type ArticleNavLinksProps = {
links: ArticleLinks
}
export async function ArticleNavLinks({ links: { prev, next } }: ArticleNavLinksProps) {
return (
<div className="article__links">
{prev && (
<Link href={prev.path ?? '/'} className="article__links__link article__links__prev">
<Icon icon="arrow-left" />
<span>{prev.title}</span>
</Link>
)}
{next && (
<Link href={next.path ?? '/'} className="article__links__link article__links__next">
<span>{next.title}</span>
<Icon icon="arrow-right" />
</Link>
)}
</div>
)
}

Wyświetl plik

@ -1,36 +0,0 @@
import { Article } from '@/types/content-types'
import { getDb } from '@/utils/ContentDatabase'
import { ArticleNavLinks } from './ArticleNavLinks'
import { Breadcrumb } from './Breadcrumb'
import { Header } from './Header'
import { Mdx } from './Mdx'
import { Sidebar } from './Sidebar'
import { Image } from './mdx-components/generic'
export async function ArticleReferenceDocsPage({ article }: { article: Article }) {
const db = await getDb()
const section = await db.getSection(article.sectionId)
const category = await db.getCategory(article.categoryId)
const links = await db.getArticleLinks(article)
const sidebar = await db.getSidebarContentList({
sectionId: section.id,
categoryId: category.id,
articleId: article.id,
})
return (
<>
<Header sectionId={section.id} />
<Sidebar {...sidebar} />
<main className="main-content article article__api-docs">
<div className="page-header">
<Breadcrumb section={section} category={category} />
<h1>{article.title}</h1>
</div>
{article.hero && <Image alt="hero" title={article.title} src={`images/${article.hero}`} />}
{article.content && <Mdx content={article.content} />}
{links && <ArticleNavLinks links={links} />}
</main>
</>
)
}

Wyświetl plik

@ -1,30 +0,0 @@
'use client'
import { useRouter } from 'next/navigation'
import { ReactNode, useEffect } from 'react'
let AutoRefresh = ({ children }: { children: ReactNode }) => {
return children
}
if (process.env.NODE_ENV === 'development') {
AutoRefresh = function AutoRefresh({ children }) {
const router = useRouter()
useEffect(() => {
const ws = new WebSocket('ws://localhost:3201')
ws.onmessage = (event) => {
if (event.data === 'refresh') {
router.refresh()
}
}
return () => {
ws.close()
}
}, [router])
return children
}
}
export default AutoRefresh

Wyświetl plik

@ -1,223 +0,0 @@
.autocomplete__wrapper {
position: relative;
display: flex;
height: 40px;
flex-direction: row;
border-radius: 24px;
padding: 0 16px;
border: 1px solid var(--color-tint-2);
}
.autocomplete__wrapper:focus-within {
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: 12px;
padding-left: 8px;
font-weight: 500;
margin-bottom: 4px;
position: relative;
letter-spacing: 0.5px;
height: 40px;
display: flex;
align-items: center;
justify-content: flex-start;
color: var(--color-text-secondary);
text-transform: uppercase;
--bg: transparent;
white-space: nowrap;
}
.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;
}
.autocomplete__popover {
margin-top: 4px;
position: relative;
z-index: 50;
min-width: 240px;
font-size: 14px;
display: flex;
max-height: min(var(--popover-available-height, 300px), 300px);
flex-direction: column;
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 14px -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 14px -3px rgb(0 0 0 / 0.25),
0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.autocomplete__empty {
padding: 0px 8px;
}
.autocomplete__item {
position: relative;
padding: 0px 8px;
display: flex;
cursor: pointer;
align-items: center;
justify-content: flex-start;
color: var(--color-text);
--bg: transparent;
height: 32px;
flex-grow: 2;
font-size: 14px;
}
.autocomplete__item::after {
position: absolute;
display: block;
content: '';
inset: 1px 0px;
background-color: var(--bg);
border-radius: var(--border-radius-menu);
transition: background-color 0.12s ease-in-out;
transition-delay: 0s;
}
.autocomplete__item[data-active-item]::after {
background-color: var(--color-tint-2);
}
@media (hover: hover) {
.autocomplete__item:hover {
color: var(--color-text);
}
.autocomplete__item:hover::after {
--bg: var(--color-tint-1);
}
}
.autocomplete__item:active,
.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%);
}

Wyświetl plik

@ -1,116 +0,0 @@
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 key={group}>
{groupsToLabel?.[group] && (
<ComboboxGroupLabel key={`${group}-group`} 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 placeholder="Search…" ref={ref} className="autocomplete__input" value={value} />
{value && <ComboboxCancel className="autocomplete__cancel" />}
{value && options.length !== 0 && (
<ComboboxPopover sameWidth className="autocomplete__popover">
{customUI}
{renderedGroups}
</ComboboxPopover>
)}
</div>
</ComboboxProvider>
)
})
export { Autocomplete }

Wyświetl plik

@ -1,28 +0,0 @@
import { Category, Section } from '@/types/content-types'
import Link from 'next/link'
export function Breadcrumb({ section, category }: { section?: Section; category?: Category }) {
return (
<div className="breadcrumb">
{section && (
<>
{section.title && section.id === 'getting-started' ? (
section.title
) : (
<Link href={`/${section.id}`}>{section.title}</Link>
)}
{category && (
<>
{category.id === section.id + '_ucg' ? null : (
<>
{` / `}
<Link href={`/${section.id}/${category.id}`}>{category.title}</Link>
</>
)}
</>
)}
</>
)}
</div>
)
}

Wyświetl plik

@ -1,35 +0,0 @@
import { Category } from '@/types/content-types'
import { getDb } from '@/utils/ContentDatabase'
import Link from 'next/link'
import { Breadcrumb } from './Breadcrumb'
import { Header } from './Header'
import { Sidebar } from './Sidebar'
export async function CategoryDocsPage({ category }: { category: Category }) {
const db = await getDb()
const section = await db.getSection(category.sectionId)
const sidebar = await db.getSidebarContentList({ sectionId: category.sectionId })
const articles = await db.getCategoryArticles(section.id, category.id)
return (
<>
<Header sectionId={section.id} />
<Sidebar {...sidebar} />
<main className={'article'}>
<div className="page-header">
<Breadcrumb section={section} category={category} />
<h1>{category.title}</h1>
</div>
{articles.length > 0 && (
<ul>
{articles.map((article) => (
<li key={article.id}>
<Link href={`/${section.id}/${category.id}/${article.id}`}>{article.title}</Link>
</li>
))}
</ul>
)}
</main>
</>
)
}

Wyświetl plik

@ -1,50 +0,0 @@
'use client'
import { SandpackCodeViewer, SandpackFiles, SandpackProvider } from '@codesandbox/sandpack-react'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export default function ExampleCodeBlock({
articleId,
files = {},
activeFile,
}: {
articleId: string
activeFile: string
files: SandpackFiles
}) {
const [isClientSide, setIsClientSide] = useState(false)
const { theme } = useTheme()
useEffect(() => setIsClientSide(true), [])
const SERVER =
process.env.NODE_ENV === 'development' ? 'http://localhost:5420' : 'https://examples.tldraw.com'
// This is to avoid hydration mismatch between the server and the client because of the useTheme.
if (!isClientSide) {
return null
}
return (
<div className="code-example">
<iframe src={`${SERVER}/${articleId}/full`} />
<SandpackProvider
className="sandpack"
key={`sandpack-${theme}-${activeFile}`}
template="react-ts"
options={{ activeFile }}
customSetup={{
dependencies: {
'@tldraw/assets': 'latest',
tldraw: 'latest',
},
}}
files={{
...files,
}}
theme={theme === 'dark' ? 'dark' : 'light'}
>
<SandpackCodeViewer />
</SandpackProvider>
</div>
)
}

Wyświetl plik

@ -1,42 +0,0 @@
import { Article } from '@/types/content-types'
import { getDb } from '@/utils/ContentDatabase'
import { ArticleNavLinks } from './ArticleNavLinks'
import ExampleCodeBlock from './ExampleCodeBlock'
import { Header } from './Header'
import { Mdx } from './Mdx'
import { Sidebar } from './Sidebar'
export async function ExampleDocsPage({ article }: { article: Article }) {
const db = await getDb()
const section = await db.getSection(article.sectionId)
const category = await db.getCategory(article.categoryId)
const links = await db.getArticleLinks(article)
const sidebar = await db.getSidebarContentList({
sectionId: section.id,
categoryId: category.id,
articleId: article.id,
})
return (
<>
<Header sectionId={section.id} />
<Sidebar {...sidebar} />
<main className={`main-content article article__example`}>
<div className="page-header">
<h1>{article.title}</h1>
{article.description && <p>{article.description}</p>}
</div>
{article.content && <Mdx content={article.content} />}
<ExampleCodeBlock
articleId={article.id}
files={{
'App.tsx': article.componentCode,
...(article.componentCodeFiles ? JSON.parse(article.componentCodeFiles) : null),
}}
activeFile={'App.tsx'}
/>
{links && <ArticleNavLinks links={links} />}
</main>
</>
)
}

Wyświetl plik

@ -1,43 +0,0 @@
'use client'
import { useRef } from 'react'
export default function FancyBox() {
const rContainer = useRef<HTMLDivElement>(null)
// const [items, setItems] = useState<number[]>([])
// useEffect(() => {
// const populate = debounce(() => {
// const elm = rContainer.current
// if (!elm) return
// const width = elm.clientWidth
// const height = elm.clientHeight
// const SIZE = 32
// const cols = Math.ceil(width / SIZE)
// const rows = Math.ceil(height / SIZE)
// const items = Array.from(Array(cols * rows)).map((_, i) => i)
// setItems(items)
// }, 100)
// populate()
// window.addEventListener('resize', populate)
// return () => {
// window.removeEventListener('resize', populate)
// }
// }, [])
return (
<div className="footer__fancybox" ref={rContainer}>
{/* {items.map((i) => {
const c = 1 + (i % 7)
return <div key={i} className="footer__fancybox__item" data-c={c} />
})} */}
</div>
)
}

Wyświetl plik

@ -1,41 +0,0 @@
import dynamic from 'next/dynamic'
import { Icon } from './Icon'
const FancyBox = dynamic(async () => await import('./FancyBox'), { ssr: false })
export function Footer() {
return (
<div className="footer">
<FancyBox />
<a className="footer__lockup" href="https://tldraw.com">
<span
className="footer__lockup__icon"
style={{
mask: `url(/tldraw-icon.svg) center 100% / 100% no-repeat`,
WebkitMask: `url(/tldraw-icon.svg) center 100% / 100% no-repeat`,
}}
/>
<p>tldraw © {new Date().getFullYear()}</p>
</a>
<div className="footer__socials">
<a href="https://x.com/tldraw" className="sidebar__button icon-button" title="x">
<Icon icon="twitter" />
</a>
<a
href="https://github.com/tldraw/tldraw"
className="sidebar__button icon-button"
title="github"
>
<Icon icon="github" />
</a>
<a
href="https://discord.com/invite/SBBEVCA4PG"
className="sidebar__button icon-button"
title="discord"
>
<Icon icon="discord" />
</a>
</div>
</div>
)
}

Wyświetl plik

@ -1,83 +0,0 @@
'use client'
import Link from 'next/link'
import { Icon } from './Icon'
import { Search } from './Search'
import { ThemeSwitcher } from './ThemeSwitcher'
export function Header({ sectionId }: { sectionId?: string }) {
return (
<div className="layout__header">
<div className="layout__header__left">
<Link href="/quick-start">
<img className="logo-dark" src="/tldraw_dev_dark.png" />
<img className="logo-light" src="/tldraw_dev_light.png" />
</Link>
</div>
<Search />
<div className="layout__header__links">
<div className="layout__header__sections">
<SectionLinks sectionId={sectionId} />
</div>
<div className="layout__header__socials">
<a
href="https://x.com/tldraw/"
className="sidebar__button icon-button"
title="twitter"
target="_blank"
>
<Icon icon="twitter" />
</a>
<a
href="https://discord.com/invite/SBBEVCA4PG"
className="sidebar__button icon-button"
title="discord"
target="_blank"
>
<Icon icon="discord" />
</a>
<a
href="https://github.com/tldraw/tldraw"
className="sidebar__button icon-button"
title="github"
target="_blank"
>
<Icon icon="github" />
</a>
<ThemeSwitcher />
</div>
</div>
</div>
)
}
export function SectionLinks({ sectionId }: { sectionId?: string | null }) {
return (
<>
<a
href="/quick-start"
title="Learn"
data-active={!['reference', 'examples'].includes(sectionId || '')}
className="layout_header__section"
>
Learn
</a>
<a
href="/reference/editor/Editor"
title="Reference"
data-active={sectionId === 'reference'}
className="layout_header__section"
>
Reference
</a>
<a
href="/examples/basic/basic"
title="Examples"
data-active={sectionId === 'examples'}
className="layout_header__section"
>
Examples
</a>
</>
)
}

Wyświetl plik

@ -1,3 +0,0 @@
export function HeroImage() {
return <></>
}

Wyświetl plik

@ -1,19 +0,0 @@
export function Icon({
icon,
className,
small,
}: {
small?: boolean
icon: string
className?: string
}) {
return (
<span
className={`icon ${small ? 'small ' : ''}${className ?? ''}`}
style={{
mask: `url(/icons/${icon}.svg) center 100% / 100% no-repeat`,
WebkitMask: `url(/icons/${icon}.svg) center 100% / 100% no-repeat`,
}}
/>
)
}

Wyświetl plik

@ -1,22 +0,0 @@
import classNames from 'classnames'
export function Chevron({ className }: { className?: string }) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={classNames('accordion__trigger__chevron', className)}
>
<path
d="M4 6L8 10L12 6"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}

Wyświetl plik

@ -1,30 +0,0 @@
import { MDXRemote } from 'next-mdx-remote/rsc'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeHighlight from 'rehype-highlight'
import rehypeSlug from 'rehype-slug-custom-id'
import { components } from './mdx-components'
interface MdxProps {
content: string
}
export function Mdx({ content }: MdxProps) {
return (
<MDXRemote
source={content}
components={components}
options={{
mdxOptions: {
// remarkPlugins: [remarkGfm, {}],
rehypePlugins: [
[rehypeHighlight as any, {}],
[rehypeAutolinkHeadings, {}],
[rehypeSlug, { enableCustomId: true, maintainCase: true, removeAccents: true }],
],
format: 'mdx',
},
parseFrontmatter: true,
}}
/>
)
}

Wyświetl plik

@ -1,119 +0,0 @@
'use client'
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 { Autocomplete, DropdownOption } from './Autocomplete'
const HOST_URL = typeof location !== 'undefined' ? location.origin : 'https://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 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 topExamples = json.results.examples.slice(0, 5)
const topArticles = json.results.articles.slice(0, 10)
const topAPI = json.results.apiDocs.slice(0, 20)
const allResults = topExamples.concat(topArticles).concat(topAPI)
if (allResults.length) {
setSearchResults(
allResults.map((result: SearchResult) => ({
label: result.title,
value: result.url,
group: result.sectionType,
}))
)
} else {
setSearchResults([{ label: 'No results found.', value: '#', group: 'docs' }])
}
}
} catch (err) {
console.error(err)
}
setIsLoading(false)
}
handleFetchResults()
}, [query, searchType])
const handleChange = (url: string) => {
router.push(url.startsWith('/') ? url : `/${url}`)
}
// AI is turned off for now.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleSearchTypeChange = () => {
setSearchResults([])
setSearchType(searchType === SEARCH_TYPE.AI ? SEARCH_TYPE.NORMAL : SEARCH_TYPE.AI)
handleInputChange(query)
}
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()
})
return (
<div className="search__wrapper">
<Autocomplete
ref={rInput}
// customUI={
// <button className="search__ai-toggle" onClick={handleSearchTypeChange}>
// {searchType === SEARCH_TYPE.NORMAL ? '✨ Search using AI' : '⭐ Search without AI'}
// </button>
// }
groups={['docs', 'examples', 'reference']}
groupsToLabel={{ examples: 'Examples', docs: 'Articles', reference: 'Reference' }}
options={searchResults}
isLoading={isLoading}
onInputChange={handleInputChange}
onChange={handleChange}
/>
{platform && (
<span className="search__keyboard">
<kbd data-platform={platform === 'mac' ? 'mac' : 'win'}>
<span>{platform === 'mac' ? '⌘' : 'Ctrl'}</span>
<span>K</span>
</kbd>
</span>
)}
</div>
)
}

Wyświetl plik

@ -1,22 +0,0 @@
import { Section } from '@/types/content-types'
import { getDb } from '@/utils/ContentDatabase'
import { Header } from './Header'
import { Sidebar } from './Sidebar'
export async function SectionDocsPage({ section }: { section: Section }) {
const db = await getDb()
const sidebar = await db.getSidebarContentList({ sectionId: section.id })
return (
<>
<Header sectionId={section.id} />
<Sidebar {...sidebar} />
<main className="main-content article">
<div className="page-header">
<h1>{section.title}</h1>
</div>
Choose your adventure on the left.
</main>
</>
)
}

Wyświetl plik

@ -1,227 +0,0 @@
'use client'
import {
APIGroup,
ArticleHeadings,
SidebarContentArticleLink,
SidebarContentCategoryLink,
SidebarContentLink,
SidebarContentList,
SidebarContentSectionLink,
} from '@/types/content-types'
import * as Accordion from '@radix-ui/react-accordion'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { UIEvent, createContext, useContext, useEffect, useRef } from 'react'
import { SectionLinks } from './Header'
import { Chevron } from './Icons'
import { Search } from './Search'
import { SidebarCloseButton } from './SidebarCloseButton'
import { ToggleMenuButton } from './ToggleMenuButton'
type SidebarProps = SidebarContentList
const linkContext = createContext<{
activeId: string | null
articleId: string | null
categoryId: string | null
sectionId: string | null
} | null>(null)
// N.B. UGH, the sidebar's scroll position keeps getting lost when navigation occurs
// and we keep track of the last known position here outside of the component because
// it keeps re-rendering.
let scrollPosition = 0
export function Sidebar({ links, sectionId, categoryId, articleId }: SidebarProps) {
const activeId = articleId ?? categoryId ?? sectionId
const sidebarRef = useRef<HTMLDivElement>(null)
const pathName = usePathname()
useEffect(() => {
document.body.classList.remove('sidebar-open')
const sidebarEl = sidebarRef.current
if (!sidebarEl) return
sidebarEl.scrollTo(0, scrollPosition)
const activeLink = document.querySelector('.sidebar__nav [data-active=true]') as HTMLElement
if (
activeLink &&
(activeLink.offsetTop < sidebarEl.scrollTop ||
activeLink.offsetTop > sidebarEl.scrollTop + sidebarEl.clientHeight)
) {
// The above will *mostly* work to keep the position but we have some accordions that will collapse
// (like in the Reference docs) and we need to scroll to the active item.
activeLink.scrollIntoView({ block: 'center' })
}
}, [pathName])
const handleScroll = (e: UIEvent) => {
e.stopPropagation()
scrollPosition = sidebarRef.current?.scrollTop ?? 0
}
return (
<>
<linkContext.Provider value={{ activeId, articleId, categoryId, sectionId }}>
<div ref={sidebarRef} className="sidebar scroll-light" onScroll={handleScroll}>
<Search />
<div className="sidebar__section__links">
<SectionLinks sectionId={sectionId} />
</div>
<SidebarLinks links={links} />
<SidebarCloseButton />
</div>
<ToggleMenuButton />
</linkContext.Provider>
</>
)
}
export function SidebarLinks({ links }: { links: SidebarContentLink[] }) {
return (
<nav className="sidebar__nav">
<ul className="sidebar__list sidebar__sections__list">
{links.map((link) => (
<SidebarLink key={link.url} {...link} />
))}
</ul>
</nav>
)
}
function SidebarLink(props: SidebarContentLink) {
switch (props.type) {
case 'section': {
return <SidebarSection {...props} />
}
case 'article': {
return <SidebarArticle {...props} />
}
case 'category': {
return <SidebarCategory {...props} />
}
}
}
function SidebarSection({ title, children }: SidebarContentSectionLink) {
if (children.length === 0) return null
return (
<li className="sidebar__section">
{title && <span className="sidebar__section__title uppercase_title">{title}</span>}
<ul className="sidebar__list">
{children.map((link) => (
<SidebarLink key={link.url} {...link} />
))}
</ul>
</li>
)
}
function SidebarCategory({ title, children }: SidebarContentCategoryLink) {
const linkCtx = useContext(linkContext)
if (children.length === 0) return null
const hasGroups = children.some((child) => !!(child as SidebarContentArticleLink).groupId)
const activeArticle = children.find(
(child) => (child as SidebarContentArticleLink).articleId === linkCtx?.articleId
)
const activeGroup = activeArticle && (activeArticle as SidebarContentArticleLink).groupId
const groups = Object.values(APIGroup)
return (
<li className="sidebar__category">
{hasGroups ? (
<>
<span className="sidebar__link sidebar__category__title">{title}</span>
<Accordion.Root
type="multiple"
defaultValue={[`${linkCtx?.categoryId}-${activeGroup}-${linkCtx?.articleId}`]}
>
{groups.map((group) => {
const articles = children.filter(
(child) => (child as SidebarContentArticleLink).groupId === group
)
if (articles.length === 0) return null
const value = `${linkCtx?.categoryId}-${group}-${linkCtx?.articleId}`
return (
<Accordion.Item key={value} value={value}>
<Accordion.Trigger className="sidebar__section__group__title">
{group}
<Chevron />
</Accordion.Trigger>
<Accordion.Content>
<ul className="sidebar__list sidebar__group">
{articles.map((link) => (
<SidebarLink key={link.url} {...link} />
))}
</ul>
</Accordion.Content>
</Accordion.Item>
)
})}
</Accordion.Root>
</>
) : (
<>
<Link
href={children[0].url}
title={title}
className="sidebar__link sidebar__category__title"
>
{title}
</Link>
<ul className="sidebar__list">
{children.map((link) => (
<SidebarLink key={link.url} {...link} />
))}
</ul>
</>
)}
<hr />
</li>
)
}
function SidebarArticle({
title,
url,
articleId,
headings,
}: SidebarContentArticleLink & { headings?: ArticleHeadings }) {
const activeLink = useContext(linkContext)
const isActive = activeLink?.activeId === articleId
return (
<li className="sidebar__article">
<Link href={url} title={title} className="sidebar__link" data-active={isActive}>
{title}
</Link>
{isActive && (
<ul className="sidebar__list">
{headings
?.filter((heading) => heading.level < 4)
.map((heading) => (
<li
key={`${heading.slug}`}
data-heading-level={heading.title === 'Constructor' ? 2 : heading.level}
>
<Link href={`#${heading.slug}`} title={heading.title} className="sidebar__link">
{heading.isCode ? (
<code>{heading.title.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')}</code>
) : (
heading.title.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
)}
</Link>
</li>
))}
</ul>
)}
</li>
)
}

Wyświetl plik

@ -1,15 +0,0 @@
import { Icon } from './Icon'
export function SidebarCloseButton() {
return (
<div className="sidebar__close">
<span onClick={() => document.body.classList.toggle('sidebar-open')}>Close</span>
<button
className="icon-button"
onClick={() => document.body.classList.toggle('sidebar-open')}
>
<Icon icon="close" small />
</button>
</div>
)
}

Wyświetl plik

@ -1,20 +0,0 @@
// TODO(mime): copied from tldraw package, needs to be in another shared location.
export function Spinner(props: React.SVGProps<SVGSVGElement>) {
return (
<svg width={16} height={16} viewBox="0 0 16 16" {...props}>
<g strokeWidth={2} fill="none" fillRule="evenodd">
<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>
)
}

Wyświetl plik

@ -1,17 +0,0 @@
'use client'
import { useTheme } from 'next-themes'
import { Icon } from './Icon'
export function ThemeSwitcher() {
const { theme, setTheme } = useTheme()
return (
<button
className="sidebar__button icon-button"
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
<Icon icon="light" />
</button>
)
}

Wyświetl plik

@ -1,12 +0,0 @@
import { Icon } from './Icon'
export function ToggleMenuButton() {
return (
<button
className="menu__button icon-button"
onClick={() => document.body.classList.toggle('sidebar-open')}
>
<Icon icon="menu" small />
</button>
)
}

Wyświetl plik

@ -1 +0,0 @@
export {}

Wyświetl plik

@ -1,29 +0,0 @@
import { ReactNode } from 'react'
export function ParametersTable({ children }: { children: ReactNode }) {
return (
<div className="article__parameters-table__wrapper">
<table className="article__parameters-table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>{children}</tbody>
</table>
</div>
)
}
export function ParametersTableRow({ children }: { children: ReactNode }) {
return <tr className="article__parameters-table__row">{children}</tr>
}
export function ParametersTableName({ children }: { children: ReactNode }) {
return <td className="article__parameters-table__name">{children}</td>
}
export function ParametersTableDescription({ children }: { children: ReactNode }) {
return <td className="article__parameters-table__description">{children}</td>
}

Wyświetl plik

@ -1,44 +0,0 @@
'use client'
import { SandpackCodeViewer, SandpackFiles, SandpackProvider } from '@codesandbox/sandpack-react'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export const Code = (props: any) => {
return <code {...props} />
}
export function CodeBlock({ code }: { code: SandpackFiles }) {
const [isClientSide, setIsClientSide] = useState(false)
const { theme } = useTheme()
useEffect(() => setIsClientSide(true), [])
// This is to avoid hydration mismatch between the server and the client because of the useTheme.
if (!isClientSide) {
return null
}
const trimmedCode = Object.fromEntries(
Object.entries(code).map(([key, value]) => [key, (value as string).trim()])
)
return (
<div className="code-example">
<SandpackProvider
className="sandpack"
key={`sandpack-${theme}`}
template="react-ts"
options={{ activeFile: Object.keys(code)[0] }}
customSetup={{
dependencies: {
'@tldraw/assets': 'latest',
tldraw: 'latest',
},
}}
files={trimmedCode}
theme={theme === 'dark' ? 'dark' : 'light'}
>
<SandpackCodeViewer />
</SandpackProvider>
</div>
)
}

Wyświetl plik

@ -1,174 +0,0 @@
import React from 'react'
/* ---------------------- Lists --------------------- */
export const UnorderedList = (props: any) => {
return <ul {...props} />
}
export const OrderedList = (props: any) => {
return <ol {...props} />
}
export const ListItem = (props: any) => {
return <li {...props} />
}
/* ------------------- Typography ------------------- */
type Heading = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
function heading(heading: Heading, props: any) {
const Element = ({ ...props }) => React.createElement(heading, props)
if (props.id) {
return (
<Element {...props}>
<a className="anchor" href={`#${props.id}`}>
{props.children}
</a>
</Element>
)
}
return <Element {...props} />
}
export const Heading1 = (props: any) => {
return heading('h1', props)
}
export const Heading2 = (props: any) => {
return heading('h2', props)
}
export const Heading3 = (props: any) => {
return heading('h3', props)
}
export const Heading4 = (props: any) => {
return heading('h4', props)
}
export const Heading5 = (props: any) => {
return heading('h5', props)
}
export const Heading6 = (props: any) => {
return heading('h6', props)
}
export const Paragraph = (props: any) => {
return <p {...props} />
}
export const A = (props: any) => {
const isLocalUrl = props.href.startsWith('/') || props.href.startsWith('#')
let maybeParsedUrl
try {
maybeParsedUrl = isLocalUrl ? null : new URL(props.href)
} catch (e) {
console.error(`Invalid URL: ${props.href}`)
}
const derivedTarget =
isLocalUrl ||
maybeParsedUrl?.host.includes('tldraw.com') ||
maybeParsedUrl?.host.includes('localhost')
? undefined
: '_blank'
const target = props.target ?? derivedTarget
return <a {...props} target={target} />
}
export const Divider = (props: any) => {
return <hr {...props} />
}
export const Blockquote = (props: any) => {
return <blockquote {...props} />
}
export const Small = (props: any) => {
return (
<p className="article__small">
<small {...props} />
</p>
)
}
/* --------------------- Tables --------------------- */
export const Table = (props: any) => {
return <table {...props} />
}
export const THead = (props: any) => {
return <thead {...props} />
}
export const TR = (props: any) => {
return <tr {...props} />
}
export const TD = (props: any) => {
return <td {...props} />
}
/* --------------------- Media --------------------- */
export const Image = (props: any) => {
return (
<a className="article__image" href={props.href ?? props.src}>
<img alt={props.title} {...props} />
{props.caption && <span className="article__caption">{props.caption}</span>}
</a>
)
}
export const Video = (props: any) => {
return (
<span className="article__video">
<video alt={props.title} {...props} />
{props.caption && <span className="article__caption">{props.caption}</span>}
</span>
)
}
/* ------------------- Code Blocks ------------------ */
export const Pre = (props: any) => {
if (props.children?.props?.className.startsWith('language-')) {
return props.children
}
return <pre {...props} />
}
export const Footnotes = (props: any) => {
return <div {...props} />
}
/* -------------------- API docs -------------------- */
export const ApiHeading = (props: any) => {
return <div className="article__api-heading uppercase_title" {...props} />
}
export const Embed = (props: any) => {
return (
<div className={props.className || 'article__embed'}>
<iframe className="iframe" src={props.src} width="100%" height={600} />
{props.caption && <span className="article__caption">{props.caption}</span>}
</div>
)
}
/* -------------------- Callouts -------------------- */
export const Callout = ({ icon, children }: any) => {
return (
<div className="article__callout">
<span>{icon}</span>
<p>{children}</p>
</div>
)
}

Wyświetl plik

@ -1,63 +0,0 @@
import * as customComponents from '../article-components'
import * as apiComponents from './api-docs'
import {
A,
ApiHeading,
Blockquote,
Callout,
Divider,
Embed,
Heading1,
Heading2,
Heading3,
Heading4,
Heading5,
Heading6,
Image,
ListItem,
OrderedList,
Paragraph,
Pre,
Small,
TD,
THead,
TR,
Table,
UnorderedList,
Video,
} from './generic'
import { Code, CodeBlock } from './code'
export const components = {
a: A,
blockquote: Blockquote,
code: Code,
h1: Heading1,
h2: Heading2,
h3: Heading3,
h4: Heading4,
h5: Heading5,
h6: Heading6,
hr: Divider,
img: Image,
li: ListItem,
ol: OrderedList,
p: Paragraph,
pre: Pre,
table: Table,
td: TD,
thead: THead,
tr: TR,
ul: UnorderedList,
video: Video,
ApiHeading,
CodeBlock,
Embed,
Image,
Small: Small,
Video,
Callout,
...customComponents,
...apiComponents,
}

Wyświetl plik

@ -1,4 +0,0 @@
declare module '*/content.json' {
const content: any
export default content
}

Wyświetl plik

@ -1,23 +0,0 @@
[
{
"id": "steveruizok",
"name": "Steve Ruiz",
"email": "steve@tldraw.com",
"twitter": "steveruizok",
"image": "steve_ruiz.jpg"
},
{
"id": "tldraw",
"name": "tldraw",
"email": "sales@tldraw.com",
"twitter": "tldraw",
"image": "tldraw.jpg"
},
{
"id": "api",
"name": "API",
"email": "sales@tldraw.com",
"twitter": "tldraw",
"image": "api.jpg"
}
]

Wyświetl plik

@ -1,13 +0,0 @@
---
title: Contributing
status: published
author: steveruizok
date: 3/22/2023
order: 1
---
Interested in contributing to the project?
You can find tldraw's source code on GitHub at [github.com/tldraw/tldraw](https://github.com/tldraw/tldraw). See our [Contributing guide](https://github.com/tldraw/tldraw/blob/main/CONTRIBUTING.md) for more information.
To contribute translations to the project, please see our [translations guide](/community/translations).

Wyświetl plik

@ -1,16 +0,0 @@
---
title: License
status: published
author: steveruizok
date: 11/7/2023
order: 2
---
tldraw's source code and distributed packages are provided under the non-commercial [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md).
This license does not permit commercial use. If you wish to use tldraw in a commercial product or enterprise, you will need to purchase a commercial license. To obtain a commercial license, please contact us at [sales@tldraw.com](mailto:sales@tldraw.com).
## Trademarks
While the copyright to our open source software is licensed under the tldraw license, our trademarks appearing in or on the open source software are the exclusive property of tldraw. Our open source license does not include a license to use our trademarks.
Please see our [trademark policy](https://github.com/tldraw/tldraw/blob/main/TRADEMARKS.md) for more information.

Wyświetl plik

@ -1,13 +0,0 @@
---
title: Translations
status: published
author: steveruizok
date: 11/7/2023
order: 0
---
The tldraw user interface (in [@tldraw/ui](/docs/user-interface)) is currently translated into over thirty different languages, with twenty languages at above 70% completion. Where a key's translation is missing in the user's current language language, the default (English) translation will be used instead.
## Contributing translations
We manage our translations through [Lokalise](https://www.lokalise.com), a long-time tldraw sponsor. If you would like to help by translating or reviewing translations, please let us know on [Discord](https://discord.gg/sKNgCZyrrf) so that we can add you to the project.

Wyświetl plik

@ -1,26 +0,0 @@
---
title: Assets
status: published
author: steveruizok
date: 6/9/2023
order: 3
keywords:
- image
- video
- file
- images
- videos
- files
---
Assets are dynamic records that store data about a shared asset. For example, our image and video shapes refer to assets rather than embedding their source files directly. This is essential because, by default, these asset sources are stored in base64 data.
You can use assets for any shared piece of information, however they're best used for images, videos, and files.
## Examples
While we're working on docs for this part of the project, please refer to the examples below:
- [Using images hosted](https://github.com/tldraw/tldraw/blob/main/apps/examples/src/examples/hosted-images/HostedImagesExample.tsx)
- [Customizing the default asset options](https://github.com/tldraw/tldraw/blob/main/apps/examples/src/examples/asset-props/AssetPropsExample.tsx)
- [Handling pasted / dropped external content](https://github.com/tldraw/tldraw/blob/main/apps/examples/src/examples/external-content-sources/ExternalContentSourcesExample.tsx)

Wyświetl plik

@ -1,75 +0,0 @@
---
title: Collaboration
status: published
author: ds300
date: 3/22/2023
order: 8
---
We've designed the tldraw SDK to work with any collaboration backend. Depending on which backend you choose, you will need an interface that pipes changes coming from the editor to the backend and then merge changes from the backend back to the editor.
The best way to get started is by adapting one of our examples.
### Yjs sync example
We created a [tldraw-yjs example](https://github.com/tldraw/tldraw-yjs-example) to illustrate a way of using the [yjs](https://yjs.dev) library with the tldraw SDK. If you need a "drop in solution" for prototyping multiplayer experiences with tldraw, start here.
### Sockets example
We have a [sockets example](https://github.com/tldraw/tldraw-sockets-example) that uses [PartyKit](https://www.partykit.io/) as a backend. Unlike the yjs example, this example does not use any special data structures to handle conflicts. It should be a good starting point if you needed to write your own conflict-resolution logic.
### Our own sync engine
We developed our own sync engine for use on tldraw.com based on a push/pull/rebase-style algorithm. It powers our "shared projects", such as [this one](https://tldraw.com/r). The engine's source code can be found [here](https://github.com/tldraw/tldraw/tree/main/packages/tlsync). It was designed to be hosted on Cloudflare workers with [DurableObjects](https://developers.cloudflare.com/durable-objects/).
We don't suggest using this code directly. However, like our other examples, it may serve as a good reference for your own sync engine.
## Store data
For information about how to synchronize the store with other processes, i.e. how to get data out and put data in, including from remote sources, see the (Persistence)[/docs/persistence] page.
## User presence
Tldraw has support for displaying the 'presence' of other users. Presence information consists of:
- The user's pointer position
- The user's set of selected shapes
- The user's viewport bounds (the part of the canvas they are currently viewing)
- The user's name, id, and a color to represent them
This information will usually come from two sources:
- The tldraw editor state (e.g. pointer position, selected shapes)
- The data layer of whichever app tldraw has been embedded in (e.g. user name, user id)
Tldraw is agnostic about how this data is shared among users. However, in order for tldraw to use the presence data it needs to be put into the editor's store as `instance_presence` records.
We provide a helper for constructing a reactive signal for an `instance_presence` record locally, which can then be sent to other clients somehow. It is called [createPresenceStateDerivation](?).
```ts
import { createPresenceStateDerivation, react, atom } from 'tldraw'
// First you need to create a Signal containing the basic user details: id, name, and color
const user = atom<{ id: string; color: string; name: string }>('user', {
id: myUser.id,
color: myUser.color,
name: myUser.name,
})
// if you don't have your own user data backend, you can use our localStorage-only user preferences store
// import { getUserPreferences, computed } from 'tldraw'
// const user = computed('user', getUserPreferences)
// Then, with access to your store instance, you can create a presence signal
const userPresence = createPresenceStateDerivation(user)(store)
// Then you can listen for changes to the presence signal and send them to other clients
const unsub = react('update presence', () => {
const presence = userPresence.get()
broadcastPresence(presence)
})
```
The other clients would then call `store.put([presence])` to add the presence information to their store.
Any such `instance_presence` records tldraw finds in the store that have a different user `id` than the editor's configured user id will cause the presence information to be rendered on the canvas.

Wyświetl plik

@ -1,320 +0,0 @@
---
title: Editor
status: published
author: steveruizok
date: 3/22/2023
order: 1
keywords:
- ui
- app
- editor
- control
- select
---
The [Editor](?) class is the main way of controlling tldraw's editor. You can use it to manage the editor's internal state, make changes to the document, or respond to changes that have occurred.
By design, the [Editor](?)'s surface area is very large. Almost everything is available through it. Need to create some shapes? Use [Editor#createShapes](?). Need to delete them? Use [Editor#deleteShapes](?). Need a sorted array of every shape on the current page? Use [Editor#getCurrentPageShapesSorted](?).
This page gives a broad idea of how the [Editor](?) class is organized and some of the architectural concepts involved. The full reference is available in the [Editor](?) API.
## Store
The editor holds the raw state of the document in its [Editor#store](?) property. Data is kept here as a table of JSON serializable records.
For example, the store contains a [TLPage](?) record for each page in the current document, as well as an [TLInstancePageState](?) record for each page that stores information about the editor's state for that page, and a single [TLInstance](?) for each editor instance which stores the id of the user's current page.
The editor also exposes many _computed_ values which are derived from other records in the store. For example, [Editor#getSelectedShapeIds](?) is a method that returns the editor's current selected shape ids for its current page.
You can use these properties directly or you can use them in signals.
```tsx
import { track, useEditor } from 'tldraw'
export const SelectedShapeIdsCount = track(() => {
const editor = useEditor()
return <div>{editor.getSelectedShapeIds().length}</div>
})
```
### Changing the state
The [Editor](?) class has many methods for updating its state. For example, you can change the current page's selection using [Editor#setSelectedShapes](?). You can also use other convenience methods, such as [Editor#select](?), [Editor#selectAll](?), or [Editor#selectNone](?).
```ts
editor.selectNone()
editor.select(myShapeId, myOtherShapeId)
editor.getSelectedShapeIds() // [myShapeId, myOtherShapeId]
```
Each change to the state happens within a transaction. You can batch changes into a single transaction using the [Editor#batch](?) method. It's a good idea to batch wherever possible, as this reduces the overhead for persisting or distributing those changes.
### Listening for changes, and merging changes from other sources
For information about how to synchronize the store with other processes, i.e. how to get data out and put data in, see the (Persistence)[/docs/persistence] page.
### Undo and redo
The history stack in tldraw contains two types of data: "marks" and "commands". Commands have their own `undo` and `redo` methods that describe how the state should change when the command is undone or redone.
You can call [Editor#mark](?) to add a mark to the history stack with the given `id`.
```ts
editor.mark('my-id')
// do some stuff
editor.bailToMark('my-id')
```
When you call [Editor#undo](?), the editor will undo each command until it finds either a mark or the start of the stack. When you call [Editor#redo](?), the editor will redo each command until it finds either a mark or the end of the stack.
```ts
// A
editor.mark('duplicate everything')
editor.selectAll()
editor.duplicateShapes(editor.getSelectedShapeIds())
// B
editor.undo() // will return to A
editor.redo() // will return to B
```
You can call [Editor#bail](?) to undo and delete all commands in the stack until the first mark.
```ts
// A
editor.mark('duplicate everything')
editor.selectAll()
editor.duplicateShapes(editor.getSelectedShapeIds())
// B
editor.bail() // will return to A
editor.redo() // will do nothing
```
You can use [Editor#bailToMark](?) to undo and delete all commands and marks until you reach a mark with the given `id`.
```ts
// A
editor.mark('first')
editor.selectAll()
// B
editor.mark('second')
editor.duplicateShapes(editor.getSelectedShapeIds())
// C
editor.bailToMark('first') // will return to A
```
## Events
The [Editor](?) class receives events from its [Editor#dispatch](?) method. When the [Editor](?) receives an event, it is first handled internally to update [Editor#inputs](?) and other state before, and then sent into to the editor's state chart.
You shouldn't need to use the [Editor#dispatch](?) method directly, however you may write code in the state chart that responds to these events. See the [Tools page](/docs/tools) to learn how to do that, or read below for a more detailed information about the state chart itself.
### State Chart
The [Editor](?) class has a "state chart", or a tree of [StateNode](?) instances, that contain the logic for the editor's tools such as the select tool or the draw tool. User interactions such as moving the cursor will produce different changes to the state depending on which nodes are active.
Each node can be active or inactive. Each state node may also have zero or more children. When a state is active, and if the state has children, one (and only one) of its children must also be active. When a state node receives an event from its parent, it has the opportunity to handle the event before passing the event to its active child. The node can handle an event in any way: it can ignore the event, update records in the store, or run a _transition_ that changes which states nodes are active.
When a user interaction is sent to the editor via its [Editor#dispatch](?) method, this event is sent to the editor's root state node ([Editor#root](?)) and passed then down through the chart's active states until either it reaches a leaf node or until one of those nodes produces a transaction.
<Image
title="Events"
src="/images/api/events.png"
alt="A diagram showing an event being sent to the editor and handled in the state chart."
title="The editor passes an event into the state start where it is handled by each active state in order."
/>
### Path
You can get the editor's current "path" of active states via `editor.root.path`. In the above example, the value would be `"root.select.idle"`.
You can check whether a path is active via [Editor#isIn](?), or else check whether multiple paths are active via [Editor#isInAny](?).
```ts
editor.store.path // 'root.select.idle'
editor.isIn('root.select') // true
editor.isIn('root.select.idle') // true
editor.isIn('root.select.pointing_shape') // false
editor.isInAny('editor.select.idle', 'editor.select.pointing_shape') // true
```
Note that the paths you pass to [Editor#isIn](?) or [Editor#isInAny](?) can be the full path or a partial of the start of the path. For example, if the full path is `root.select.idle`, then [Editor#isIn](?) would return true for the paths `root`, `root.select`, or `root.select.idle`.
> If all you're interested in is the state below `root`, there is a convenience method, [Editor#getCurrentToolId](?), that can help with the editor's currently selected tool.
```tsx
import { track, useEditor } from 'tldraw'
export const BubbleToolUi = track(() => {
const editor = useEditor()
// Only show the UI if the bubble tool is active
if (!editor.getCurrentToolId() === 'bubble') return null
return <div>Creating bubble</div>
})
```
## Side effects
The [Editor#sideEffects](?) object lets you register callbacks for key parts of the lifecycle of records in the [Store](#Store).
You can register callbacks for before or after a record is created, changed, or deleted.
These callbacks are useful for applying constraints, maintaining relationships, or checking the integrity of different records in the document.
For example, we use side effects to create a new [TLCamera](?) record every time a new page is made.
The "before" callbacks allow you to modify the record itself, but shouldn't be used for modifying other records.
You can create a different record in the place of what was asked, prevent a change (or make a different one) to an existing record, or stop something from being deleted.
The "after" callbacks let you make changes to other records in response to something happening.
You could create, update, or delete any related record, but you should avoid changing the same record that triggered the change.
For example, if you wanted to know every time a new arrow is created, you could register a handler like this:
```ts
editor.sideEffects.registerAfterCreateHandler('shape', (newShape) => {
if (newShape.type === 'arrow') {
console.log('A new arrow shape was created', newShape)
}
})
```
Side effect handlers are also given a `source` argument - either `"user"` or `"remote"`.
This indicates whether the change originated from the current user, or from another remote user in the same multiplayer room.
You could use this to e.g. prevent the current user from deleting shapes, but allow deletions from others in the same room.
## Inputs
The [Editor#inputs](?) object holds information about the user's current input state, including their cursor position (in page space _and_ screen space), which keys are pressed, what their multi-click state is, and whether they are dragging, pointing, pinching, and so on.
Note that the modifier keys include a short delay after being released in order to prevent certain errors when modeling interactions. For example, when a user releases the "Shift" key, `editor.inputs.shiftKey` will remain `true` for another 100 milliseconds or so.
This property is stored as regular data. It is not reactive.
## Editor instance state
The [Editor#getInstanceState](?) method returns settings that relate to each individual instance of the editor. In the case that the user has the same editor open in multiple tabs, or if there are multiple editors on the same page, then each editor will have its own instance state. See the [TLInstance](?) docs to learn more about the record itself.
## User preferences
The editor's user preferences are shared between all instances. See the [TLUserPreferences](?) docs for more about the user preferences.
## Common things to do with the editor
### Create a shape id
To create an id for a shape (a [TLShapeId](?)), use the libary's [createShapeId](?) helper.
```ts
import { createShapeId } from 'tldraw'
createShapeId() // `shape:some-random-uuid`
createShapeId('kyle') // `shape:kyle`
```
The `id` property of any record in tldraw is "branded" with the type of that record. For shapes, that means that all shape ids are formatted as `shape:{id}`. The TypeScript type of a record's `id` also includes a reference to the type of the record that it belongs to. TypeScript will complain if you use a regular `shape:some-id` string, but the [createShapeId](?) helper will provide the type.
### Create shapes
To create shapes, use the [Editor#createShape](?) or [Editor#createShapes](?) methods.
```ts
editor.createShapes([
{
id,
type: 'geo',
x: 0,
y: 0,
props: {
geo: 'rectangle',
w: 100,
h: 100,
dash: 'draw',
color: 'blue',
size: 'm',
},
},
])
```
A shape must be a partial of the full shape (a [TLShapePartial](?)). All props are optional except for the `type` of the shape. The shape's corresponding [ShapeUtil](?) will provide the default props for any props not provided. The `id` will be created if not provided.
### Update shapes
To update shapes, use the [Editor#updateShape](?) or [Editor#updateShapes](?) methods.
```ts
editor.updateShapes([
{
id: shape.id, // required
type: shape.type, // required
x: 100,
y: 100,
props: {
w: 200,
},
},
])
```
The update must be a partial of the full shape (a [TLShapePartial](?)). All props are optional except for the `type` of the shape and its `id`.
### Delete shapes
To delete shapes, use the [Editor#deleteShape](?) or [Editor#deleteShapes](?) methods.
```ts
editor.deleteShapes([shape.id])
editor.deleteShapes([shape])
```
You can delete a shape using the shape's `id` or the shape record itself.
### Get a shape
You can get a shape with the [Editor#getShape](?) method.
```ts
editor.getShape(myShapeId)
editor.getShape(myShape)
```
You can get a shape using the shape's `id` or the shape record itself.
### Turn on read only mode
You can use the [Editor#updateInstanceState](?) method to turn on read only mode.
```ts
editor.updateInstanceState({ isReadonly: true })
```
### Move the camera
You can set the camera to a specific x, y, and zoom with the [Editor#setCamera](?) method.
```ts
editor.setCamera(0, 0, 1)
```
### Freeze the camera
You can prevent the user from changing the camera using the [Editor#updateInstanceState](?) method.
```ts
editor.updateInstanceState({ canMoveCamera: false })
```
### Turn on dark mode
You can turn on or off dark mode via the [setUserPreferences](?) method. Note that this effects all editor instances that share the same user—even instances in other tabs.
```ts
setUserPreferences({ isDarkMode: true })
```
---
See the [tldraw repository](https://github.com/tldraw/tldraw/tree/main/apps/examples) for an example of how to use tldraw's Editor API to control the editor.

Wyświetl plik

@ -1,411 +0,0 @@
---
title: Persistence
status: published
author: steveruizok
date: 3/22/2023
order: 6
keywords:
- data
- sync
- persistence
- database
- indexeddb
- localstorage
---
Persistence in tldraw means storing information about the editor's state to a database and then restoring it later. There are a few options that developers have for getting data into tldraw and out again.
## The `"persistenceKey"` prop
Both the `<Tldraw>` or `<TldrawEditor>` components support local persistence and cross-tab synchronization via the `persistenceKey` prop. Passing a value to this prop will persist the contents of the editor locally to the browser's IndexedDb.
```tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function () {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw persistenceKey="my-persistence-key" />
</div>
)
}
```
Using a `persistenceKey` will synchronize data automatically with any other tldraw component with the same `persistenceKey` prop, even if that component is in a different browser tab.
```tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function () {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<div style={{ width: '50%', height: '100%' }}>
<Tldraw persistenceKey="my-persistence-key" />
</div>
<div style={{ width: '50%', height: '100%' }}>
<Tldraw persistenceKey="my-persistence-key" />
</div>
</div>
)
}
```
In the example above, both editors would synchronize their document locally. They would still have two independent instance states (e.g. selections) but the document would be kept in sync and persisted under the same key.
## Document Snapshots
You can get a JSON snapshot of the editor's content using the [Editor#store](?)'s [Store#getSnapshot](?) method.
```tsx
function SaveButton() {
const editor = useEditor()
return (
<button
onClick={() => {
const snapshot = editor.store.getSnapshot()
const stringified = JSON.stringify(snapshot)
localStorage.setItem('my-editor-snapshot', stringified)
}}
>
Save
</button>
)
}
```
You can load the snapshot into a new editor with [Store#loadSnapshot](?).
```tsx
function LoadButton() {
const editor = useEditor()
return (
<button
onClick={() => {
const stringified = localStorage.getItem('my-editor-snapshot')
const snapshot = JSON.parse(stringified)
editor.store.loadSnapshot(snapshot)
}}
>
Load
</button>
)
}
```
A [snapshot](/reference/store/StoreSnapshot) includes both the store's [serialized records](/reference/store/SerializedStore) and its [serialized schema](/reference/store/SerializedSchema), which is used for migrations.
> By default, the `getSnapshot` method returns only the editor's document data. If you want to get records from a different scope, you can pass in `session`, `document`, `presence`, or else `all` for all scopes.
Note that loading a snapshot does not reset the editor's in memory state or UI state. For example, loading a snapshot during a resizing operation may lead to a crash. This is because the resizing state maintains its own cache of information about which shapes it is resizing, and its possible that those shapes may no longer exist!
## The `"store"` prop
While it's possible to load the editor and then load data into its store, we've found it best to create the store, set its data, and then pass the store into the editor.
The `store` property of the `<Tldraw>` / `<TldrawEditor>` components accepts a store that you've defined outside of the component.
```tsx
export default function () {
const [store] = useState(() => {
// Create the store
const newStore = createTLStore({
shapeUtils: defaultShapeUtils,
})
// Get the snapshot
const stringified = localStorage.getItem('my-editor-snapshot')
const snapshot = JSON.parse(stringified)
// Load the snapshot
newStore.loadSnapshot(snapshot)
return newStore
})
return <Tldraw persistenceKey="my-persistence-key" store={store} />
}
```
Sometimes you won't be able to access the store's data synchronously. To handle this case, the `store` property also accepts a [TLStoreWithStatus](?).
```tsx
export default function () {
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
status: 'loading',
})
useEffect(() => {
let cancelled = false
async function loadRemoteSnapshot() {
// Get the snapshot
const snapshot = await getRemoteSnapshot()
if (cancelled) return
// Create the store
const newStore = createTLStore({
shapeUtils: defaultShapeUtils,
})
// Load the snapshot
newStore.loadSnapshot(snapshot)
// Update the store with status
setStoreWithStatus({
store: newStore,
status: 'ready',
})
}
loadRemoteSnapshot()
return () => {
cancelled = true
}
})
return <Tldraw persistenceKey="my-persistence-key" store={storeWithStatus} />
}
```
For a good example of this pattern, see the [yjs-example](https://github.com/tldraw/tldraw-yjs-example).
## Listening for changes
You can listen for incremental updates to the document state by calling `editor.store.listen`, e.g.
```ts
const unlisten = editor.store.listen(
(update) => {
console.log('update', update)
},
{ scope: 'document', source: 'user' }
)
```
These updates contain information about which records were added, removed, and updated. See [HistoryEntry](?)
The `scope` filter can be used to listen for changes to a specific record scope, e.g. `document`, `session`, `presence`, or `all`.
The `source` filter can be used to listen for changes from a specific source, e.g. `user`, `remote`, or `all`. (See [Store#mergeRemoteChanges](?) for more information on remote changes.)
Note that these incremental updates do not include the schema version. You should make sure that you keep a record of the latest schema version for your snapshots.
You can get the schema version by calling `editor.store.schema.serialize()` and the returned value can replace the `schema` property in the snapshot next time you need to load a snapshot. The schema does not change at runtime so you only need to do this once per session.
## Handling remote changes
If you need to synchronize changes from a remote source, e.g. a multiplayer backend, you can use the `editor.store.mergeRemoteChanges` method. This will 'tag' the changes with the `source` property as `'remote'` so you can filter them out when listening for changes.
```ts
myRemoteSource.on('change', (changes) => {
editor.store.mergeRemoteChanges(() => {
changes.forEach((change) => {
// Apply the changes to the store
editor.store.put(/* ... */)
})
})
})
```
## Migrations
Tldraw uses migrations to bring data from old snapshots up to date. These run automatically when calling `editor.store.loadSnapshot`.
### Running migrations manually
If you need to run migrations on a snapshot without loading it into the store, you can call [StoreSchema#migrateStoreSnapshot](?) directly.
```ts
import { createTLSchema } from 'tldraw'
const snapshot = await getSnapshotFromSomewhere()
const migrationResult = createTLSchema().migrateStoreSnapshot(snapshot)
if (migrationResult.type === 'success') {
console.log('Migrated snapshot', migrationResult.value)
} else {
console.error('Migration failed', migrationResult.reason)
}
```
### Custom migrations
Tldraw supports a couple of ways of adding custom data types to the tldraw store:
- [Custom shape types](/docs/shapes#Custom-shapes-1)
- [`meta` properties](/docs/shapes#Meta-information) on all of our built-in record types.
You might wish to migrate your custom data types over time as you make changes to them.
To enable this, tldraw provides two ways to add custom migrations:
1. **Shape props migrations**, specifically for migrating the shape.props objects on your custom shape types.
2. **The `migrations` config option**, which is more general purpose but much less commonly needed. This will allow you to migrate any data in the store.
#### Shape props migrations
If you have a custom shape type, you can define a `migrations` property on the shape util class. Use the `createShapePropsMigrationSequence` helper to define this property.
```ts
import { createShapePropsMigrationSequence, createShapePropsMigrationIds, ShapeUtil } from 'tldraw'
// Migrations must start a 1 and be sequential integers.
const Versions = createShapePropMigrationIds('custom-shape', {
AddColor: 1,
})
class MyCustomShapeUtil extends ShapeUtil {
static type = 'custom-shape'
static migrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions.AddColor,
up(props) {
// set the default color
props.color = 'black'
},
},
],
})
// ...
}
```
#### The `migrations` config option
First create a set of migration ids.
```ts
import { createMigrationIds } from 'tldraw'
// The first argument is a unique namespace for your migration sequence.
// We recommend using a reverse domain name, e.g. we use 'com.tldraw.foo.bar'
const SEQUENCE_ID = 'com.example.my-app'
const Versions = createMigrationIds(SEQUENCE_ID, {
// Migrations must start at 1 and be sequential integers.
AddColor: 1,
})
```
Then create a migration sequence.
```ts
import { createMigrationSequence, isShape } from 'tldraw'
const myMigrations = createMigrationSequence({
sequenceId: SEQUENCE_ID,
sequence: [
{
id: Versions.AddColor,
// Scope can be one of
// - 'store' to have the up function called on the whole snapshot at once
// - 'record' to have the up function called on each record individually
scope: 'record',
// if scope is 'record', you can filter which records the migration runs on
filter: (record) => isShape(record) && record.type === 'custom-shape',
up(record) {
record.props.color = 'black'
},
},
],
})
```
And finally pass your migrations in to tldraw via the `migrations` config option. There are a few places where you might need to do this, depending on how specialized your usage of Tldraw is:
```tsx
// When rendering the Tldraw component
<Tldraw
...
migrations={[myMigrations]}
/>
// or when creating the store
store = createTLStore({
...
migrations: [myMigrations],
})
// or when creating the schema
schema = createTLSchema({
...
migrations: [myMigrations],
})
```
### Updating legacy shape migrations (defineMigrations)
You can convert your legacy migrations to the new migrations format by the following process:
1. Wrap your version numbers in `createShapePropsMigrationIds`
```diff
- const Versions = {
+ const Versions = createShapePropMigrationIds('custom-shape', {
AddColor: 1
- }
+ })
```
2. Replace your `defineMigrations` call with `createShapePropsMigrationSequence`
```ts
const migrations = defineMigrations({
currentVersion: Versions.AddColor,
migrators: {
[Versions.AddColor]: {
up: (shape: any) => ({ ...shape, props: { ...shape.props, color: 'black' } }),
down: ({ props: { color, ...props }, ...shape }: any) => ({ ...shape, props }),
},
},
})
```
Becomes
```ts
const migrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions.AddColor,
// [!!!] You no longer have access to the top-level shape object.
// Only the shape.props object is passed in to the migrator function.
up(props) {
// [!!!] You no longer need to return a new copy of the shape object.
// Instead, you can modify the props object in place.
props.color = 'black'
},
// [!!!] You no longer need to specify a down migration.
},
],
})
```
## Examples
### Local persistence
Tldraw ships with a local-only sync engine based on `IndexedDb` and `BroadcastChannel` called [`TLLocalSyncClient`](https://github.com/tldraw/tldraw/blob/main/packages/editor/src/lib/utils/sync/TLLocalSyncClient.ts).
### Tldraw.com sync engine
[tldraw.com/r](https://tldraw.com/r) currently uses a simple custom sync engine based on a push/pull/rebase-style algorithm.
It can be found [here](https://github.com/tldraw/tldraw/tree/main/packages/tlsync).
It was optimized for Cloudflare workers with [DurableObjects](https://developers.cloudflare.com/durable-objects/)
We don't suggest using our code directly yet, but it may serve as a good reference for your own sync engine.
### Yjs sync example
We created a [tldraw-yjs example](https://github.com/tldraw/tldraw-yjs-example) to illustrate a way of using yjs with the tldraw SDK.
### Shape props migrations example
Our [custom-config example](/examples/shapes/tools/custom-config) shows how to add custom shape props migrations to the tldraw store.
### Meta properties migrations example
Our [custom-config example](/examples/shapes/tools/custom-config) shows how to add custom migrations to the tldraw store.

Wyświetl plik

@ -1,244 +0,0 @@
---
title: Shapes
status: published
author: steveruizok
date: 3/22/2023
order: 2
keywords:
- custom
- shapes
- shapeutils
- utils
---
In tldraw, a shape is something that can exist on the page, like an arrow, an image, or some text.
This article is about shapes: what they are, how they work, and how to create your own shapes. If you'd prefer to see an example, see the tldraw repository's [examples app](https://github.com/tldraw/tldraw/tree/main/apps/examples) for examples of how to create custom shapes in tldraw.
## Types of shape
We make a distinction between three types of shapes: "core", "default", and "custom".
### Core shapes
The editor's core shapes are shapes that are built in and always present. At the moment the only core shape is the [group shape](/reference/tlschema/TLGroupShape).
### Default shapes
The default shapes are all of the shapes that are included by default in the [Tldraw](?) component, such as the [TLArrowShape](?) or [TLDrawShape](?). They are exported from the `tldraw` library as [defaultShapeUtils](?).
### Custom shapes
Custom shapes are shapes that were created by you or someone you love. Find more information about custom shapes [below](#Custom-shapes-1).
## The shape object
Shapes are just records (JSON objects) that sit in the [store](/docs/editor#Store). For example, here's a shape record for a rectangle geo shape:
```ts
{
"parentId": "page:somePage",
"id": "shape:someId",
"typeName": "shape"
"type": "geo",
"x": 106,
"y": 294,
"rotation": 0,
"index": "a28",
"opacity": 1,
"isLocked": false,
"props": {
"w": 200,
"h": 200,
"geo": "rectangle",
"color": "black",
"labelColor": "black",
"fill": "none",
"dash": "draw",
"size": "m",
"font": "draw",
"text": "diagram",
"align": "middle",
"verticalAlign": "middle",
"growY": 0,
"url": ""
},
"meta": {},
}
```
### Base properties
Every shape contains some base information. These include the shape's type, position, rotation, opacity, and more. You can find the full list of base properties [here](/reference/tlschema/TLBaseShape).
### Props
Every shape also contains some shape-specific information, called `props`. Each type of shape can have different props. For example, the `props` of a text shape are much different than the props of an arrow shape.
### Meta
Meta information is information that is not used by tldraw but is instead used by your application. For example, you might want to store the name of the user who created a shape, or the date that the shape was created. You can find more information about meta information [below](#Meta-information).
## The `ShapeUtil` class
While tldraw's shapes themselves are simple JSON objects, we use [ShapeUtil](?) classes to answer questions about shapes. For example, when the editor needs to render a text shape, it will find the [TextShapeUtil](?) and call its [ShapeUtil#component](?) method, passing in the text shape object as an argument.
---
## Custom shapes
You can create your own custom shapes. In the examples below, we will create a custom "card" shape. It'll be a simple rectangle with some text inside.
> For an example of how to create custom shapes, see our [custom shapes example](/examples/shapes/tools/custom-shape).
### Shape type
In tldraw's data model, each shape is represented by a JSON object. Let's first create a type that describes what this object will look like.
```ts
import { TLBaseShape } from 'tldraw'
type CardShape = TLBaseShape<'card', { w: number; h: number }>
```
With the [TLBaseShape](?) helper, we define the shape's `type` property (`card`) and the shape's `props` property (`{ w: number, h: number }`). The type can be any string but the props must be a regular [JSON-serializable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description) JavaScript object.
The [TLBaseShape](?) helper adds the other base properties of a shape, such as `x`, `y`, `rotation`, and `opacity`.
### Shape Util
While tldraw's shapes themselves are simple JSON objects, we use [ShapeUtil](?) classes to answer questions about shapes.
Let's create a [ShapeUtil](?) class for the shape.
```tsx
import { HTMLContainer, ShapeUtil } from 'tldraw'
class CardShapeUtil extends ShapeUtil<CardShape> {
static override type = 'card' as const
getDefaultProps(): CardShape['props'] {
return {
w: 100,
h: 100,
}
}
getGeometry(shape: ICardShape) {
return new Rectangle2d({
width: shape.props.w,
height: shape.props.h,
isFilled: true,
})
}
component(shape: CardShape) {
return <HTMLContainer>Hello</HTMLContainer>
}
indicator(shape: CardShape) {
return <rect width={shape.props.w} height={shape.props.h} />
}
}
```
This is a minimal [ShapeUtil](?). We've given it a static property `type` that matches the type of our shape, we've provided implementations for the abstract methods [ShapeUtil#getDefaultProps](?), [ShapeUtil#getBounds](?), [ShapeUtil#component](?), and [ShapeUtil#indicator](?).
We still have work to do on the `CardShapeUtil` class, but we'll come back to it later. For now, let's put the shape onto the canvas by passing it to the [Tldraw](?) component.
### The `shapeUtils` prop
We pass an array of our shape utils into the [Tldraw](?) component's `shapeUtils` prop.
```tsx
const MyCustomShapes = [MyCardShape]
export default function () {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw shapeUtils={MyCustomShapes} />
</div>
)
}
```
We can create one of our custom card shapes using the [Editor](?) API. We'll do this by setting the `onMount` prop of the [Tldraw](?) component.
```tsx
export default function () {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw
shapeUtils={MyCustomShapes}
onMount={(editor) => {
editor.createShapes([{ type: 'card' }])
}}
/>
</div>
)
}
```
Once the page refreshes, we should now have our custom shape on the canvas.
### Meta information
Shapes also have a `meta` property (see [TLBaseShape#meta](?)) that you can fill with your own data. This should feel like a bit of a hack, however it's intended to be an escape hatch for applications where you want to use tldraw's existing shapes but also want to attach a bit of extra data to the shape.
Note that tldraw's regular shape definitions have an unknown object for the shape's `meta` property. To type your shape's meta, use a union like this:
```ts
type MyShapeWithMeta = TLGeoShape & { meta: { createdBy: string } }
const shape = editor.getShape<MyShapeWithMeta>(myGeoShape.id)
```
You can update a shape's `meta` property in the same way you would update its props, using [Editor#updateShapes](?).
```ts
editor.updateShapes<MyShapeWithMeta>([
{
id: myGeoShape.id,
type: 'geo',
meta: {
createdBy: 'Steve',
},
},
])
```
Like [TLBaseShape#props](?), the data in a [TLBaseShape#meta](?) object must be JSON serializable.
In addition to setting meta properties this way, you can also set the default meta data for shapes using the Editor's [Editor#getInitialMetaForShape](?) method.
```tsx
editor.getInitialMetaForShape = (shape: TLShape) => {
if (shape.type === 'text') {
return { createdBy: currentUser.id, lastModified: Date.now() }
} else {
return { createdBy: currentUser.id }
}
}
```
Whenever new shapes are created using the [Editor#createShapes](?) method, the shape's meta property will be set using the [Editor#getInitialMetaForShape](?) method. By default this method returns an empty object.
### Using starter shapes
You can use "starter" shape utils like [BaseBoxShapeUtil](?) to get regular rectangular shape behavior.
### Flags
You can use flags like [ShapeUtil#hideRotateHandle](?) to hide different parts of the UI when the shape is selected, or else to control different behaviors of the shape.
### Interaction
You can turn on `pointer-events` to allow users to interact inside of the shape.
### Editing
You can make shapes "editable" to help decide when they're interactive or not.
### Migrations
You can add migrations for your shape props by adding a `migrations` property to your shape's util class. See [the persistence docs](/docs/persistence#Shape-props-migrations) for more information.

Some files were not shown because too many files have changed in this diff Show More