Full Code of excalidraw/excalidraw-blog for AI

master 083a4277a924 cached
43 files
175.7 KB
45.3k tokens
15 symbols
1 requests
Download .txt
Repository: excalidraw/excalidraw-blog
Branch: master
Commit: 083a4277a924
Files: 43
Total size: 175.7 KB

Directory structure:
gitextract_g20e0di6/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .prettierignore
├── .vscode/
│   └── settings.json
├── .yarnrc
├── LICENSE
├── README.md
├── content/
│   └── blog/
│       ├── browser-fs-access/
│       │   └── index.md
│       ├── building-excalidraw-p2p-collaboration-feature/
│       │   └── index.md
│       ├── deprecating-excalidraw-electron/
│       │   └── index.md
│       ├── enabling-translations/
│       │   └── index.md
│       ├── end-to-end-encryption/
│       │   └── index.md
│       ├── excalidraw-and-fugu/
│       │   └── index.md
│       ├── introducing-excalidraw-plus/
│       │   └── index.md
│       ├── one-year-of-excalidraw/
│       │   └── index.md
│       ├── open-colors/
│       │   └── index.md
│       ├── redesigning-editor-api/
│       │   └── index.md
│       ├── reflections-on-excalidraw/
│       │   └── index.md
│       ├── rethinking-virtual-whiteboard/
│       │   └── index.md
│       ├── tell-your-story-with-charts/
│       │   └── index.md
│       ├── webex-meetings-integration/
│       │   └── index.md
│       ├── year-three/
│       │   └── index.md
│       └── year-two/
│           └── index.md
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── package.json
├── src/
│   ├── blog.css
│   ├── code.css
│   ├── components/
│   │   ├── Toggle.css
│   │   ├── Toggle.js
│   │   ├── excalidraw.js
│   │   ├── layout.js
│   │   ├── layoutStyles.css
│   │   └── seo.js
│   ├── pages/
│   │   ├── 404.js
│   │   └── index.js
│   ├── styles.css
│   ├── templates/
│   │   └── blog-post.js
│   └── utils/
│       └── typography.js
├── static/
│   └── robots.txt
└── vercel.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
      day: sunday
      time: "01:00"


================================================
FILE: .github/workflows/test.yml
================================================
name: Test formatting

on:
  push:
    branches:
      - master
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Install and test
        run: |
          yarn
          yarn test


================================================
FILE: .gitignore
================================================
.cache
.DS_Store
.yarn-integrity
*.log
node_modules
npm-debug.log*
package-lock.json
public
yarn-debug.log*
yarn-error.log
yarn-error.log*


================================================
FILE: .prettierignore
================================================
.cache
public


================================================
FILE: .vscode/settings.json
================================================
{
  "cSpell.enabled": true
}


================================================
FILE: .yarnrc
================================================
--add.exact true


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2020 Excalidraw

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Excalidraw Blog

> For news and updates visit: https://blog.excalidraw.com.

## Develop

We are using [Gatsby](https://www.gatsbyjs.com/) and in order to run it locally, execute the following from the root:

```
yarn
yarn start
```

Visit [`localhost:8000`](http://localhost:8000) to test it.

## Writing a Blog Post

- Create a new folder inside the `content/blog/`
  - The folder name should follow `kebab-case`
  - Use the slug of the title of the post to name it
- Create `index.md` inside that folder
- Create `og.png` inside that folder for the open graph image
- Add a frontmatter
  - `title`: Use Title Case for Titles
  - `date`: Date in ISO format. Example: `2020-03-12`
  - `note`: Optional. Displayed next to the date when reading a post.
  - `image`: Filename of the open graph image. Example: `og.png`
- The `note` field is usually used to link to the original post when reposting ([example](https://blog.excalidraw.com/reflections-on-excalidraw/))
- Add somewhere the `<!-- end -->` to declare your `excerpt` (it's used on the front page)


================================================
FILE: content/blog/browser-fs-access/index.md
================================================
---
title: Reading and writing files and directories with the browser-fs-access library
date: 2020-12-09
note: 'This post appeared first on <a href="https://web.dev/browser-fs-access/">web.dev</a>.'
author: tomayac
link: https://twitter.com/tomayac
image: chrome-save-as.png
---

Browsers have been able to deal with files and directories for a long time. The [File API](https://w3c.github.io/FileAPI/) provides features for representing file objects in web applications, as well as programmatically selecting them and accessing their data. The moment you look closer, though, all that glitters is not gold.

<!-- end -->

## The traditional way of dealing with files

<blockquote>
  If you know how it used to work the old way, you can
  <a href="#the-file-system-access-api">jump down straight to the new way</a>.
</blockquote>

### Opening files

As a developer, you can open and read files via the [`<input type="file">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file) element. In its simplest form, opening a file can look something like the code sample below. The `input` object gives you a [`FileList`](https://developer.mozilla.org/en-US/docs/Web/API/FileList), which in the case below consists of just one [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). A `File` is a specific kind of [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob), and can be used in any context that a Blob can.

```js
const openFile = async () => {
  return new Promise((resolve) => {
    const input = document.createElement("input");
    input.type = "file";
    input.addEventListener("change", () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};
```

### Opening directories

For opening folders (or directories), you can set the [`<input webkitdirectory>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-webkitdirectory) attribute. Apart from that, everything else works the same as above. Despite its vendor-prefixed name, `webkitdirectory` is not only usable in Chromium and WebKit browsers, but also in the legacy EdgeHTML-based Edge as well as in Firefox.

### Saving (rather: downloading) files

For saving a file, traditionally, you are limited to _downloading_ a file, which works thanks to the [`<a download>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-download:~:text=download) attribute. Given a Blob, you can set the anchor's `href` attribute to a `blob:` URL that you can get from the [`URL.createObjectURL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) method.

<blockquote>
  To prevent memory leaks, always revoke the URL after the download.
</blockquote>

```js
const saveFile = async (blob) => {
  const a = document.createElement("a");
  a.download = "my-file.txt";
  a.href = URL.createObjectURL(blob);
  a.addEventListener("click", (e) => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};
```

### The problem

A massive downside of the _download_ approach is that there is no way to make a classic open→edit→save flow happen, that is, there is no way to _overwrite_ the original file. Instead, you end up with a new _copy_ of the original file in the operating system's default Downloads folder whenever you "save".

<h2 id="the-file-system-access-api">The File System Access API</h2>

The File System Access API makes both operations, opening and saving, a lot simpler. It also enables _true saving_, that is, you can not only choose where to save a file, but also overwrite an existing file.

<blockquote>
  For a more thorough introduction to the File System Access API, see the article
  <a href="https://web.dev/file-system-access/">The File System Access API: simplifying access to local files</a>.
</blockquote>

### Opening files

With the [File System Access API](https://wicg.github.io/file-system-access/), opening a file is a matter of one call to the `window.showOpenFilePicker()` method. This call returns a file handle, from which you can get the actual `File` via the `getFile()` method.

```js
const openFile = async () => {
  try {
    // Always returns an array.
    const [handle] = await window.showOpenFilePicker();
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};
```

### Opening directories

Open a directory by calling `window.showDirectoryPicker()` that makes directories selectable in the file dialog box.

### Saving files

Saving files is similarly straightforward. From a file handle, you create a writable stream via `createWritable()`, then you write the Blob data by calling the stream's `write()` method, and finally you close the stream by calling its `close()` method.

```js
const saveFile = async (blob) => {
  try {
    const handle = await window.showSaveFilePicker({
      types: [
        {
          accept: {
            // Omitted
          },
        },
      ],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
    return handle;
  } catch (err) {
    console.error(err.name, err.message);
  }
};
```

## Introducing browser-fs-access

As perfectly fine as the File System Access API is, it's [not yet widely available](https://caniuse.com/native-filesystem-api).

<figure>
  <img src="caniuse.png"
       alt="Browser support table for the File System Access API. All browsers are marked as 'no support' or 'behind a flag'.">
  <figcaption>
    Browser support table for the File System Access API.
    (<a href="https://caniuse.com/native-filesystem-api">Source</a>)
  </figcaption>
</figure>

This is why I see the File System Access API as a [progressive enhancement](https://web.dev/progressively-enhance-your-pwa). As such, I want to use it when the browser supports it, and use the traditional approach if not; all while never punishing the user with unnecessary downloads of unsupported JavaScript code. The [browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access) library is my answer to this challenge.

### Design philosophy

Since the File System Access API is still likely to change in the future, the browser-fs-access API is not modeled after it. That is, the library is not a [polyfill](https://developer.mozilla.org/en-US/docs/Glossary/Polyfill), but rather a [ponyfill](https://github.com/sindresorhus/ponyfill). You can (statically or dynamically) exclusively import whatever functionality you need to keep your app as small as possible. The available methods are the aptly named [`fileOpen()`](https://github.com/GoogleChromeLabs/browser-fs-access#opening-files), [`directoryOpen()`](https://github.com/GoogleChromeLabs/browser-fs-access#opening-directories), and [`fileSave()`](https://github.com/GoogleChromeLabs/browser-fs-access#saving-files). Internally, the library feature-detects if the File System Access API is supported, and then imports the corresponding code path.

### Using the browser-fs-access library

The three methods are intuitive to use. You can specify your app's accepted `mimeTypes` or file `extensions`, and set a `multiple` flag to allow or disallow selection of multiple files or directories. For full details, see the [browser-fs-access API documentation](https://github.com/GoogleChromeLabs/browser-fs-access#api-documentation). The code sample below shows how you can open and save image files.

```js
// The imported methods will use the File
// System Access API or a fallback implementation.
import {
  fileOpen,
  directoryOpen,
  fileSave,
} from "https://unpkg.com/browser-fs-access";

(async () => {
  // Open an image file.
  const blob = await fileOpen({
    mimeTypes: ["image/*"],
  });

  // Open multiple image files.
  const blobs = await fileOpen({
    mimeTypes: ["image/*"],
    multiple: true,
  });

  // Open all files in a directory,
  // recursively including subdirectories.
  const blobsInDirectory = await directoryOpen({
    recursive: true,
  });

  // Save a file.
  await fileSave(blob, {
    fileName: "Untitled.png",
  });
})();
```

### Demo

You can see the above code in action in a [demo](https://browser-fs-access.glitch.me/) on Glitch. Its [source code](https://glitch.com/edit/#!/browser-fs-access) is likewise available there. Since for security reasons cross origin sub frames are not allowed to show a file picker, the demo cannot be embedded in this article.

## The browser-fs-access library in the wild

In my free time, I contribute a tiny bit to an [installable PWA](https://web.dev/progressive-web-apps/#installable) called [Excalidraw](https://excalidraw.com/), a whiteboard tool that lets you easily sketch diagrams with a hand-drawn feel. It is fully responsive and works well on a range of devices from small mobile phones to computers with large screens. This means it needs to deal with files on all the various platforms whether or not they support the File System Access API. This makes it a great candidate for the browser-fs-access library.

I can, for example, start a drawing on my iPhone, save it (technically: download it, since Safari does not support the File System Access API) to my iPhone Downloads folder, open the file on my desktop (after transferring it from my phone), modify the file, and overwrite it with my changes, or even save it as a new file.

<figure>
  <img src="iphone-original.png" width="300" alt="An Excalidraw drawing on an iPhone.">
  <figcaption>
    Starting an Excalidraw drawing on an iPhone where the File System Access API is not supported,
    but where a file can be saved (downloaded) to the Downloads folder.
  </figcaption>
</figure>

<figure>
  <img src="chrome-modify.png" alt="The modified Excalidraw drawing on Chrome on the desktop.">
  <figcaption>
    Opening and modifying the Excalidraw drawing on the desktop where the File System Access API is supported
    and thus the file can be accessed via the API.
  </figcaption>
</figure>

<figure>
  <img src="chrome-oversave.png" alt="Overwriting the original file with the modifications.">
  <figcaption>
    Overwriting the original file with the modifications to the original Excalidraw drawing file.
    The browser shows a dialog asking me whether this is fine.
  </figcaption>
</figure>

<figure>
  <img src="chrome-save-as.png" alt="Saving the modifications to a new Excalidraw drawing file.">
  <figcaption>
    Saving the modifications to a new Excalidraw file. The original file remains untouched.
  </figcaption>
</figure>

### Real life code sample

Below, you can see an actual example of browser-fs-access as it is used in Excalidraw. This excerpt is taken from [`/src/data/json.ts`](https://github.com/excalidraw/excalidraw/blob/cd87bd6901b47430a692a06a8928b0f732d77097/src/data/json.ts#L24-L52). Of special interest is how the `saveAsJSON()` method passes either a file handle or `null` to browser-fs-access' `fileSave()` method, which causes it to overwrite when a handle is given, or to save to a new file if not.

```js
export const saveAsJSON = async (
  elements: readonly ExcalidrawElement[],
  appState: AppState,
  fileHandle: any,
) => {
  const serialized = serializeAsJSON(elements, appState);
  const blob = new Blob([serialized], {
    type: "application/json",
  });
  const name = `${appState.name}.excalidraw`;
  (window as any).handle = await fileSave(
    blob,
    {
      fileName: name,
      description: "Excalidraw file",
      extensions: ["excalidraw"],
    },
    fileHandle || null,
  );
};

export const loadFromJSON = async () => {
  const blob = await fileOpen({
    description: "Excalidraw files",
    extensions: ["json", "excalidraw"],
    mimeTypes: ["application/json"],
  });
  return loadFromBlob(blob);
};
```

### UI considerations

Whether in Excalidraw or your app, the UI should adapt to the browser's support situation. If the File System Access API is supported (`if ('showOpenFilePicker' in window) {}`) you can show a **Save As** button in addition to a **Save** button. The screenshots below show the difference between Excalidraw's responsive main app toolbar on iPhone and on Chrome desktop. Note how on iPhone the **Save As** button is missing.

<figure>
  <img src="save.png" alt="Excalidraw app toolbar on iPhone with just a 'Save' button." width="300">
  <figcaption>
    Excalidraw app toolbar on iPhone with just a <strong>Save</strong> button.
  </figcaption>
</figure>

<figure>
  <img src="save-save-as.png" alt="Excalidraw app toolbar on Chrome desktop with a 'Save' and a 'Save As' button." width="300">
  <figcaption>
    Excalidraw app toolbar on Chrome  with a <strong>Save</strong> and a focused <strong>Save As</strong> button.
  </figcaption>
</figure>

## Conclusions

Working with native files technically works on all modern browsers. On browsers that support the File System Access API, you can make the experience better by allowing for true saving and overwriting (not just downloading) of files and by letting your users create new files wherever they want, all while remaining functional on browsers that do not support the File System Access API. The [browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access) makes your life easier by dealing with the subtleties of progressive enhancement and making your code as simple as possible.

## Acknowledgements

This article was reviewed by [Joe Medley](https://github.com/jpmedley) and [Kayce Basques](https://github.com/kaycebasques). Thanks to the [contributors to Excalidraw](https://github.com/excalidraw/excalidraw/graphs/contributors) for their work on the project and for reviewing my Pull Requests.


================================================
FILE: content/blog/building-excalidraw-p2p-collaboration-feature/index.md
================================================
---
title: Building Excalidraw's P2P Collaboration Feature
date: 2020-03-29
author: idlewinn
link: https://twitter.com/edwinlin1987
image: og.png
---

[Excalidraw](https://excalidraw.com/) is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them. As tech companies started to institute mandatory work from home policies due to the [COVID-19 pandemic](https://en.wikipedia.org/wiki/2019%E2%80%9320_coronavirus_pandemic), we realized that Excalidraw could be the perfect substitute for the whiteboard that is usually required for systems design interviews.

<!-- end -->

The problem was that Excalidraw was built to be a fundamentally single-player experience. However, we ([@idlewinn](https://twitter.com/edwinlin1987) and [@floydophone](https://twitter.com/floydophone)) were able to quickly add basic multiplayer support over a long weekend, and are continuing to make improvements to it every day.

## The requirements

- We wanted to be able to use this in our day jobs. In order to do that, it had to support [end-to-end encryption](/end-to-end-encryption/).
- We did not want to store anything server-side. Therefore, we opted for a pseudo-P2P model, where a central server relays end-to-end encrypted messages to all the peers in the room, but does no centralized coordination.

## Excalidraw's single-player architecture

Originally, Excalidraw kept an array of all the different drawn shapes -- called `ExcalidrawElement`s -- in Z-index order. The `ExcalidrawElement` interface looked something like this:

```typescript
interface ExcalidrawElement {
  id: string;
  type: "square" | "circle" | "arrow";
  width: number;
  height: number;
  // ... other fields describing the shape ...
  canvas: HTMLCanvasElement;
  isSelected: boolean;
}
```

This architecture was very easy to use client-side, but as we'll see, presented some challenges as we moved to multiplayer.

## Moving to multiplayer

We actually had two parallel multiplayer implementations. One was built on top of [Firebase](https://firebase.com/), the other on top of [Socket.io](https://socket.io/). We ended up going with Socket.io, but both implementations developed the same set of bugs, and the mitigations we applied in one branch were easily ported to the other.

Conceptually, we want to share the array of `ExcalidrawElement`s amongst multiple players. The first step was to refactor `ExcalidrawElement` to remove any state specific to a single player. This meant pulling out `canvas` and `isSelected` properties and storing them in a different data structure, as an `HTMLCanvasElement` can't be shared over the network, and every player will have a different selection state. Once we had these properties moved out, we started sharing the `ExcalidrawElement` array between peers.

## Socket.IO

When we started investigating multiplayer, we knew that WebSockets would be an important part of the this project because they enable rapid message based communication between clients without polling the server - in order to provide a smooth user experience we would need to sync very frequently. Socket.IO was a great candidate for this project because it's a popular open source library with a lot of support, it has many features relevant to our use case right out of the box, and it has some level of support even for browsers that don't have support for the [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API).

Some features of Socket.IO that are particularly beneficial to multiplayer:

- **Auto-reconnection:** if you get disconnected for any reason, you don't need to reload the page to see updates.
- **Binary Support:** our encrypted messages can be passed between the client and the server back to other clients as `ArrayBuffer`, so we don't need to convert them to other data structures at any point in the transit.
- **Room Support:** we want to limit updates only to the clients that care about them, and having a separate room per collaborative session was a straightforward way to implement that.

## Dealing with conflicts: adding new elements

Once we started sharing state between peers, we immediately ran into issues. The first was when elements were added.

In single-player Excalidraw, we would just append the element to the end of the array. In multiplayer Excalidraw, there was a race condition. If _peer A_ is adding a new element while _peer B_ is changing the color of an existing element, _peer C_ will either lose _peer A_'s new element or _peer B_'s edits. Even worse, if _peer A_ receives _peer B_'s updates while they are editing, the editor would delete the element from right under their mouse! This was clearly a bad user experience and we had to fix it before sharing this with anyone.

https://excalidraw.com/#json=5068269564198912,PmL8fegqNyHb0fKxoG6YAA

To fix this, we adopted an architecture of _merging states_ when we receive them. When _peer A_ receives an update from _peer B_, it looks at all the `ExcalidrawElement.id`s it has locally, and all of the incoming `ExcalidrawElement.id`s, and creates a new `ExcalidrawElement` array containing the union of both the local and incoming set.

## Dealing with conflicts: deleting elements

There's a big problem with the model above. If we're always keeping the union of all of the elements, there will be no way to delete any shapes from the canvas, as other peers would immediately resync the deleted shape and it would be merged back into the array.

To solve this problem, we used a method called [tombstoning](<https://en.wikipedia.org/wiki/Tombstone_(programming)>). We added a new field to the `ExcalidrawElement` interface called `isDeleted`. Peers would set this field to `true` to delete an element, and would filter the deleted elements out of the array at runtime.

https://excalidraw.com/#json=5148123005452288,FnrZbAe4qkHQCSd2BSkUIQ

The problem with tombstoning is that the size of your list grows forever, as nothing is ever removed from the array. We don't consider this a problem as a shared whiteboard session will only contain a few dozen shapes, but we do remove any element with `isDeleted` set to true when saving to persistent storage, so long-lived drawings where this may become a problem are cleaned up.

## Dealing with conflicts: merging concurrent edits

There is still a problem with the model. Concurrent edits will still be lost, as our merge algorithm only looks at new elements being added or removed, and not changes to existing elements themselves.

To fix this, we introduced a new field to `ExcalidrawElement`: a **version number**. Whenever a peer edits an element -- changing its color, deleting it, resizing it, etc -- it increments the version number of that element before sending the state to its peers. Then, when we merge multiple peers state together, we throw out old versions of each element and just keep the latest ones.

https://excalidraw.com/#json=5147452789227520,QaCOJixahz7VLHs3eG1s7g

This algorithm is simple but effective, and solved most of our collaboration problems.

## Dealing with conflicts: breaking ties

The version number only solves race conditions when players are editing different elements concurrently. What if they're editing the same element concurrently?

For Excalidraw, we don't really care! We think this will be a pretty rare situation, and that users will tolerate some jankiness if it happens.

With that said, it is important that all peers converge on the same state. Otherwise, they will see different versions of the drawing and it will be very confusing!

In order to do this, we made one final change. We added a new field called `versionNonce` to the `ExcalidrawElement`. Any time we increment the `version` of an element, we set `versionNonce` to a random integer. At merge time, if we encounter two elements with the same version number but different data (i.e. two players editing the same element at the same time), we break the tie by favoring the one with the lower `versionNonce`. This ensures that one player will deterministically "win" on every peer.

## Future work

One problem we haven't solved yet is implementing multiplayer undo/redo. Our hack around this was to clear the undo/redo stack whenever you receive an update from a new peer. While this is a subpar user experience, it does solve a common case of one person doing the majority of the whiteboarding, and quickly undoing their most recent action.

Maybe you can help us solve this problem! Go to [our GitHub](https://github.com/excalidraw/excalidraw), fork the repo, and contribute!


================================================
FILE: content/blog/deprecating-excalidraw-electron/index.md
================================================
---
title: Deprecating Excalidraw Electron in favor of the Web version
date: 2020-12-17
author: tomayac
link: https://twitter.com/tomayac
image: excalidraw-icon.png
---

On the [Excalidraw project](https://github.com/excalidraw), we have decided to deprecate [Excalidraw Desktop](https://github.com/excalidraw/excalidraw-desktop), an [Electron](https://www.electronjs.org/) wrapper for Excalidraw, in favor of the web version that you can—and always could—find at [excalidraw.com](https://excalidraw.com/). After a careful analysis, we have decided that [Progressive Web App](https://web.dev/pwa/) (PWA) is the future we want to build upon. Read on to learn why.

<!-- end -->

## How Excalidraw Desktop came into being

Soon after [@vjeux](https://twitter.com/vjeux) created the initial version of Excalidraw in January 2020 and [blogged about it](/reflections-on-excalidraw/), he proposed the following in [Issue #561](https://github.com/excalidraw/excalidraw/issues/561#issue-555138343):

> Would be great to wrap Excalidraw within Electron (or equivalent) and publish it as a [platform-specific] application to the various app stores.

The immediate reaction by [@voluntadpear](https://github.com/voluntadpear) was to suggest:

> What about making it a PWA instead? Android currently supports adding them to the Play Store as Trusted Web Activities and hopefully iOS will do the same soon. On Desktop, Chrome lets you download a desktop shortcut to a PWA.

The decision that [@vjeux](https://github.com/vjeux) took in the end was simple:

> We should do both :)

While work on converting the version of Excalidraw into a PWA was started by [@voluntadpear](https://github.com/voluntadpear) and later others, [@lipis](https://github.com/lipis) independently [went ahead](https://github.com/excalidraw/excalidraw/issues/561#issuecomment-579573783) and created a [separate repo](https://github.com/excalidraw/excalidraw-desktop) for Excalidraw Desktop.

To this day, the initial goal set by [@vjeux](https://github.com/vjeux), that is, to submit Excalidraw to the various app stores, has not been reached yet. Honestly, no one has even started the submission process to any of the stores. But why is that? Before I answer, let's look at Electron, the platform.

## What is Electron?

The unique selling point of [Electron](https://www.electronjs.org/) is that it allows you to _"build cross-platform desktop apps with JavaScript, HTML, and CSS"_. Apps built with Electron are _"compatible with Mac, Windows, and Linux"_, that is, _"Electron apps build and run on three platforms"_. According to the homepage, the hard parts that Electron makes easy are [automatic updates](https://www.electronjs.org/docs/api/auto-updater), [system-level menus and notifications](https://www.electronjs.org/docs/api/menu), [crash reporting](https://www.electronjs.org/docs/api/crash-reporter), [debugging and profiling](https://www.electronjs.org/docs/api/content-tracing), and [Windows installers](https://www.electronjs.org/docs/api/auto-updater#windows). Turns out, some of the promised features need a detailed look at the small print.

- For example, automatic updates _"are [currently] only [supported] on macOS and Windows. There is no built-in support for auto-updater on Linux, so it is recommended to use the distribution's package manager to update your app"_.

- Developers can create system-level menus by calling `Menu.setApplicationMenu(menu)`. On Windows and Linux, the menu will be set as each window's top menu, while on macOS there are many system-defined standard menus, like the [Services](https://developer.apple.com/documentation/appkit/nsapplication/1428608-servicesmenu?language=objc) menu. To make one's menus a standard menu, developers should set their menu's `role` accordingly, and Electron will recognize them and make them become standard menus. This means that a lot of menu-related code will use the following platform check: `const isMac = process.platform === 'darwin'`.

- Windows installers can be made with [windows-installer](https://github.com/electron/windows-installer). The README of the project highlights that _"for a production app you need to sign your application. Internet Explorer's SmartScreen filter will block your app from being downloaded, and many anti-virus vendors will consider your app as malware unless you obtain a valid cert"_ [sic].

Looking at just these three examples, it is clear that Electron is far from "write once, run everywhere". Distributing an app on app stores requires [code signing](https://www.electronjs.org/docs/tutorial/code-signing), a security technology for certifying app ownership. Packaging an app requires using tools like [electron-forge](https://github.com/electron-userland/electron-forge) and thinking about where to host packages for app updates. It gets complex relatively quickly, especially when the objective truly is cross platform support. I want to note that it is _absolutely_ possible to create stunning Electron apps with enough effort and dedication. For Excalidraw Desktop, we were not there.

## Where Excalidraw Desktop left off

Excalidraw Desktop so far is basically the Excalidraw web app bundled as an [`.asar`](https://github.com/electron/asar) file with an added **About Excalidraw** window. The look and feel of the application is almost identical to the web version.

<figure>
  <img src="excalidraw-desktop.png" alt="The Excalidraw Desktop application running in an Electron wrapper.">
  <figcaption>Excalidraw Desktop is almost indistinguishable from the Web version</figcaption>
</figure>

<figure>
  <img src="about-excalidraw.png" alt="The Excalidraw Desktop 'About' window displaying the version of the Electron wrapper and the Web app.">
  <figcaption>The <strong>About Excalidraw</strong> menu providing insights into the versions</figcaption>
</figure>

On macOS, there is now a system-level menu at the top of the application, but since none of the menu actions—apart from **Close Window** and **About Excalidraw**—are hooked up to to anything, the menu is, in its current state, pretty useless. Meanwhile, all actions can of course be performed via the regular Excalidraw toolbars and the context menu.

<figure>
  <img src="menu.png" alt="The Excalidraw Desktop menu bar on macOS with the 'File', 'Close Window' menu item selected.">
  <figcaption>The menu bar of Excalidraw Desktop on macOS</figcaption>
</figure>

We use [electron-builder](https://github.com/electron-userland/electron-builder), which supports [file type associations](https://www.electron.build/configuration/configuration#PlatformSpecificBuildOptions-fileAssociations). By double-clicking an `.excalidraw` file, ideally the Excalidraw Desktop app should open. The relevant excerpt of our `electron-builder.json` file looks like this:

```json
{
  "fileAssociations": [
    {
      "ext": "excalidraw",
      "name": "Excalidraw",
      "description": "Excalidraw file",
      "role": "Editor",
      "mimeType": "application/json"
    }
  ]
}
```

Unfortunately, in practice, this does not always work as intended, since, depending on the installation type (for the current user, for all users), apps on Windows&nbsp;10 do not have the rights to associate a file type to themselves.

These shortcomings and the pending work to make the experience truly app-like on _all_ platforms (which, again, with enough effort _is_ possible) were a strong argument for us to reconsider our investment in Excalidraw Desktop. The way bigger argument for us, though, was that we foresee that for _our_ use case, we do not need all the features Electron offers. The grown and still growing set of capabilities of the web serves us equally well, if not better.

## How the web serves us today and in the future

Even in 2020, [jQuery](https://jquery.com/) is still [incredibly popular](https://almanac.httparchive.org/en/2020/javascript#libraries). For many developers it has become a habit to use it, despite the fact that today they [might not need jQuery](http://youmightnotneedjquery.com/). There is a similar resource for Electron, aptly called [You Might Not Need Electron](https://youmightnotneedelectron.com/). Let me outline why we think we do not need Electron.

### Installable Progressive Web App

Excalidraw today is an [installable](https://web.dev/installable/) Progressive Web App with a [service worker](https://excalidraw.com/service-worker.js) and a [Web App Manifest](https://excalidraw.com/manifest.json). It caches all its resources in two caches, one for fonts and font-related CSS, and one for everything else.

<figure>
  <img src="excalidraw-cache.png" alt="Chrome DevTools Application tab showing the two Excalidraw caches.">
  <figcaption>Excalidraw's cache contents</figcaption>
</figure>

This means the application is fully offline-capable and can run without a network connection. Chromium-based browsers on both desktop and mobile prompt the user to install the app. You can see the installation prompt in the screenshot below.

<figure>
  <img src="install-excalidraw.png" alt="Excalidraw prompting the user to install the app in Chrome on macOS.">
  <figcaption>The Excalidraw install dialog in Chrome</figcaption>
</figure>

Excalidraw is configured to run as a standalone application, so when you install it, you get an app that runs in its own window. It is fully integrated in the operating system's multitasking UI and gets its own app icon on the home screen, Dock, or task bar; depending on the platform where you install it.

<figure>
  <img src="excalidraw-pwa.png" alt="Excalidraw running in its own window.">
  <figcaption>The Excalidraw PWA in a standalone window</figcaption>
</figure>

<figure>
  <img src="excalidraw-icon.png" alt="Excalidraw icon on the macOS Dock.">
  <figcaption>The Excalidraw icon on the macOS Dock</figcaption>
</figure>

### File system access

Excalidraw uses [browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access) for accessing the file system of the operating system. On supporting browsers, this allows for a true open→edit→save workflow and actual over-saving and "save as", with a transparent fallback for other browsers. You can learn more about this feature in my blog post [Reading and writing files and directories with the browser-fs-access library](/browser-fs-access/).

### Drag and drop support

Files can be dragged and dropped onto the Excalidraw window just as in platform-specific applications. On a browser that supports the [File System Access API](https://web.dev/file-system-access/), a dropped file can be immediately edited and the modifications be saved to the original file. This is so intuitive that you sometimes forget that you are dealing with a web app.

### Clipboard access

Excalidraw works well with the operating system's clipboard. Entire Excalidraw drawings or also just individual objects can be copied and pasted in `image/png` and `image/svg+xml` formats, allowing for an easy integration with other platform-specific tools like [Inkscape](https://inkscape.org/) or web-based tools like [SVGOMG](https://jakearchibald.github.io/svgomg/).

<figure>
  <img src="clipboard.png" alt="Excalidraw context menu showing the 'copy to clipboard as SVG' and 'copy to clipboard as PNG' menu items.">
  <figcaption>The Excalidraw context menu offering clipboard actions</figcaption>
</figure>

### File handling

Excalidraw already supports the experimental [File Handling API](https://web.dev/file-handling/), which means `.excalidraw` files can be double-clicked in the operating system's file manager and open directly in the Excalidraw app, since Excalidraw registers as a file handler for `.excalidraw` files in the operating system.

### Declarative link capturing

Excalidraw drawings can be shared by link. Here is an [example](https://excalidraw.com/#json=4646308765761536,jwZJW8JsOM75vdhqG2nBgA). In the future, if people have Excalidraw installed as a PWA, such links will not open in a browser tab, but launch a new standalone window. Pending implementation, this will work thanks to [declarative link capturing](https://github.com/WICG/sw-launch/blob/master/declarative_link_capturing.md), an, at the time of writing, bleeding-edge proposal for a new web platform feature.

## Conclusion

The web has come a long way, with more and more features landing in browsers that only a couple of years or even months ago were unthinkable on the web and exclusive to platform-specific applications. Excalidraw is at the forefront of what is possible in the browser, all while acknowledging that not all browsers on all platforms support each feature we use. By betting on a progressive enhancement strategy, we enjoy the latest and greatest wherever possible, but without leaving anyone behind. Best viewed in _any_ browser.

Electron has served us well, but in 2020 and beyond, we can live without it. Oh, and for that objective of [@vjeux](https://github/com/vjeux): since the Android Play Store now accepts PWAs in a container format called [Trusted Web Activity](https://web.dev/using-a-pwa-in-your-android-app/) and since the [Microsoft Store supports PWAs](https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps-edgehtml/microsoft-store), too, you can expect Excalidraw in these stores in the not too distant future. Meanwhile, you can always use and install [Excalidraw in and from the browser](https://excalidraw.com/).

## Acknowledgements

This article was reviewed by [@lipis](https://github.com/lipis), [@dwelle](https://github.com/dwelle), and [Joe Medley](https://github.com/jpmedley).


================================================
FILE: content/blog/enabling-translations/index.md
================================================
---
title: Enabling Translations
date: 2020-04-16
author: Lipis
link: https://twitter.com/lipis
---

From the early days people asked for [Excalidraw](https://excalidraw.com) to be translated to other languages. Translation infrastructure and community maintenance have historically been a pain to maintain. Thankfully, with projects like [Crowdin](https://crowdin.com/project/excalidraw) and [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector), Excalidraw is now translated in 20 languages and the whole process has been very low maintenance.

<!-- end -->

## Technical Infrastructure

The first thing we need to do is to update all our literal strings that need to be translated to be using translated strings from a dictionary.

```js
import { t } from "../i18n";

<span>{t("labels.paste")}</span>;
```

The translated strings are stored as JSON in a file per language like this.

```js
// en.json
{
  "labels": {
    "paste": "Paste",
    "selectAll": "Select all",
    "copy": "Copy",
    "copyAsPng": "Copy to clipboard as PNG",
    // ...
  }
}
```

We started using a fully fledged internalization library but unfortunately it was a massive dependency and caused multiple React re-renders on startup and added one long round-trip. So instead [we rolled our own](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts) in less than 100 lines of code.

Since we don't have that many strings, we decided to bundle all the languages by default to avoid an expensive roundtrip during startup if you're not using English. For this we use `require`.

```js
export const languages = [
  { lng: "en", label: "English", data: require("./locales/en.json") },
  { lng: "bg-BG", label: "Български", data: require("./locales/bg-BG.json") },
  { lng: "de-DE", label: "Deutsch", data: require("./locales/de-DE.json") },
  // ...
];
```

The `t` function is pretty simple, where it splits the path by `.` and looks up both the current and fallback languages.

```ts
// t("labels.paste")
//   + current  {"labels": {"paste": "Coller"}}
//   + fallback {"labels": {"paste": "Paste"}}
// -> "Coller"
export function t(path: string, replacement?: { [key: string]: string }) {
  const parts = path.split(".");
  return (
    findPartsForData(currentLanguage.data, parts) ||
    findPartsForData(fallbackLanguage.data, parts)
  );
}
```

The only external library we're using is [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector) which has a lot of great heuristics to figure out what language the user is currently using.

Finally the last piece of the puzzle is to be able to change the language. We opted for a simple `<select>` element.

![Language selector](language-selector.png)

## Enter Crowdin

Previously, if you wanted to contribute, you had to edit one of the JSON files on GitHub and send a pull request. While this process worked, managing this was tedious enough to make us look for alternatives. We chose [Crowdin](https://crowdin.com/project/excalidraw) which dramatically improved the whole process.

The initial setup was easy, you create a new project on Crowdin, upload your `en.json` file and connect a GitHub account. Then, any time a translation is added, a pull request is created/updated with the JSON files being modified.

[![Pull Request](pull-request.png)](https://github.com/excalidraw/excalidraw/pull/1416/commits)

The first cool feature enabled by Crowdin is a language completion illustration. It lets you see at a quick glance what's the current translation status for all the strings. This is also a great motivator for people to translate the languages they speak!

[![Language Completion](language-completion.png)](https://crowdin.com/project/excalidraw)

Where Crowdin really excels is in the translator experience. For every string, it offers many translation suggestions. In practice the workflow is to chose a string to translate, scroll through the suggestions and click on one that works and repeat. Crowdin also has cool features like capitalization warnings.

![Translation Help](translation-help.png)

Finally, when a new string is added to the default language, all the language contributors receive a notification saying that new strings are available for translation. This helps keep the entire application properly translated!


================================================
FILE: content/blog/end-to-end-encryption/index.md
================================================
---
title: End-to-End Encryption in the Browser
date: 2020-03-21
author: vjeux
link: https://twitter.com/vjeux
image: og.png
---

[Excalidraw](https://excalidraw.com/) is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them. It is very handy to dump your thoughts many of which are sensitive: designs for new features not yet released, interview questions, org charts, etc.

<!-- end -->

By default, **Excalidraw** doesn’t send anything you draw to the server. But sometimes it is useful to be able to send a link to the scene you are working on to someone else.

You could save the drawing to a file, send it to the other person, and have them open it. But that's pretty cumbersome. In this post we'll show how it’s possible to share just a link without the server having access to the data.

## Traditional Website Architecture

In a traditional website architecture, you’d save a scene by sending it to the server, which gives you a shareable URL. The recipient then downloads the scene data from the server.

In that world, you trust the server to contain your information but you don't have to trust the pipes in between the client and the server, because you use HTTPS to encrypt the data.

https://excalidraw.com/#json=5649116445016064,yOfExolZoMhtGnysT3-LWA

This works well unless the server gets compromised. The attacker will have access to every single drawing ever made! This is something we'd like to avoid.

## End-to-End Encryption

WhatsApp popularized end-to-end encryption, a technique that allows various clients to communicate without the server being able to read the content of the communication.

The idea is to encrypt the content before sending it to the server. The server would just store the encrypted blob and send it back to the client.

https://excalidraw.com/#json=5645858175451136,8w-G0ZXiOfRYAn7VWpANxw

The biggest challenge with this architecture is how to distribute the key to encrypt the message in such a way that the server cannot see it.

Thankfully, in the context of a website, we can exploit the hash part of the URL. Anything that's added after the `#` doesn't get sent to the server, but is readable from the client-side JavaScript code.

https://excalidraw.com/#json=5660568841093120,vki3y9xuEulFVHDqt-PBMw

## Show me the code

Fortunately, the Web Cryptography APIs are now widely available to all the browsers that let us implement this. That said, the APIs to deal with encryption, keys and binary data are not the most straightforward, this next section walks you through how to wire it all together.

### Upload

We generate a random key that will be used to encrypt the data.

```javascript
const key = await window.crypto.subtle.generateKey(
  { name: "AES-GCM", length: 128 },
  true, // extractable
  ["encrypt", "decrypt"],
);
```

We encrypt the content with that random key. In this case, we only encrypt the content once with the random key so we don't need an `iv` and can leave it filled with 0 ([I hope...](https://www.youtube.com/watch?v=LP1t_pzxKyE)).

```javascript
const encrypted = await window.crypto.subtle.encrypt(
  { name: "AES-GCM", iv: new Uint8Array(12) /* don't reuse key! */ },
  key,
  new TextEncoder().encode(JSON.stringify(content)),
);
```

We upload the encrypted content to the server. Note that we don't send the key to the server!

```javascript
const response = await (
  await fetch("/upload", {
    method: "POST",
    body: encrypted,
  })
).json();
```

We generate the shareable URL. We use the `jwk` encoding in order to extract a base64 version of the key instead of having a binary encoded one.

```javascript
const objectURL = response.url;
const objectKey = (await window.crypto.subtle.exportKey("jwk", key)).k;
const url = objectURL + "#key=" + objectKey;
// Example: https://excalidraw.com/?scene=1234#key=BQ1moYESmTEXgtA1KozyVw
```

## Download

In the opposite direction, we download the file back from the server.

```javascript
const response = await fetch(`/download?id={id}`);
const encrypted = await response.arrayBuffer();
```

The key that we encoded in the url is the `k` field of the `jwk` object that represents the key. In order to get back a full key object we need to reproduce all the other fields that are static. It's pretty verbose but it works!

```javascript
const objectKey = window.location.hash.slice("#key=".length);
const key = await window.crypto.subtle.importKey(
  "jwk",
  {
    k: objectKey,
    alg: "A128GCM",
    ext: true,
    key_ops: ["encrypt", "decrypt"],
    kty: "oct",
  },
  { name: "AES-GCM", length: 128 },
  false, // extractable
  ["decrypt"],
);
```

We decrypt the message, decode it to string and parse it back as JSON.

```javascript
const decrypted = await window.crypto.subtle.decrypt(
  { name: "AES-GCM", iv: new Uint8Array(12) },
  key,
  encrypted,
);
const decoded = new window.TextDecoder().decode(new Uint8Array(decrypted));
const content = JSON.parse(decoded);
```

## Conclusion

As the maintainer of [Excalidraw](https://excalidraw.com/), I now sleep much better at night. If the hosting service gets compromised, it doesn't really matter as none of the content can be decrypted without the key.

It also gives me the peace of mind to use **Excalidraw** for work related projects knowing that nothing will leak.

If you're building a website that needs to store data on the server, you may want to add end-to-end encryption, it's pretty easy!


================================================
FILE: content/blog/excalidraw-and-fugu/index.md
================================================
---
title: "Excalidraw and Fugu: Improving Core User Journeys"
date: 2021-05-21
note: 'This post appeared first on <a href="https://web.dev/excalidraw-and-fugu/">web.dev</a>.'
author: tomayac
link: https://twitter.com/tomayac
image: FcDeDjh1bW8zAHIzA2BF
---

Any sufficiently advanced technology is indistinguishable from magic. Unless you understand it. My name is Thomas Steiner, I work in Developer Relations at Google and in this write-up of my Google I/O talk, I will look at some of the new Fugu APIs and how they improve core user journeys in the Excalidraw PWA, so you can take inspiration from these ideas and apply them to your own apps. If you prefer watching it, see the video below

<!-- end -->

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/EK1AkxgQwro" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

## How I came to Excalidraw

I want to start with a story. On January 1st, 2020, [Christopher Chedeau](https://twitter.com/vjeux), a software engineer at Facebook, [tweeted](https://twitter.com/Vjeux/status/1212503324982792193) about a small drawing app he had started to work on. With this tool, you could draw boxes and arrows that feel cartoony and hand-drawn. The next day, you could also draw ellipses and text, as well as select objects and move them around. On January 3, the app had gotten its name, Excalidraw, and, like with every good side project, buying the [domain name](https://excalidraw.com/) was one of Christopher's first acts. By now, you could use colors and export the whole drawing as a PNG.

<img src="VbicbA7xj5azVcDUBSKt.png" alt="Screenshot of the Excalidraw prototype application showing that it supported rectangles, arrows, ellipses, and text." width="800" height="600">

On January 15, Christopher put out a [blog post](https://blog.vjeux.com/2020/uncategorized/reflections-on-excalidraw.html) that drew a lot of attention on Twitter, including mine. The post started with some impressive stats:

- 12K unique active users
- 1.5K stars on GitHub
- 26 contributors

For a project that started a mere two weeks ago, that's not bad at all. But the thing that truly spiked my interest was further down in the post. Christopher wrote that he tried something new this time: _giving everyone who landed a pull request unconditional commit access._ The same day of reading the blog post, I had a [pull request](https://github.com/excalidraw/excalidraw/pull/388) up that added File System Access API support to Excalidraw, fixing a [feature request](https://github.com/excalidraw/excalidraw/issues/169) that someone had filed.

<img src="9VJ9EqPzKdzUpxFeM5wH.png" alt="Screenshot of the tweet where I announce my PR." width="550" height="424">

My pull request was merged a day later and from thereon, I had full commit access. Needless to say, I didn't abuse my power. And nor did anybody else from the 149 contributors so far.

Today, [Excalidraw](https://excalidraw.com/) is a full-fledged installable progressive web app with offline support, a stunning dark mode, and yes, the ability to open and save files thanks to the File System Access API.

<img src="Wzz6UELRpcvkKZQtmVmc.png" alt="Screenshot of the Excalidraw PWA in today's state." width="800" height="537">

## Lipis on why he dedicates so much of his time to Excalidraw

So this marks the end of my "how I came to Excalidraw" story, but before I dive into some of Excalidraw's amazing features, I have the pleasure to introduce Panayiotis. Panayiotis Lipiridis, on the Internet simply known as [lipis](https://github.com/lipis), is the most prolific contributor to Excalidraw. I asked lipis what motivates him to dedicate so much of his time to Excalidraw:

> Like everyone else I learned about this project from Christopher's tweet. My first contribution was to add the [Open Color library](https://yeun.github.io/open-color/), the colors that are still part of Excalidraw today. As the project grew and we had quite many requests, my next big contribution was to build a backend for storing drawings so users could share them. But what really drives me to contribute is that whoever tried Excalidraw is looking to find excuses to use it again.

I fully agree with lipis. Whoever tried Excalidraw is looking to find excuses to use it again.

## Excalidraw in action

I want to show you now how you can use Excalidraw in practice. I'm not a great artist, but the Google I/O logo is simple enough, so let me give it a try. A box is the "i", a line can be the slash, and the "o" is a circle. I hold down <kbd>shift</kbd>, so I get a perfect circle. Let me move the slash a little, so it looks better. Now some color for the "i" and the "o". Blue is good. Maybe a different fill style? All solid, or cross-hatch? Nah, hachure looks great. It's not perfect, but that's the idea of Excalidraw, so let me save it.

<video autoplay loop muted playsinline src="wK9jDdHG7A7qT5ViOuEQ.mp4"></video>

I click the save icon and enter a file name in the file save dialog. In Chrome, a browser that supports the File System Access API, this is not a download, but a true save operation, where I can choose the location and name of the file, and where, if I make edits, I can just save them to the same file.

<video autoplay loop muted playsinline src="HvKcKNk8Q3bbaVe36E3T.mp4"></video>

Let me change the logo and make the "i" red. If I now click save again, my modification is saved to the same file as before. As a proof, let me clear the canvas and reopen the file. As you can see, the modified red-blue logo is there again.

<video autoplay loop muted playsinline src="XzlUi88cPDYl8YFAH1J8.mp4"></video>

## Working with files

On browsers that currently don't support the File System Access API, each save operation is a download, so when I make changes, I end up with multiple files with an incrementing number in the filename that fill up my Downloads folder. But despite this downside, I can still save the file.

<video autoplay loop muted playsinline src="1oVPIESBNhoL4AhOSNli.mp4"></video>

### Opening files

So what's the secret? How can opening and saving work on different browsers that may or may not support the File System Access API? Opening a file in Excalidraw happens in a function called `loadFromJSON)(`), which in turn calls a function called `fileOpen()`.

```js
export const loadFromJSON = async (localAppState: AppState) => {
  const blob = await fileOpen({
    description: "Excalidraw files",
    extensions: [".json", ".excalidraw", ".png", ".svg"],
    mimeTypes: ["application/json", "image/png", "image/svg+xml"],
  });
  return loadFromBlob(blob, localAppState);
};
```

The `fileOpen()` function that comes from a small library I wrote called [browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access) that we use in Excalidraw. This library provides file system access through the [File System Access API](/file-system-access/) with a legacy fallback, so it can be used in any browser.

Let me first show you the implementation for when the API is supported. After negotiating the accepted MIME types and file extensions, the central piece is calling the File System Access API's function `showOpenFilePicker()`. This function returns an array of files or a single file, dependent on whether multiple files are selected. All that's left then is to put the file handle on the file object, so it can be retrieved again.

```js
export default async (options = {}) => {
  const accept = {};
  // Not shown: deal with extensions and MIME types.
  const handleOrHandles = await window.showOpenFilePicker({
    types: [
      {
        description: options.description || "",
        accept: accept,
      },
    ],
    multiple: options.multiple || false,
  });
  const files = await Promise.all(handleOrHandles.map(getFileWithHandle));
  if (options.multiple) return files;
  return files[0];
  const getFileWithHandle = async (handle) => {
    const file = await handle.getFile();
    file.handle = handle;
    return file;
  };
};
```

The fallback implementation relies on an `input` element of type `"file"`. After the negotiation of the to-be-accepted MIME types and extensions, the next step is to programmatically click the input element so the file open dialog shows. On change, that is, when the user has selected one or multiple files, the promise resolves.

```js
export default async (options = {}) => {
  return new Promise((resolve) => {
    const input = document.createElement("input");
    input.type = "file";
    const accept = [
      ...(options.mimeTypes ? options.mimeTypes : []),
      options.extensions ? options.extensions : [],
    ].join();
    input.multiple = options.multiple || false;
    input.accept = accept || "*/*";
    input.addEventListener("change", () => {
      resolve(input.multiple ? Array.from(input.files) : input.files[0]);
    });
    input.click();
  });
};
```

### Saving files

Now to saving. In Excalidraw, saving happens in a function called `saveAsJSON()`. It first serializes the Excalidraw elements array to JSON, converts the JSON to a blob, and then calls a function called `fileSave()`. This function is likewise provided by the [browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access) library.

```js
export const saveAsJSON = async (
  elements: readonly ExcalidrawElement[],
  appState: AppState,
) => {
  const serialized = serializeAsJSON(elements, appState);
  const blob = new Blob([serialized], {
    type: 'application/vnd.excalidraw+json',
  });
  const fileHandle = await fileSave(
    blob,
    {
      fileName: appState.name,
      description: 'Excalidraw file',
      extensions: ['.excalidraw'],
    },
    appState.fileHandle,
  );
  return { fileHandle };
};
```

Again let me first look at the implementation for browsers with File System Access API support. The first couple of lines look a little involved, but all they do is negotiate the MIME types and file extensions. When I have saved before and already have a file handle, no save dialog needs to be shown. But if this is the first save, a file dialog gets displayed and the app gets a file handle back for future use. The rest is then just writing to the file, which happens through a [writable stream](/streams/).

```js
export default async (blob, options = {}, handle = null) => {
  options.fileName = options.fileName || "Untitled";
  const accept = {};
  // Not shown: deal with extensions and MIME types.
  handle =
    handle ||
    (await window.showSaveFilePicker({
      suggestedName: options.fileName,
      types: [
        {
          description: options.description || "",
          accept: accept,
        },
      ],
    }));
  const writable = await handle.createWritable();
  await writable.write(blob);
  await writable.close();
  return handle;
};
```

#### The "save as" feature

If I decide to ignore an already existing file handle, I can implement a "save as" feature to create a new file based on an existing file. To show this, let me open an existing file, make some modification, and then not overwrite the existing file, but create a new file by using the save-as feature. This leaves the original file intact.

<video autoplay loop muted playsinline src="oTNuosQmoMBP2G7XR8Wb.mp4"></video>

The implementation for browsers that don't support the File System Access API is short, since all it does is create an anchor element with a `download` attribute whose value is the desired filename and a blob URL as its `href` attribute value.

```js
export default async (blob, options = {}) => {
  const a = document.createElement("a");
  a.download = options.fileName || "Untitled";
  a.href = URL.createObjectURL(blob);
  a.addEventListener("click", () => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};
```

The anchor element then gets clicked programmatically. To prevent memory leaks, the blob URL needs to be revoked after use. As this is just a download, no file save dialog gets shown ever, and all files land in the default `Downloads` folder.

<video autoplay loop muted playsinline src="1oVPIESBNhoL4AhOSNli.mp4"></video>

## Drag and drop

One of my favorite system integrations on desktop is drag and drop. In Excalidraw, when I drop an `.excalidraw` file onto the application, it opens right away and I can start editing. On browsers that support the File System Access API, I can then even immediately save my changes. No need to go through a file save dialog since the required file handle has been obtained from the drag and drop operation.

<video autoplay loop muted playsinline src="aOPKhOOe20od8uOzehdy.mp4"></video>

The secret for making this happen is by calling `getAsFileSystemHandle()` on the [data transfer](/datatransfer/) item when the File System Access API is supported. I then pass this file handle to `loadFromBlob()`, which you may remember from a couple paragraphs above. So many things you can do with files: opening, saving, over-saving, dragging, dropping. My colleague Pete and I have documented all these tricks and more in [our article](/file-system-access/) so you can catch up in case all this went a little too fast.

```js
const file = event.dataTransfer?.files[0];
if (file?.type === 'application/json' || file?.name.endsWith('.excalidraw')) {
  this.setState({ isLoading: true });
  // Provided by browser-fs-access.
  if (supported) {
    try {
      const item = event.dataTransfer.items[0];
      file as any.handle = await item as any
        .getAsFileSystemHandle();
    } catch (error) {
      console.warn(error.name, error.message);
    }
  }
  loadFromBlob(file, this.state).then(({ elements, appState }) =>
    // Load from blob
  ).catch((error) => {
    this.setState({ isLoading: false, errorMessage: error.message });
  });
}
```

## Sharing files

Another system integration currently on Android, Chrome OS, and Windows is through the [Web Share Target API](/web-share-target/). Here I am in the Files app in my `Downloads` folder. I can see two files, one of them with the non-descript name `untitled` and a timestamp. To check what it contains, I click on the three dots, then share, and one of the options that appears is Excalidraw. When I tap the icon, I can then see that the file just contains the I/O logo again.

<video autoplay loop muted playsinline src="x93JgKGcp1o8at5P7exv.mp4"></video>

## Lipis on the deprecated Electron version

One thing you can do with files that I haven't talked about yet is doubleclick them. What typically happens when you doubleclick a file is that the app that's associated with the file's MIME type opens. For example for `.docx` this would be Microsoft Word.

Excalidraw [used to have an Electron version](/deprecating-excalidraw-electron/) of the app that supported such file type associations, so when you double-clicked an `.excalidraw` file, the Excalidraw Electron app would open. Lipis, whom you have already met before, was both the creator and the deprecator of Excalidraw Electron. I asked him why he felt it was possible to deprecate the Electron version:

> People have been asking for an Electron app since the beginning, mainly because they wanted to open files by double-clicking. We also intended to put the app in app stores. In parallel, someone suggested creating a PWA instead, so we just did both. Luckily we were introduced to Project Fugu APIs like file system access, clipboard access, file handling, and more. With a sole click you can install the app on your desktop or mobile, without the extra weight of Electron. It was an easy decision to deprecate the Electron version, concentrate just on the web app, and make it the best-possible PWA. On top, we're now able to publish PWAs to the Play Store and the Microsoft Store! That's huge!

One could say Excalidraw for Electron was not deprecated because Electron is bad, not at all, but because the web has become good enough. I like this!

## File handling

When I say "the web has become good enough", it's because of features like the upcoming File Handling.

This is a regular macOS Big Sur installation. Now check out what happens when I right-click an Excalidraw file. I can choose to open it with Excalidraw, the installed PWA. Of course double-clicking would work, too, it's just less dramatic to demonstrate in a screencast.

<video autoplay loop muted playsinline src="Gz1w0Gey1XerN86sIF01.mp4"></video>

So how does this work? The first step is to make the file types my application can handle known to the operating system. I do this in a new field called `file_handlers` in the Web App Manifest. Its value is an array of objects with an action and an `accept` property. The action determines the URL path the operating system launches your app at and the accept object are key value pairs of MIME types and the associated file extensions.

```json
{
  "name": "Excalidraw",
  "description": "Excalidraw is a whiteboard tool...",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff",
  "file_handlers": [
    {
      "action": "/",
      "accept": {
        "application/vnd.excalidraw+json": [".excalidraw"]
      }
    }
  ]
}
```

The next step is to handle the file when the application launches. This happens in the `launchQueue` interface where I need to set a consumer by calling, well, `setConsumer()`. The parameter to this function is an asynchronous function that receives the `launchParams`. This `launchParams` object has a field called files that gets me an array of file handles to work with. I only care for the first one and from this file handle I get a blob that I then pass to our old friend `loadFromBlob()`.

```js
if ('launchQueue' in window && 'LaunchParams' in window) {
  window as any.launchQueue
    .setConsumer(async (launchParams: { files: any[] }) => {
      if (!launchParams.files.length) return;
      const fileHandle = launchParams.files[0];
      const blob: Blob = await fileHandle.getFile();
      blob.handle = fileHandle;
      loadFromBlob(blob, this.state).then(({ elements, appState }) =>
        // Initialize app state.
      ).catch((error) => {
        this.setState({ isLoading: false, errorMessage: error.message });
      });
    });
}
```

Again, if this went too fast, you can read more about the File Handling API in [my article](/file-handling/). You can enable file handling by setting the experimental web platform features flag. It's scheduled to land in Chrome later this year.

## Clipboard integration

Another cool feature of Excalidraw is the clipboard integration. I can copy my entire drawing or just parts of it into the clipboard, maybe adding a watermark if I feel like, and then paste it into another app. This is a web version of the Windows 95 Paint app by the way.

<video autoplay loop muted playsinline src="EHHQS78y6RJf21J1wD7y.mp4"></video>

The way this works is surprisingly simple. All I need is the canvas as a blob, which I then write onto the clipboard by passing a one-element array with a `ClipboardItem` with the blob to the `navigator.clipboard.write()` function. For more information on what you can do with the clipboard API, See Jason's and [my article](/async-clipboard/).

```js
export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) => {
  const blob = await canvasToBlob(canvas);
  await navigator.clipboard.write([
    new window.ClipboardItem({
      "image/png": blob,
    }),
  ]);
};

export const canvasToBlob = async (
  canvas: HTMLCanvasElement,
): Promise<Blob> => {
  return new Promise((resolve, reject) => {
    try {
      canvas.toBlob((blob) => {
        if (!blob) {
          return reject(
            new CanvasError(
              t("canvasError.canvasTooBig"),
              "CANVAS_POSSIBLY_TOO_BIG",
            ),
          );
        }
        resolve(blob);
      });
    } catch (error) {
      reject(error);
    }
  });
};
```

## Collaborating with others

### Sharing a session URL

Did you know that Excalidraw also has a collaborative mode? Different people can work together on the same document. To start a new session, I click on the live collaboration button and then start a session. I can share the session URL with my collaborators easily thanks to the [Web Share API](/web-share/) that Excalidraw has integrated.

<video autoplay loop muted playsinline src="7tbl5j0jrVZd3ffxhpoX.mp4"></video>

### Live collaboration

I have simulated a collaboration session locally by working on the Google I/O logo on my Pixelbook, my Pixel 3a phone, and my iPad Pro. You can see that changes I make on one device are reflected on all other devices.

I can even see all cursors move around. The Pixelbook's cursor moves steadily, since it's controlled by a trackpad, but the Pixel 3a phone's cursor and the iPad Pro's tablet cursor jump around, since I control these devices by tapping with my finger.

<video autoplay loop muted playsinline src="7muh13F0CjvKBntVrUTp.mp4"></video>

### Seeing collaborator statuses

To improve the realtime collaboration experience, there is even an idle detection system running. The cursor of the iPad Pro shows a green dot when I use it. The dot turns black when I switch to a different browser tab or app. And when I'm in the Excalidraw app, but just not doing anything, the cursor shows me as idle, symbolized by the three zZZs.

<video autoplay loop muted playsinline src="Y7vEI1qHTDJpHNdXjteS.mp4"></video>

Avid readers of our publications might be inclined to think that idle detection is realized through the [Idle Detection API](/idle-detection/), an early stage proposal that's been worked on in the context of Project Fugu. Spoiler alert: it's not. While we had an implementation based on this API in Excalidraw, in the end, we decided to go for a more traditional approach based on measuring pointer movement and page visibility.

<img src="SudM7tqa3ZooUJYx7aBB.png" alt="Screenshot of the Idle Detection feedback filed on the WICG Idle Detection repo." width="800" height="685">

We filed [feedback](https://github.com/WICG/idle-detection/issues/36) on why the Idle Detection API wasn't solving the use case we had. All Project Fugu APIs are being developed in the open, so everyone can chime in and have their voice heard!

## Lipis on what is holding back Excalidraw

Talking of which, I asked lipis one last question regarding what he thinks is missing from the web platform that holds back Excalidraw:

> The File System Access API is great, but you know what? Most files that I care about these days live in my Dropbox or Google Drive, not on my hard disk. I wish the File System Access API would include an abstraction layer for remote file systems providers like Dropbox or Google to integrate with and that developers could code against. Users could then relax and know their files are safe with the cloud provider they trust.

[👆 This talk was recorded in March 2021. We have since [introduced Excalidraw+](https://blog.excalidraw.com/introducing-excalidraw-plus/). Lipis' remark still holds for web apps in general, although we have solved the problem he raises for Excalidraw users.]

I fully agree with lipis, I live in the cloud, too. Here's hoping that this will be implemented soon.

## Tabbed application mode

Wow! We have seen a lot of really great API integrations in Excalidraw. [File system](/file-system-access/), [file handling](/file-handling/), [clipboard](\async-clipboard/), [web share](\web-share/), and [web share target](/web-share-target/). But here is one more thing. Up until now, I could only ever edit one document at a given time. Not anymore. Please enjoy for the first time an early version of tabbed application mode in Excalidraw. This is how it looks.

<video autoplay loop muted playsinline src="h8zrwaB8jBXVnQuxglpS.mp4"></video>

I have an existing file open in the installed Excalidraw PWA that's running in standalone mode. Now I open a new tab in the standalone window. This is not a regular browser tab, but a PWA tab. In this new tab I can then open a secondary file, and work on them independently from the same app window.

Tabbed application mode is in its early stages and not everything is set in stone. If you're interested, be sure to read up on the current status of this feature in [my article](/tabbed-application-mode/).

## Closing

To stay in the loop on this and other features, be sure to watch our [Fugu API tracker](https://fugu-tracker.web.app/). We're super excited to push the web forward and allow you to do more on the platform. Here's to an ever improving Excalidraw, and here's to all the amazing applications that you will build. Go start creating at [excalidraw.com](https://excalidraw.com/).

I can't wait to see some of the APIs that I have shown today pop up in your apps. My name is Tom, you can find me as [@tomayac](https://twitter.com/tomayac) on Twitter and the internet in general. Thank you very much for watching, and enjoy the rest of Google I/O.


================================================
FILE: content/blog/introducing-excalidraw-plus/index.md
================================================
---
title: Introducing Excalidraw+
date: 2021-05-03
author: Excalidraw Team
link: https://twitter.com/excalidraw
image: excalidraw-plus.png
---

Excalidraw has had tremendous success as a tool for drawing up your ideas, but it hasn't put teamwork on the center stage. Read on to find out what we've been up to for the past several months!

<!-- end -->

<hr/>

Excalidraw, the virtual whiteboard app for sketching hand-drawn like diagrams, has come a [long way](https://blog.excalidraw.com/one-year-of-excalidraw/). With the remote work becoming the status quo, Excalidraw was able to show its true potential and we've come to a point where over 100K people have used it just the last month!

While it has been extremely successful as a free open source project, the biggest source of complaints nowadays is around adoption within companies: How do you share diagrams with your co-workers easily? Can your company depend on the service for live collaboration? Is your data secure?

We've thought long and hard about how to make it happen in a volunteer-based open source model but haven't found a great solution. So we decided to **build a company around Excalidraw with the first product being Excalidraw+**.

## Going beyond Excalidraw

In Excalidraw+, whether you're a company or an individual, you can create a workspace where you can manage and organize all the drawings you have created. The drawings are easily accessible to your workspace co-workers, and securely saved to the cloud. The live collaboration is enabled by default with no extra steps necessary.

With account-based access you can rest assured only the right people will have access to your data, while still retain the ability to share specific drawings with outside collaborators or interviewees lest the need arise.

In the coming weeks we'll focus on making collaboration more robust, especially when collaborating in multiple continents.

We didn't want to keep you waiting any longer, but we are hard at work to bring many more awesome features to Excalidraw+.

**[Try Excalidraw+ for free](https://plus.excalidraw.com/?utm_source=excalidraw&utm_medium=blog&utm_campaign=launch)**, and then for $7/month.

<video src="./organize.mp4" autoplay playsinline loop muted style="width: 100%; height: auto;"></video>

## Going forward

One of our goals is to get Excalidraw to as many people as possible, and our commitment to the open source project remains strong. Excalidraw+ is built on the same platform that you can build on yourself using our npm package, and you have, from [HackerRank⁠](https://www.hackerrank.com/), [Replit⁠](https://twitter.com/Replit/status/1385628186193448963), [RoamResearch⁠](https://roamresearch.com/), to Facebook and others.

In fact, one of the reasons for creating Excalidraw+ is to ensure that the Excalidraw ecosystem not only survives, but thrives going forward. We will be able to invest more time and resources into it, which will benefit all, no matter what app you use Excalidraw to draw in.

As for the [excalidraw.com](https://excalidraw.com) website, it will of course continue to be free and actively developed.

We believe that this is going to be a win-win situation. People that want to use Excalidraw in a work environment will finally have a solution that works well for them and the open source project will be properly funded so that it can stand the test of time.

## We’re just beginning

Think big, start small. Excalidraw has a bright future ahead, and we extend an invitation for you to come and shape that future with us!

<center>
<a href="https://plus.excalidraw.com/?utm_source=excalidraw&utm_medium=blog&utm_campaign=launch">Join Excalidraw+</a>
</center>

<a style="margin: 0 auto" href="https://plus.excalidraw.com/?utm_source=excalidraw&utm_medium=blog&utm_campaign=launch"><img src="./excalidraw-plus.png"></a>


================================================
FILE: content/blog/one-year-of-excalidraw/index.md
================================================
---
title: One Year of Excalidraw
date: 2021-01-01
author: Excalidraw Team
link: https://github.com/orgs/excalidraw/people
image: og.png
---

> It's been kind of a different year, but it was the first year and pretty amazing for [Excalidraw](https://excalidraw.com).

<!-- end -->

Excalidraw started as a way to procrastinate on January 1st, 2020, and ended up being a fully fledged whiteboard product only one year later! In this post, we'll go over the most important features that made Excalidraw great at being a virtual whiteboard for sketching hand-drawn like diagrams.

We are so incredibly proud to have built something that is being used by **20k weekly active people**.

https://excalidraw.com/#json=6443031091740672,amxJZJxlZAlUBLADWIukFg

## Tech Stack

Excalidraw is fully open source, but it also stands on the shoulders of many other projects. We couldn't do it without [Rough.js](https://github.com/rough-stuff/rough), the library that gives Excalidraw its unique look; [Virgil](https://virgil.excalidraw.com), the hand-written font designed by [Ellinor Rapp](https://www.myfonts.com/newsletters/cc/200712.html); [TypeScript](https://github.com/microsoft/TypeScript), to tame the complexity of our codebase, giving a helping hand to new contributors (and old ones alike); and last but not least, [React](https://github.com/facebook/react). Our full list of [dependencies](https://github.com/excalidraw/excalidraw/network/dependencies) is listed on GitHub.

In addition, Excalidraw is depending on many awesome services such as [Vercel](https://vercel.com/) for hosting and pull request previews, [Crowdin](https://crowdin.com/project/excalidraw) for managing dozens of translations, [CodeSandbox](https://codesandbox.io/) for easy hacking on the project, [Sentry](https://sentry.io/) for error reporting, and [Dependabot](https://github.blog/2020-06-01-keep-all-your-packages-up-to-date-with-dependabot/) to keep our dependencies up to date.

Finally, this success wouldn't be possible without you, the over [100 contributors](https://github.com/excalidraw/excalidraw/graphs/contributors) who helped us ship new features, improvements, and fixes right to production.

https://excalidraw.com/#json=6671570797854720,bqkIFBlioHfMMLYHtcdGjA

## Some of our coolest features

### 🤝 Collaboration

When the lockdown started, companies all around the world struggled to adapt to remote work. [idlewinn](https://github.com/idlewinn) and [petehunt](https://github.com/petehunt) [implemented live collaboration](https://github.com/excalidraw/excalidraw/pull/879) that could be used for interviews, brainstorming, presentations, and more. If you're curious, we [explained how it works in a blog post](/building-excalidraw-p2p-collaboration-feature/).

https://twitter.com/Vjeux/status/1238907727906127872

### 🔒 Your data is encrypted

Many of Excalidraw use cases involve drawing sensitive data. As such, we architected our system so that our servers never see the content of your drawings, using end-to-end encryption. The [backend](https://github.com/excalidraw/excalidraw-json) support for storing data was implemented by [lipis](https://github.com/lipis), while the [client-side encryption itself](https://github.com/excalidraw/excalidraw/pull/642) was added by [vjeux](https://github.com/vjeux). Read how we are doing it in our article on [end-to-end encryption](/end-to-end-encryption/).

https://excalidraw.com/#json=5645858175451136,8w-G0ZXiOfRYAn7VWpANxw

### 🇺🇳 Translations

It was important for us early on to make sure that Excalidraw was translated into many languages so that it could be used all over the world. The [initial implementation](https://github.com/excalidraw/excalidraw/pull/638) was done by [fernandoalava](https://github.com/fernandoalava), while the automatic integration with our [Crowdin project](https://crowdin.com/project/excalidraw) was set up by [lipis](https://github.com/lipis). Support for right-to-left languages was [implemented](https://github.com/excalidraw/excalidraw/pull/1154) soon after by [j-f1](https://github.com/j-f1). To top it off, [Ellinor Rapp](https://www.myfonts.com/newsletters/cc/200712.html) designed new font glyphs for several non-latin languages. You can read more [about how we manage translations on the blog](/enabling-translations/)!

### 📱 Mobile first

Touch support and mobile-optimized layout [was](https://github.com/excalidraw/excalidraw/pull/787) [first](https://github.com/excalidraw/excalidraw/pull/788) [added](https://github.com/excalidraw/excalidraw/pull/790) by [j-f1](https://github.com/j-f1). This includes the creation of a toolbar for mobile devices that displays relevant controls while still leaving most of the screen free for the canvas.

### 📚 Library

The library was [first implemented](https://github.com/excalidraw/excalidraw/pull/1787) by [petehunt](https://github.com/petehunt). After adding support for exporting/importing the library, we eventually introduced a public directory where you can share yours. Visit [libraries.excalidraw.com](https://libraries.excalidraw.com) for more.

https://twitter.com/dbs_sticky/status/1340349749086580736

### 📊 Excalicharts

While we aim to keep Excalidraw simple to use, sometimes we hide little easter eggs that you need to find out for yourself (or find some hints by following our [Twitter account](https://twitter.com/excalidraw)). For example, you can copy any two-column dataset from a spreadsheet, or comma separated values (CSV) from a text file, and paste them into Excalidraw to quickly produce a chart. The [first implementation](https://github.com/excalidraw/excalidraw/pull/1723) was done by [petehunt](https://github.com/petehunt) and several [improvements](https://github.com/excalidraw/excalidraw/pull/2495) were made by [lipis](https://github.com/lipis).

### 🏹 Lines and Arrows

Possibly the single most complex feature in Excalidraw, lines/arrows have come a long way since the beginning. Initially, we've only had two-point lines. Multi-point support was added by [gasimgasimzada](https://github.com/GasimGasimzada), with improvements by [dai-shi](https://github.com/dai-shi), line editing by [dwelle](https://github.com/dwelle), arrowheads by [steveruizok](https://github.com/steveruizok), and by popular demand the arrow binding by [xixixao](https://github.com/xixixao).

https://twitter.com/excalidraw/status/1292403762427039744

Related, the [free hand drawing](https://github.com/excalidraw/excalidraw/pull/1570), one of the most [requested](https://github.com/excalidraw/excalidraw/issues/25) features, was implemented by [kbariotis](https://github.com/kbariotis).

https://twitter.com/excalidraw/status/1260287781596794880

### 🔄 More powerful editing

We must give a shout-out to [dai-shi](https://github.com/dai-shi) for continuous implementations of seemingly simple, but in fact pretty hard problems such as rotation and resizing, especially in combination of editing multiple elements at once.

https://twitter.com/dai_shi/status/1245273872053579776

### #️⃣ Grid and Stats

From early on, people were asking for more precision in their hand drawn diagrams. We complied by adding the [grid support](https://github.com/excalidraw/excalidraw/pull/1788) implemented by [dai-shi](https://github.com/dai-shi), and—inspired by _YouTube's stats for nerds_—our own version of [stats](https://github.com/excalidraw/excalidraw/pull/2453) implemented by [lipis](https://github.com/lipis). Both features could be found under the context menu by right-clicking on the canvas.

### 💾 File system integration and file handling

In Excalidraw, we use the [browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access) library to integrate with the file system of the operating system. This allows us to support a true open→edit→save workflow with proper over-saving and save-as on supported browsers, with a fallback to file uploads and downloads on other browsers. Read more about this feature in [tomayac](https://github.com/tomayac)'s [earlier article](/browser-fs-access/) on this blog. We also have [experimental support](https://web.dev/file-handling/#demo) for file type association, so that when you double-click an `.excalidraw` file in your file explorer, the Excalidraw PWA opens.

### ⚙️ Gatsby plugin

We also have a plugin for Gatsby that automatically converts links to saved Excalidraw drawings to inline SVG at build time. We are actually using it in this post for our charts. Implemented by [trevorblades](https://github.com/trevorblades) and [j-f1](https://github.com/j-f1). You can find it under [@excalidraw/gatsby-embedder-excalidraw](https://github.com/excalidraw/gatsby-embedder-excalidraw).

https://twitter.com/Vjeux/status/1257906664239333376

### 🌒 Dark Mode

To help protect your eyes, [@xixixao](https://twitter.com/xixixao) added dark mode, effectively turning Excalidraw into an actual blackboard.

https://twitter.com/Msieur_Jo/status/1245288337897914373

### 📦 npm package

One of the last things we've introduced this year was a completely new npm package, available at [`@excalidraw/excalidraw`](https://www.npmjs.com/package/@excalidraw/excalidraw). A long time in the making (thanks to [@aakansha1216](https://twitter.com/aakansha1216) for most of the work), this package allows you to easily embed Excalidraw as a React component into your apps.

## Excalidraw in the news

- Featured a few times on Hacker News:
  - https://news.ycombinator.com/item?id=23525648
  - https://news.ycombinator.com/item?id=22663435
  - https://news.ycombinator.com/item?id=25608336
- As an example on **web.dev** article: https://web.dev/browser-fs-access/
- A few blog posts:
  - https://pakstech.com/blog/draw-diagrams/
  - https://dev.to/ndsn/why-excalidraw-is-mightier-than-the-pen-and-the-sword-329f
- On Product Hunt: https://www.producthunt.com/posts/excalidraw
- React Europe talk: https://www.youtube.com/watch?v=fix2-SynPGE
- Used to illustrate an O'Reilly Book: https://www.amazon.com/dp/1492057096

  https://twitter.com/wietsevenema/status/1253752608671621124

- Another book: https://twitter.com/dchest/status/1264237749642637312
- Few companies also integrated Excalidraw in the product
  - [HackerRank](https://blog.hackerrank.com/virtual-whiteboarding-for-system-design-interviews/)
  - [Lobelia Earth](https://twitter.com/lobeliaearth/status/1275073557484244992)

## Get involved

Excalidraw wouldn't have become what it is today without all the wonderful contributions. If you haven't already, you can start today! And remember, it's not just code that makes Excalidraw better. Every bit helps, be it bug reports, translations, suggestions for improvements, or just hanging out on our [Discord chat](https://discord.com/invite/UexuTaE). Don't forget to follow us on Twitter [@excalidraw](https://twitter.com/excalidraw) for all the latest news and announcements.

## What's next

We will continue working hard on improving the performance, adding features where it makes sense, fixing bugs, working with [designers](https://github.com/excalidraw/excalidraw/issues/2506) to make Excalidraw look better, and more. But whatever we do, we'll try our best to ensure we don't lose the simplicity and charm that makes Excalidraw the product you love. 💕

## Some cool drawings

The best part of this project is to see a constant stream of awesome public drawings that people are making with Excalidraw. Here are some of our favorites from 2020.

https://twitter.com/LeaVerou/status/1306001020636540934

https://twitter.com/_cloudmu/status/1318288738422824962

https://twitter.com/elijahmanor/status/1287734485987950592

https://twitter.com/_JessicaSachs/status/1337555805609013248

https://twitter.com/aqandrew/status/1289275670871252995

https://twitter.com/Clainchoupi/status/1321200109707808769

https://twitter.com/addyosmani/status/1241801981955420160

https://twitter.com/levsthings/status/1224104529324412929

https://twitter.com/_RobDominguez/status/1222174661041180673

https://twitter.com/duanebester/status/1220561761964675072

https://twitter.com/abdellah_js/status/1225755552065769472

https://twitter.com/ilyamkin/status/1226609908327514113

https://twitter.com/dai_shi/status/1240494226531479552

https://twitter.com/bartekci/status/1246270772043296768

https://twitter.com/masbagal/status/1247763747755589633

https://twitter.com/Pinnassog/status/1247893044231168001

https://twitter.com/CandideTech/status/1250454449933426688

https://twitter.com/veenusav/status/1251101998184726533

https://twitter.com/jeudesprits/status/1264901836970098689

https://twitter.com/gitpitch/status/1265627223610056707

https://twitter.com/caroso1222/status/1278397651592122371

https://twitter.com/pomber/status/1281339741682753542

https://twitter.com/Vjeux/status/1282909088733511680

https://twitter.com/anas_aito/status/1283487054018600960

https://twitter.com/xnimorz/status/1300065301552390146

https://twitter.com/patak_js/status/1317097465158553600

https://twitter.com/wietsevenema/status/1343593994895417344


================================================
FILE: content/blog/open-colors/index.md
================================================
---
title: Open Colors
date: 2020-11-10
note: Part of a series where we pick apart various libraries and projects we use on Excalidraw.
author: Lipis
link: https://twitter.com/lipis
image: og.png
---

One of the qualities of Excalidraw is its simplicity. Even though we have the option to use any color of the [spectrum](https://www.google.com/search?q=%23c0ffee&hl=en), we have decided to limit our palette to a curated set of 15 colors in three different shades.

<!-- end -->

The [Open Colors](https://yeun.github.io/open-color/) color scheme has a total of 13 colors with 10 different brightnesses (0-9). For the canvas background we chose the lightest of the values (0), for the strokes the darkest one (9), and for the element background fill we went with the 6th (or the 7th, depending on how you count them).

![Color pickers](color-pickers.png)

I remember it was my very first [pull request](https://github.com/excalidraw/excalidraw/pull/378) to Excalidraw to add the **Open Colors** color scheme. I've known about this color scheme for a while now, but never found a great project to actually use it on. Excalidraw was a perfect fit.

They say a picture is worth a thousand words. (Click on the image below to open it in Excalidraw.)

https://excalidraw.com/#json=5120999011909632,Y57VloPaA1LSKT4-1NTgNA

Eventually we decided to install their [npm package](https://www.npmjs.com/package/open-color) so we don't have to hand-code every value. We use it both in [CSS](https://github.com/excalidraw/excalidraw/blob/master/src/css/_variables.scss), and [TypeScript](https://github.com/excalidraw/excalidraw/blob/master/src/colors.ts).

---

**P.S.** Thanks to [@heeyeun](https://twitter.com/_heeyeun) and the contributors for creating and maintaining this awesome project. Open Colors come with MIT License, which means you can use it anywhere you like without restrictions.


================================================
FILE: content/blog/redesigning-editor-api/index.md
================================================
---
title: Rethinking the Component API
date: 2023-01-13
author: Excalidraw Team
link: https://github.com/orgs/excalidraw/people
# image: og3.jpg
---

<!-- end -->

Since we've shipped the editor redesign late last year, one burning question many of you devs had was when is it going to be released to the Excalidraw package, and why has it not been released in the first place?

https://twitter.com/excalidraw/status/1587483527804854277

The reason we couldn't ship it on day one alongside the excalidraw.com release was customization. In the public app we've hardcoded some things, such as the `main menu` and the `welcome screen` — things you likely would not want hardcoded in your own apps — and we weren't quite sure how to design the API to make this easily customizable.

![Custom UI on excalidraw.com](./excalidraw-custom-ui.png)

Today, we're releasing our initial take of what the new API will look like, allowing you to customize the major parts of what was blocking the new release. If your feedback is positive, we will continue exposing more API in similar vein so you can tailor the editor experience to your and your user's needs.

Let's take a closer look at the changes, and how or why we've implemented them that way.

---

The two new major things we've introduced as part of the redesign is the top-left dropdown that serves as the main menu, and the welcome screen, including hints for new users to help them get the gist of the UI.

![Main components to customize](./main-components.png)

It was clear from that get go that you will want to customize these to your needs, and more importantly, remove the parts that would be non-functional in your own apps.

Up to this point we've been using a combination of config objects (e.g. `props.UIOptions`), and render props (e.g. `props.renderFooter`). While these are fine and you can get most things done this way, we've set out in search of API that would be more flexible and composable, and also a bit more intuitive to use.

# Getting rid of render props

We want you to be able not only to render custom components (e.g. custom footer), but also modify the default ones. Previously we were exposing extension points through render props. Render props work fine, but were envisioning an API where you could just render everything as children of the Excalidraw component, something like this:

```jsx
import { Excalidraw, MainMenu, Footer } from '@excalidraw/excalidraw';
import { MyCustomButton } from './MyCustomButton';

export const App = () => (
  <Excalidraw>
    <MainMenu>
      {/* menu items */}
    </MainMenu>
    <Footer>
      <MyCustomButton>
    </Footer>
  </Excalidraw>
)
```

In the future, we may even expose plugins as components, so you will end up doing this:

```jsx
import { Excalidraw, MinimapPlugin } from "@excalidraw/excalidraw";

export const App = () => (
  <Excalidraw>
    <MainMenu>{/* menu items */}</MainMenu>
    <MinimapPlugin />
  </Excalidraw>
);
```

At the end of the day, it's more of an aesthetic decision rather than functional one, as we could achieve the same with render props as well. One benefit is that the API surface area is smaller. We won't export a component, and then also have a render prop for it — you just render it.

Another goal is to start decoupling the UI from the editor. We want the core to be usable without React, and as such, delineating the UI more clearly and separating it from editor configuration and logic made sense.

But, using `children` is not without tradeoffs, so let's go over some of the new API changes, explain how it works under the hood, and what are the implications for your apps.

# `<Footer/>`

Here's what the footer looks like in the editor:

![footer area](./footer-area.png)

We have the default footer UI, which you currently cannot change. And there's an area in the middle you can render into. Previously you'd do that using a `renderFooter` prop. Now, you'll import a `Footer` component from our package, and render it as a child of the `Excalidraw` component, alongside any UI you want.

```jsx
import { Excalidraw, Footer } from "@excalidraw/excalidraw";

const App = () => (
  <Excalidraw>
    <Footer>
      <button onClick={() => console.log("Clicked!")}>Click me</button>
    </Footer>
  </Excalidraw>
);
```

So how does this work underneath? How do we know what is a `Footer` child component, and what is an unrelated component when it comes down to rendering it to an appropriate place in the UI?

For this, we've decided to reach for an older React API, now considered [legacy](https://beta.reactjs.org/reference/react/Children): `React.Children`. But it does its job well, so something like that will not stop us from using it :).

![React.Children](./react-children.png)

In short, we loop through the children you pass to Excalidraw and filter the components we are looking for using their `displayName`. Here's what the simplified code looks like:

```tsx
export const getReactChildren = <
  ExpectedChildren extends {
    [k in string]?: React.ReactNode;
  }
>(
  children: React.ReactNode,
) => {
  return React.Children.toArray(children).reduce(
    (acc: Partial<ExpectedChildren>, child) => {
      if (React.isValidElement(child)) {
        acc[child.type.displayName] = child;
      }
      return acc;
    },
    {},
  );
};
```

In practice we also validate against expected children names, and render the rest as is.

This is great as it allows you to render all the UI components as children, irrespective of the order, and we can pick and choose where to render what.

But it also has some downsides.

For one, you have to render the components as top-level children of the `Excalidraw` component. This is not a big deal, but it does mean that you can't render the `Footer` component as a child of another component, or we wouldn't be able to find it:

```jsx
const MyFooter = () => {
  return <Footer />;
};

const App = () => (
  <Excalidraw>
    {/* nope :( */}
    <MyFooter />
  </Excalidraw>
);
```

Let's move on to the next component.

# `<MainMenu/>`

The top-left dropdown menu was introduced in the new editor design, and we wanted you to be able to customize it to your needs.

Below is what the menu looks like on excalidraw.com (left), vs what we render by default in the package (right).

![main menu differences](./main-menu-differences.png)

But, we've opted for maximum flexibility. If the default items do not suit you, you can render the `MainMenu` component yourself, and we'll let you build it up from scratch — using the default menu item components, or your own.

```jsx
import { Excalidraw, MainMenu } from "@excalidraw/excalidraw";

const App = () => (
  return (
    <Excalidraw>
      <MainMenu>
        <MainMenu.DefaultItems.LoadScene />
        <MainMenu.DefaultItems.Export />
        <MainMenu.DefaultItems.SaveAsImage />
        <MainMenu.Separator />
        <MainMenu.Item onSelect={() => alert("Hello to you too!")}>
          Hello!
        </MainMenu.Item>
      </MainMenu>
    </Excalidraw>
  );
);
```

As with `Footer`, you'll need to make sure it's the top-level child of the `Excalidraw` component.

# `<WelcomeScreen/>`

Another thing we've introduced in the redesign is the welcome screen. This one is a slightly more complicated beast, as it is composed of several separate elements, each rendered in different parts of the UI.

The two main components is the center part containing the logo and quick actions, and the hints pointing out what users can find in the UI.

![welcome screen](./welcome-screen-overview.png)

You can again customize most of everything. If you want to render just the center, be our guest! If you want to change the hints a bit, you can do so as well.

One caveat is that we require not just the `<WelcomeScreen>` to be a top-level child, but also the `<WelcomeScreen.Center/>` and `<WelcomeScreen.Hints/>` to be the direct children of the `<WelcomeScreen>`.

```jsx
import { Excalidraw, WelcomeScreen } from "@excalidraw/excalidraw";

const App = () => (
  return (
    <Excalidraw>
      <WelcomeScreen>
        <WelcomeScreen.Hints.ToolbarHint />
        <WelcomeScreen.Center>
          <WelcomeScreen.Center.Logo />
          <WelcomeScreen.Center.Heading>
            You can draw anything you want!
          </WelcomeScreen.Center.Heading>
          <WelcomeScreen.Center.Menu>
            <WelcomeScreen.Center.MenuItemHelp />
            <WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
              onSelect={() => setCollabDialogShown(true)}
            />
            {!isExcalidrawPlusSignedUser && (
              <WelcomeScreen.Center.MenuItem
                onSelect={() => console.log("doing something!")}
              >
                Do something
              </WelcomeScreen.Center.MenuItem>
            )}
          </WelcomeScreen.Center.Menu>
        </WelcomeScreen.Center>
      </WelcomeScreen>
    </Excalidraw>
  );
);
```

# Wrapping up

So that's it. Let us know how you like the new API, and what you think about the direction we're taking it in. While there are still some rough edges about the way we do it now, and there are some implications we will cover later, we're excited to see what you'll be able to build with it. We'll be closely listening to your feedback, so let us know in the [Discord](http://discord.gg/UexuTaE) or on [GitHub](https://github.com/excalidraw/excalidraw/discussions)!

For now, you can read more on the newly introduced API in our [readme docs](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#component-api). But, we are working on much improved docs as we speak which we'll be releasing soon, alongside more detailed examples on the above API and how to handle specific cases. Stay tuned! 💜

In the meantime, you can also let us know what you'd like us to cover!

https://twitter.com/excalidraw/status/1613207731799834625


================================================
FILE: content/blog/reflections-on-excalidraw/index.md
================================================
---
title: Reflections on Excalidraw
date: 2020-01-15
note: 'This post appeared first on <a href="https://blog.vjeux.com/2020/uncategorized/reflections-on-excalidraw.html">Vjeux’s blog</a>.'
author: vjeux
link: https://twitter.com/vjeux
image: s-curve.png
---

On January 1st I started building a little tool that lets you create diagrams that look like they are hand-written. That whole project exploded and in two weeks it got 12k unique active users, 1.5k stars on GitHub and 26 contributors on GitHub (who produced real code, we don't have any docs). If you want to play with it, go to [excalidraw.com](https://excalidraw.com/).

<!-- end -->

Many people have asked me how I got so many people to contribute in such a short amount of time for Excalidraw, while this is still fresh in my mind, let me post about what I was thinking about during the process.

## S Curve

Before we get started with the actual content, here's an interesting concept that was in my mind thorough the project. I discovered the concept of a [S curve through Kent Beck's video series](https://www.facebook.com/KentBeckProgrammer/videos/947793608667282/). There are three rough phases:

- the first phase is when you do R&D and develop the product, there's a lot of work done but no real visible impact
- the second phase is the exponential part where everything is growing tremendously
- the third phase is when the growth flattens and you're doing smaller improvements (which can still be huge if the baseline is huge)

The S curve is usually used to describe bigger projects but it turns out Excalidraw just went through a S curve as seen in this chart that plots the number of stars over the past two weeks.

![S Curve](s-curve.png)

The most important part for me was to capitalize on the growth phase so that the project doesn't die when it hits the stabilization phase.

## Proven Value Proposition

Excalidraw didn't come out of nowhere, I've been using a tool called [Zwibbler](https://zwibbler.com/demo/) for probably 10 years in order to build hand-drawn like diagrams to illustrate my blog posts. I've always had this feeling that this tool was underrated. I seemingly was the only one to use it even though it felt like it could be used much more broadly.

![Example of image drawn with Zwibbler](zwibbler.png)

So when excalidraw came out, there was a clear value proposition and I knew it was going to be somewhat successful. Those days I don't have that much free time so I tend to spend my time on things that I believe have a high likelihood of being successful, especially side projects.

## Make Some Noise

The first thing was to get people excited! I'm fortunate to have a sizable audience on Twitter so I used it by posting a bunch of videos of the progress of building the first version of the tool.

https://twitter.com/Vjeux/status/1212503324982792193

## Convert Attention to Action

I got more attention than I anticipated so I felt like I could convert it into actual action. For this, the best way I've found is to create a bunch of issues about all the things that need to be done. I've been thinking about rebuilding a Zwibbler equivalent for a long time so I had a pretty good sense of what needed to be done.

People that wanted to contribute could just skim through the list of things to be done and start hacking. That worked really well!

![GitHub issues](issues.jpeg)

## Who is Contributing?

When I open sourced React Native, I was convinced that the same people that contributed to React would contribute to React Native. It turns out I was plain wrong, a new set of people started contributing. This same pattern applied to all the subsequent projects I've worked on since then.

This is a very broad generalization but most people that tend to contribute significantly to early projects like this are unknown (if they were well known, they'd likely have better opportunities to spend their time) but experienced (they are able to jump in on a random codebase and contribute).

## Keeping People Engaged

The name of the game is to get as much from people that are interested in contributing as possible. Your initial buzz is only going to last so long (a few days), so you want to capitalize on that time. Everyone (myself included) is likely going to have to go back to their real job soon.

For this, I usually try to be very responsive on the pull requests coming in. If you can get turnaround in less than 10 minutes, then you can have real-time work and people will stay engaged as long as you are.

I've tried something new this time and gave commit access to everyone that got a PR merged in. In the past I would do it after I've seen sustained work. This worked really well where this gave an extra motivation for people to contribute and they also started to review each other's code which was awesome! I am not worried about people abusing their power, people that spend energy getting something of quality in tend to be considerate.

A trick I've been also using is to merge pull requests even if they're not exactly the way I want and then push all the follow ups I had in mind. This way the person can have their feature shipped and likely to come back without having expensive back and forth (we never know when / if they're going to apply suggestions).

## Be Decisive

People are going to try and stir the project in all sorts of directions with their ideas and pull requests. It's pretty tricky to think in advance what kind of suggestions you're going to get because people tend to get very creative (in both good and bad ways...).

If you want something to happen, you need to give a very clear "yes" with concrete things that need to be done. If you're not sure or change your mind multiple times or answer days/weeks later, people are either not going to invest their time making it happen, or will lose interest and not push it to conclusion.

On the flip side, you're likely going to see a lot of pull requests or suggestions that you don't think are a good idea. I've found that it's usually not a good idea to give a clear "no" as it's a hard message to give to a stranger over text. Instead, what I found tends to work better is to space out replies and ask for more information. The other party will naturally lose interest and move on. You should use this technique very sparingly as it is not a nice approach.

## Keeper of Quality

With so many simultaneous contributions, the product can easily start losing quality. I view myself as the keeper of quality. I've been pretty obsessed about all the small details and things that feel off.

Every time I see a problem, I open an issue with a small repro case. In many cases, those issues are easy to fix and someone will get to it. I also make sure to clear the backlog so that we're always in a good enough shape.

![GitHub Issue](issue.png)

I've also made sure that some core values were being maintained. I want minimal friction to get started drawing. In particular, this means that what you see first should be the shapes. I had to actively prevent people from adding title selection and login to keep this property.

## Celebrate Success

Posting about all the good things that happen, be it a new cool feature, or interesting usage or thoughts in the topic will increase the size of that channel as those posts will attracts an audience.

The other interesting thing that will happen is that you will provide an audience to a lot of the people that are contributing. As I mentioned earlier, they're unlikely going to have a big one of their own that cares about this topic.

This is a win-win situation! It takes time to actually post all those things but I've seen it being valuable time and time again.

## Empty Canvas

What I found fascinating with this project is that many people were able to project their dreams and ideas onto it. I've been told that I should quit my job by at least three people and build a startup around this project as they saw a lot of growth potential in different areas. (Sorry, I'm not, but if you want to, the business is up for grab!)

I'm not exactly sure what to make of that but it led to great conversations! That's more than I hoped for with this project.

## Things That Went My Way

I wish anyone could read this and reproduce it but that's not completely true. I had a lot of things that went my way. I found it to be useful to know what advantages people behind success stories have to see how they affect their abilities to deliver.

- I have more than 10 years of experience building front-end and it turns out that I learned very little on the technical front during this project. I've done all the pieces many times one way or another. So when it was time to architect the project, split up the work, review code or suggestions, do the work, manage contributors, evangelize... All of this was pretty much mechanical and didn't require much thinking. This helped speed up everything so that a lot more than usual would fit within one buzz cycle.
- I have a large audience on Twitter and I've worked closely in the past with other people with large audiences (hi Dan Abramov and Jordan Walke!) who were willing to evangelize the project. Without that, I wouldn't have been able to get the project in front of so many people so quickly.
- Excalidraw was built with other projects such as CodeSandbox, Zeit, Rough. They've been fantastic to use and were part of the reason why the project got off the ground so quickly. I encountered some small issues with those dependencies, which likely would have ended up somewhere on an issue tracker and eventually got fixed. But because I personally knew the owners of the first two projects and was visible enough for the third, I was able to get those issues resolved extremely quickly, which is not everyone's experience.

## Conclusion

This was a fun project to work on while procrastinating on writing performance reviews. I'm not exactly sure what the future holds for Excalidraw but I'm happy that it is now at a point where I can finally use it to illustrate the blog post I wanted to write that started this whole project (hello rabbit hole!).

Now, go draw some things with [excalidraw.com](https://excalidraw.com) and if you see something you'd like improved, please contribute on GitHub! [github.com/excalidraw/excalidraw](https://github.com/excalidraw/excalidraw)


================================================
FILE: content/blog/rethinking-virtual-whiteboard/index.md
================================================
---
title: Rethinking Virtual Whiteboard
date: 2020-03-28
author: vjeux
link: https://twitter.com/vjeux
---

[Excalidraw](https://www.excalidraw.com/) started as a virtual tool to draw diagrams but a lot of people started using it to replace physical whiteboards. In this post we'll walk through many aspects of physical whiteboards that do not make sense to translate as is in the virtual world.

<!-- end -->

## Predefined Shapes

Hand-free drawing with a mouse or trackpad is hard! For example, here is me trying to write the word "whiteboard". It took me 25 seconds and the result looks okay at best.

![Writing "whiteboard" on a physical whiteboard](whiteboard-physical.gif)

Writing text is such a common operation that keyboards have been developed for this use case. With Excalidraw I can just type the letters and have it written in less than 3 seconds.

![Writing "whiteboard" on Excalidraw](whiteboard-excalidraw.gif)

Fortunately, most whiteboard applications let you write text using the keyboard, but for some reason they don't apply the same concept with common shapes you're drawing when using a whiteboard.

![Drawing shapes on a physical whiteboard](shapes-physical.gif)

Speed wasn't that much an issue here but look at this circle... And it's not for lack of trying, I took a few takes and this is the best circle I managed to draw.

Because those shapes are so commonly drawn, Excalidraw provides them by default. Here's the same diagram drawn with Excalidraw:

![Drawing shapes on Excalidraw](shapes-excalidraw.gif)

## Modifications

I'm not happy with the way that previous circle looked like and want to redo it. In a physical whiteboard I first need to erase the current one. This is again an operation that's very painful (Aaarrrghhh!!) when directly ported from the physical world.

![Erasing on a physical whiteboard](erase-physical.gif)

Excalidraw lets you select the shapes you've previously drawn and use backspace key to get rid of them.

![Erasing on Excalidraw](erase-excalidraw.gif)

Once you move to the virtual world, you can do operations like resizing, moving, copy pasting shapes. Those operations are not possible in the real world.

![Operations on Excalidraw](operations-excalidraw.gif)

## Infinite Canvas

Another constraint of the physical world is that the whiteboard has a fixed size. It is shaped by a lot of physical factors like the size of the room / wall, at a height so that it's comfortable for a standing human to draw...

Have you ever been in a situation where you end up at the bottom of the whiteboard but your diagram is not complete and you end up drawing the rest somewhere else completely unrelated?

![Finite physical whiteboard](finite-physical.gif)

If we remove this physical limitation, I can just scroll a bit and continue the drawing.

![Infinite virtual whiteboard on Excalidraw](infinite-excalidraw.gif)

A nice benefit of having a virtual canvas is that if you want to draw something else, you can just scroll to a place that's empty and start drawing. You don't need to take an explicit step of saving the current file, creating a new empty file, figuring out what to name it...

## Conclusion

If you need a replacement for a physical whiteboard as you are working from home, [Excalidraw](https://www.excalidraw.com/) may be a good solution for you! Using the live collaboration feature and [end to end encryption](/end-to-end-encryption/), we've seen people successfully use Excalidraw for brainstorming sessions, architecture interviews or even drawing org charts to prepare for re-orgs...

If you are building or evaluating physical whiteboard replacements, think about what physical limitations do not make sense anymore in the virtual world and what are the various ways to adapt it.


================================================
FILE: content/blog/tell-your-story-with-charts/index.md
================================================
---
title: Tell your story with Charts
date: 2020-12-20
author: Lipis
link: https://twitter.com/lipis
image: og.png
---

One of the hidden features of Excalidraw, is that you can generate charts in seconds. Once you imported the chart, all the elements are yours to manipulate using Excalidraw features for you to tell the story you want! Read more to see how to make a chart.

<!-- end -->

## Telling a story

Charts primary reason to be created is to tell a story. I don't know about you, but I often spend a lot of time configuring the chart to be telling the story I want. I will make one or two elements stand out by changing their color, adding arrows to annotate some parts, adding white boxes around information that's extraneous... Unfortunately, this is often not well supported by charting libraries.

In my ideal workflow, I want to take the data and generate the chart, then treat all the elements of the chart as free form and modify them however I want. This is exactly how Excalidraw charting feature works! Once your chart is generated, you can use all the power of Excalidraw to style it, move things around, hide some details...

## Examples

![Copy pasting charts](charts.gif)

Copy any two columns data from Excel, Spreadsheet, or even HTML tables and paste it directly to Excalidraw. It will work when the number of columns is one or two. Here is the generated chart from the data below it.

https://excalidraw.com/#json=6035723371151360,_YC8ms6v1fhghy3SCLYljQ

| Month | Accounts |
| ----- | -------: |
| Jan   |      653 |
| Feb   |      751 |
| Mar   |      941 |
| Apr   |      116 |
| May   |      828 |
| Jun   |       85 |
| Jul   |      169 |
| Aug   |      666 |
| Sep   |      127 |
| Oct   |      484 |
| Nov   |      288 |
| Dec   |      687 |

But it doesn't stop there. You can also copy the data from a plain text file as comma separated values (CSV). Open your favorite editor, type the values separated by comma or tab, copy/paste and you are ready to go. Here is another example from the data bellow the chart.

https://excalidraw.com/#json=4659903914311680,mBoVCGfah7dPzNI90_8JcA

```
Day,Commits
Sun,143
Mon,167
Tue,92
Wed,114
Thu,128
Fri,155
Sat,193
```

## Modifying the chart

Once the chart is imported you can select it and change any of the properties to adjust it to your needs. For example, here is the story of Excalidraw traffic in 2020.

https://excalidraw.com/#json=6443031091740672,amxJZJxlZAlUBLADWIukFg

## Implementation

The implementation of this feature was done in two iterations and if you are interested on how it was done, check out the first [pull request](https://github.com/excalidraw/excalidraw/pull/1723) by [petehunt](https://github.com/petehunt) and the [second one](https://github.com/excalidraw/excalidraw/pull/2495) by [lipis](https://github.com/lipis). If you find any edge case, [submit an issue](https://github.com/excalidraw/excalidraw/issues) and we will make sure to address it!


================================================
FILE: content/blog/webex-meetings-integration/index.md
================================================
---
title: Introducing Webex Integration
date: 2021-10-28
author: Aakansha, David
link: https://twitter.com/aakansha1216, https://twitter.com/dluzar
image: excalidraw_webex.png
---

Collaborate using Excalidraw whiteboard directly in your Webex meetings.

<!-- end -->

## Excalidraw ❤️ Meetings

Listening to your stories it's amazing what you use Excalidraw for in the wild, from illustrating blog posts, to wireframing, teaching, presenting, to interviewing. One common denominator is collaboration, which has always been a core part of Excalidraw. Oftentimes, text and drawings aren't enough, and for this purpose, we're excited to announce Excalidraw integration for Webex Meetings.

## Excalidraw in Webex

Whether you're brainstoring with your team, conducting interviews, or host classroom sessions, Excalidraw for Webex gives you the ability to collaborate on drawings right where you make your call without the need to switch apps.

<video src="./webex-blog-promo.mp4" autoplay playsinline loop muted style="width: 100%; height: auto;"></video>

## How to use

For now, to use a specific embedded app such as Excalidraw in your Webex meeting, you will first need to enable it for for your organization (as an administrator) — once that's done, you will be able to use Excalidraw in your meetings going forward.

When you open the Excalidraw app for the first time, you will see a blank canvas. You can draw something to prepare beforehand, or you can start collaborating right away by clicking the "Open together" button at the bottom of the screen. See these steps illustrated on our [Webex homepage](https://webex.excalidraw.com/#how-to-install).

Your data is [end-to-end encrypted](/end-to-end-encryption) the same way as you are used to on [excalidraw.com](https://excalidraw.com), and the whole integration is open-source and [hosted on GitHub](https://github.com/excalidraw/excalidraw-webex).

Note that the collaboration room is active only for the duration of the meeting. Each meeting creates its own room.

## More to come

Some features you are used to from [excalidraw.com](https://excalidraw.com) such as exporting to [Excalidraw Plus](https://plus.excalidraw.com) or the recently added image support isn't yet available in Webex Meetings due to a few platform limitations that will be resolved over the coming weeks. For similar reasons, some features available on specific operating systems may not work on others (such as exporting to a file, which currently only works on Windows).

Also, collaborating between browsers and desktop clients doesn't work reliably yet as there's no official support for Emebedded Apps in browsers yet, so best stick to the desktop application.

As with Excalidraw itself, we will be improving the Webex integration as time goes on, but even now we are confident it can offer a great value when conducting your meetings. Have fun! ❤️

<div style="margin-bottom: 2em; text-align: center">
Visit <a href="https://webex.excalidraw.com/#how-to-install" target="_blank" rel="noopener noreferer">webex.excalidraw.com</a> for more.
</div>


================================================
FILE: content/blog/year-three/index.md
================================================
---
title: Three for three
date: 2023-01-04
author: Excalidraw Team
link: https://github.com/orgs/excalidraw/people
image: og3.jpg
---

How did Excalidraw fare in its third year of existence, and what's to come? Let's find out!

<!-- end -->

Greetings Excalidraw users! Thank you all for the support throughout the year, and for being a part of our growing user base! ❤️

![users in 2022](./users.jpg)

With your help, we've recently crossed **350K monthly users**. 🔥 On top of that, we’ve counted [**38K stars on GitHub**](https://github.com/excalidraw/excalidraw) just before the end of the year, and are constantly trending everywhere, such as on the [bestofjs.org](https://bestofjs.org) project repository — this helps the open-source part of Excalidraw get in front of more developers like you!

![bestofjs](./bestofjs2.png)

Let's have a look at what we and you have been up to the past year!

(Psst, if you don't get to the end of the post, and you're looking for work, [we're hiring](https://excalidraw.slite.com/app/docs/kqzVFHykBHg-we/Join-the-Excalidraw-team)! 🚀)

# What we’ve shipped in the editor

One of the biggest changes we've been proud to unveil last year was undoubtedly the editor redesign. While having scores of people contribute to the editor is awesome (much love!), if not careful, over the years you can end up with a design without a strong single thread going through!

This is where [Tony Bures](https://twitter.com/mrkvi) came in with a new design, and enlisting the help of [Barnabás Molnár](https://twitter.com/_barnabasmolnar) we were able to ship the new interface that still keeps out of your way, is even a bit more compact, a little more consistent, and even makes room for new features we'll be shipping this year!

There are some rough edges we need to fix up, such as missing hover/active states here and there, and some components still need porting (looking at you, context menu!). And not to forget, mobile design — while it's sporting the new drip, it will need a bit more work to truly shine and make you effective on your small devices!

https://twitter.com/excalidraw/status/1587483527804854277

### What may have you missed in 2022?

If you're using Excalidraw regularly, you shouldn't have missed many new features. But some can be a bit hidden, so let's take a look at some of the prominent changes this year. (We've included nuggets of information you may like 👀).

---

Freedrawing is fun, but sometimes it can get annoying if drawing gets slow, or unwanted gestures start happening, especially when using a stylus.

Palm rejection is hard in general, but doubly so on the web, so instead of doing that, we decided to sidestep it for now, and [Zsolt Viczián](https://twitter.com/zsviczian) helped us ship pen mode instead. It's enabled when we first detect a pen (stylus), and we start preventing most touch events until you leave it. You can still pan the canvas with two fingers, but we've learned that your hand triggered zooming too often so we've disabled touch-zooming in pen mode, too.

In the future we'll want to investigate better palm rejection so that if nothing else, we can re-enable touch-zooming.

https://twitter.com/zsviczian/status/1488879818305384449

Other common problem on tablets was performance. Some pens were firing pointer events much more frequently than needed, causing a bottleneck in rendering. So we've throttled them to not exceed the framerate of your screen (commonly 60Hz), which had the desired effect.

https://twitter.com/excalidraw/status/1491044664731856900

While performance improved, it may technically result in slightly less line resolution — for most use cases it's perfectly fine, but for handwriting we may want to investigate other venues (on that note, we would need to modify the freedraw algorithm, and there's also the fact that most devices don't emit high-resolution pointer events in the first place).

We've also heavily improved the Apple Pencil Scribble getting in the way, often dropping draw gestures altogether. Even so, sometimes drawing on iPads still triggers character recognition, which it's unclear whether we can prevent — if you're facing these problems, you might consider disabling Scribble in your settings.

Mobile and tablet experience is an ongoing effort and there are many more improvements to be had! But, you’re already making art in Excalidraw, so that makes us happy! ❤️

https://twitter.com/Biernacki/status/1584990065729888256

Do you know we support hyperlinks in the editor? Added by [Aakansha Doshi](https://twitter.com/aakansha1216), it allows you to add clickable links to elements to adding additional information and resources. Especially useful for linking diagrams together (e.g. on Excalidraw Plus 😋).

Support for in-diagram linking coming this year!

https://twitter.com/aakansha1216/status/1489256535817854977

One missing feature was to add background to freedraw shapes, which was correct by [Arun](https://twitter.com/node_monk)!

https://twitter.com/excalidraw/status/1491456843742605313

Until we do a proper redesign of the color picker, a useful feature we've added last year is to pick from the custom colors you've been already using on your canvas.

You'll be able to pick from more shades and better colors in the upcoming color picker. 🎨

https://twitter.com/aakansha1216/status/1498292311381655554

Now having a prominent position in the toolbar, previously you may have missed we've also added the Eraser tool, which allows you to remove multiple elements with way more ease, especially those tiny freedraw lines!

https://twitter.com/aakansha1216/status/1502296876405891074

[Tom Sherman](https://twitter.com/tomus_sherman) added element locking to the editor, allowing you to lock elements in place to prevent accidental modifications or deletions.

It's hidden under the context menu because the only way to unlock elements at the moment is by right-clicking and unlocking them from the context menu again. Thus, we haven't put the lock button elsewhere in the UI so you don't lock elements unintentionally as you explore what each button does, and not knowing how to unlock them.

https://twitter.com/excalidraw/status/1512104367213658122

Libraries are something we'll be spending way more time on this year, but a big change led by [Ishtiaq](https://twitter.com/ishtiaq103) last year was moving libraries to a sidebar, allowing you to keep the library open at all times:

https://twitter.com/excalidraw/status/1539269812253335553

And little known feature is the ability to add multiple libraries to your canvas at once:

https://twitter.com/excalidraw/status/1524423937542795265

You could add new points to existing arrow/lines in the line editor (<kbd>Ctrl/Cmd-Enter</kbd>) before, but it was hard to add points in the middle segments, so we've decided to work on that.

First, Aakansha added support for adding a midpoint to lines outside the line editor:

https://twitter.com/aakansha1216/status/1557748574712041473

And then also allowed adding midpoints to any segment from within the line editor. We've decided to not expose this outside the line editor for now because it tends to get in the way too often when you try to move lines around.

https://twitter.com/excalidraw/status/1570059398881636354

[Ryan](https://twitter.com/_ryan_di) and [David](https://twitter.com/dluzar) have improved the default radius sizing for rectangles, making rectangles rounding to look similar across different sizes. While in the future we may support customizing radius to a point, our goal still is to make diagramming easy with as little tinkering with the settings as possible.

https://twitter.com/excalidraw/status/1600883623980171265

Towards the end of the year, Aakansha added the long awaited ability to add text labels to arrows!

https://twitter.com/excalidraw/status/1599792132725669888

But, Excalidraw is also about the community helping out! Here, [Alex Kim](https://github.com/alex-kim-dev) fixed a vexing issue when resizing certain elements.

https://twitter.com/excalidraw/status/1559205594782990345

Want to help make Excalidraw better yourself? We'll soon be making it easier to make your first contribution, so stay tuned, but you can head over to our [GitHub](https://github.com/excalidraw/excalidraw) right now.

# What's happening on the Plus side?

If you don't know, last year we've unveiled [Excalidraw Plus](https://plus.excalidraw.com/), an app aimed at teams built on top of the Excalidraw editor, offering companies much needed authorization and team management, and everyone a secure environment to organize their drawings, while still keeping our focus on simplicity and low friction. Adoption is not slowing down, and the app development is not lagging behind!

We have big plans this year you will certainly appreciate, but let's give a shout-out to a few things that happened in Plus last year.

---

We're iterating on the design not just in the editor, but in the Plus app, too! Besides the new look, we've added a search panel you can invoke anywhere:

https://twitter.com/excalidraw/status/1529106689600921600

An important addition was the introduction of the Dashboard. Get around quickly and view who's drawing right now, and where.

https://twitter.com/excalidrawPlus/status/1540091685874634754

As the Plus app lives on a different subdomain, some of you were wishing for a more streamlined access. [Milos](https://twitter.com/milosvete) added support for automatically redirecting you to Plus when you're signed in, so you can access it directly from [excalidraw.com](https://excalidraw.com).

We're considering to make this the default (you would be always able to opt-out), because it can be confusing for some still. Let us know your thoughts on this one!

https://twitter.com/excalidrawPlus/status/1526973455475671040

On that note, we've also made it possible to choose what page will be loaded first. You can pick between the editor or the dashboard, and more. What other shortcuts should we add?

https://twitter.com/excalidraw/status/1559936348508884994

To reduce friction further, you can now create drawings even quicker. Use the "Start drawing" button in the Dashboard, or <kbd>Opt/Alt + A</kbd> everywhere else.

https://twitter.com/excalidraw/status/1571886450903220232

Next, we've added support for sorting your collections, and choosing the sort order of your drawings.

https://twitter.com/excalidraw/status/1508847388517089292

https://twitter.com/excalidrawPlus/status/1549790547606388736

To please the accountants, we've added yearly billing (it's cheaper, too 😉).

https://twitter.com/excalidraw/status/1545109954616508416

Commenting is easier in Plus now, too!

https://twitter.com/excalidrawPlus/status/1552670831402225664

# VS Code extension & GitHub

Another major thing happening in the Excalidraw ecosystem was the release of the [Excalidraw VS Code extension](https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor), maintained by [Achille Lacoin](https://twitter.com/pomdtrr?ref_src=twsrc%5Etfw). 💜

If neither the [excalidraw.com](https://excalidrwa.com) app, nor the [Plus app](https://plus.excalidraw.com) suits your needs, perhaps editing Excalidraw files from your VS Code workspace will. And you can use the extension on [vscode.dev](https://vscode.dev/) & [github.dev](https://github.dev/), too – try it out!

https://twitter.com/excalidraw/status/1511332566216884237

And you'll be able to use Excalidraw in the upcoming [GitHub Blocks](https://blocks.githubnext.com/):

https://twitter.com/excalidraw/status/1590828212615102464

# Community

## Discord

We restructured the Excalidraw Discord server to encourage engagement and interaction within our community!

If you have questions, feedback, or just want to chat, join us on [Discord](https://discord.gg/UexuTaE)! We have some surprises in the works for Discord next year! 😉

## In the wild

Excalidraw is being used far and wide.

From company docs...

https://twitter.com/willmcgugan/status/1584174595023609857

To illustrating whole books!

https://twitter.com/Pragmatic_Eng/status/1575167020139970562

Or making awesome illustrations in general:

https://twitter.com/jamesspurin/status/1605154429170229249

https://twitter.com/victor_bigfield/status/1608796818225127424

At conferences...

https://twitter.com/erikras/status/1522342340664303616

https://twitter.com/excalidraw/status/1573305116333338626

Look, here's our Aakansha! ❤️

https://twitter.com/aakansha1216/status/1555559465508040705

Those slides must have taken a lot of work! (Or maybe not, with Excalidraw 😋).

https://twitter.com/FUSAKLA/status/1538248868885893120

And not to forget [Chris's](https://twitter.com/Vjeux) talk at Next.js Conf!

https://twitter.com/Vjeux/status/1586062861662507008

Excalidraw is also getting big on Twitch and YouTube, from [@t3dotgg](https://www.youtube.com/@t3dotgg) to [@d0nutptr](https://www.twitch.tv/d0nutptr), [melkeydev](https://www.twitch.tv/melkeydev), [@devagr](https://www.youtube.com/@devagr), among [others](https://twitter.com/excalidraw/status/1596176397411971072)!

Speaking of YouTube, Aakansha recently explained how to build your own Excalidraw over at [Chirag's channel](https://www.youtube.com/watch?v=1lNJVDfsTSo).

https://twitter.com/aakansha1216/status/1562817277258371073

You rock hard at Excalidraw! 🍉

# Coming up

## Excalidraw

Last year we've added many great features, but we are not done! And we want to improve on existing ones, too, such as as better text editing support, arrow binding, and more. At the same time, making sure the editor retains its simplicity and low friction.

We'll also be focusing on performance so that even huge drawings can be edited smoothly.

And we're not forgetting [libraries](https://libraries.excalidraw.com). More on that soon!

## Package

Next year, we will taking the [Excalidraw package](https://www.npmjs.com/package/@excalidraw/excalidraw) to the next level, beginning with the upcoming npm release bringing the redesigned editor to your apps, and featuring an improved API to customize some parts of the editor UI. We'll use this as a test bed on whether this type of new API works well, and we'll be making changes to the rest of the editor to allow for much greater customization.

There are lots of places in need of much smoother experience, such as the images API, listening on changes, and programmatic API in general, so we'll be working on that, too.

## Plus

For [Plus](https://plus.excalidraw.com), we'll be making strides on three fronts. Features, friction, and management.

We've learned from your feedback, and are adding features that you will like. From better sharing and readonly links integration, to presenting from Excalidraw a breeze.

At the same time, we want to ensure the app is as frictionless as possible. Getting your ideas on the canvas is what Excalidraw is about.

And we want to make it easier for business to make the transition. From user provisioning and auth (think SAML and SCIM), to billing, and enterprise support.

# Join the Excalidraw team!

As alluded at the top, we are looking for talented devs such as you! If you're as pumped about building Excalidraw as we are, we have open positions at both the Excalidraw editor and Excalidraw Plus. Check the [details](https://excalidraw.slite.com/app/docs/kqzVFHykBHg-we/Join-the-Excalidraw-team) and drop us an [email](mailto:careers@excalidraw.com)! 💪

If you don't feel as confident but want to prove yourself, start contributing to [excalidraw](https://github.com/excalidraw/excalidraw). That's always a good way through which to join our ranks!

---

It's going to be a packed year for Excalidraw across the board!

Happy New Year everyone! 🎉

Excalidraw Team


================================================
FILE: content/blog/year-two/index.md
================================================
---
title: Year two of Excalidraw
date: 2022-01-03
author: Excalidraw Team
link: https://github.com/orgs/excalidraw/people
image: og.png
---

Excalidraw celebrated its second birthday! What happened during the last year and what's next?

<!-- end -->

It's been another great year for [Excalidraw](https://excalidraw.com), the virtual whiteboard for sketching hand-drawn like diagrams. Let's review what happened since [the last time](https://blog.excalidraw.com/one-year-of-excalidraw/).

The importance of remote working and collaboration has not been diminished, and more and more people are reaching for tools such as Excalidraw. **We've seen the usage go from 60K monthly active to 170K monthly active users!**

![Excalidraw monthly active users](graph-mau.png)

But it's not just the times. We are hearing far and wide how much you love Excalidraw. Thank you!

# Excalidraw+

Probably the most commonly requested feature from people using Excalidraw was to be able to manage all their drawings and teams.

[Excalidraw Plus](https://plus.excalidraw.com) was released in May 2021 to address this. But another motive was to make Excalidraw sustainable and increase its funding.

https://twitter.com/excalidraw/status/1389253752742350858

We're succeeding on both fronts. Excalidraw+ has been adopted by many companies, big and small, as well as you lot 😉, and we are now able to invest back into the editor. In October, [Aakansha](https://twitter.com/aakansha1216), a long-time Excalidraw maintainer, officially joined our ranks, and is currently devoting her time making the editor even better. ❤️

We will be releasing a roadmap for Excalidraw+ soon, but in the near future we will be finishing the much requested yearly billing, improve workflows, and reduce friction from the initial load of the app to switching between drawings. We will support sharing libraries between team members, and managing these libraries. And we will finally introduce versioning.

If you want to help build something great, don't hesitate to [reach us](mailto:careers@excalidraw.com), or simply start contributing to [Excalidraw](https://github.com/excalidraw/excalidraw).

# Excalidraw

As part of our review, let's celebrate and look back on some of the contributions of the past year.

### The editor

We're continuing to improve the core editor experience. While adding new features is part of this process, we're also working on enhancing existing features, fixing new bugs, and ensuring the UI stays simple to use.

Two of the most heavily requested features were added towards the end of the year.

[David](https://twitter.com/dluzar) introduced [image support](https://github.com/excalidraw/excalidraw/pull/4011), so you can embed images and svgs into your drawings.

https://twitter.com/excalidraw/status/1451280455647563783

Need to create sticky notes? [Aakansha](https://twitter.com/aakansha1216) added support for [binding text to a container](https://github.com/excalidraw/excalidraw/pull/4343). No more manual resizing, positioning, or adding line breaks so the text fits into your containers!

https://twitter.com/aakansha1216/status/1471509326674030592

The original freedraw tool was great, but as it was only an extension of the line tool, and it was often creating rough shapes with too many points, we thought it could use a boost. At the time, [Steve Ruiz](https://twitter.com/steveruizok) was finishing up his library [perfect-freedraw](https://github.com/steveruizok/perfect-freehand), so we asked him [to integrate](https://github.com/excalidraw/excalidraw/pull/3512) it to Excalidraw. 🙏

https://twitter.com/excalidraw/status/1391443782516740096

At the onset of the year, Aakansha has added [a view mode](https://github.com/excalidraw/excalidraw/pull/2840), useful in presentations or when you just want to view your drawings without editing them. It's complementing our existing zen mode, which we're continually improving as well.

https://twitter.com/excalidraw/status/1356350401944158211

[Riley Schnee](https://twitter.com/rileyschnee) [imlemented](https://github.com/excalidraw/excalidraw/pull/2520) for object flipping.

https://twitter.com/excalidraw/status/1375479950535458819

Creating new shapes should be easy and fun, but sometimes you need a bit more control. One example is the the line/arrow editor where you can modify or add new points (double-click on a line or select it and hit Enter). Recently, we've added support for [selecting multiple points at once](https://github.com/excalidraw/excalidraw/pull/4373), and more is yet to come.

https://twitter.com/dluzar/status/1470389942262054920

### Keeping things simple

This is an ongoing process, and while one of Excalidraw's principles is simplicity, there will always be ways in which can communicate features better, or improve the design.

We've added [toast messages](https://github.com/excalidraw/excalidraw/pull/2772) to notify of activity or interactions that aren't inherently visible, such as when using shortcuts to do things like exporting an image to your clipboard.

https://twitter.com/excalidraw/status/1350099747709841410

The export dialog [has been redesigned](https://github.com/excalidraw/excalidraw/pull/3613), but more work will be done to ensure you know what to click on, and which action does what.

Recently we've introduced a common brand color, made small [tweaks to the toolbar](https://github.com/excalidraw/excalidraw/pull/4387) as well as several button states to clarify which ones are selected. Accessibility is not the only objective, we also want the editor to look nice, and we will continue working on this going forward.

https://twitter.com/excalidraw/status/1471126947145072649

### Library improvements

While it's easy to make beautiful drawings in Excalidraw, often nothing beats a well crafted work that you can readily drop onto your canvas. Libraries are here to stay and we're working on making them even more awesome.

First, the library menu itself got better. [Arun](https://twitter.com/node_monk) made it easy to [clear your entire library](https://github.com/excalidraw/excalidraw/pull/2997), and we later introduced a way to select specific items so you can delete, or export them individually.

Installing was improved as well, as Aakansha did some digging and [found out](https://github.com/excalidraw/excalidraw/pull/3299) we can reuse the editor's browser tab when installing libraries.

And Arun [made sure](https://github.com/excalidraw/excalidraw-libraries/pull/106) the library page supports dark theme same as Excalidraw does.

https://twitter.com/excalidraw/status/1402374369670832131

Many more goodies are coming next year, but the above made installing already a pleasant experience.

Everyone loves using libraries, but many of you also love creating and sharing your own (thank you!). The not so fun part is publishing and the busywork around it. That's why we've been focusing on smoothing out this part of the experience.

Towards the end of year, Aakansha and David have [put a lot of work](https://github.com/excalidraw/excalidraw/pull/4115) into [streamlining the publishing experience](https://github.com/excalidraw/excalidraw-libraries-server) so that you don't have to manually create GitHub pull requests. We've also started requiring the authors to name individual library items — this will help everyone when searching for specific items they need, support for which will be added in early 2022.

https://twitter.com/aakansha1216/status/1461045987678453760

### Collaboration

Live-collaborating with people has been an important pillar right from the start and it remains so.

[Thomas Steiner](https://twitter.com/tomayac) introduced [idle detection](https://github.com/excalidraw/excalidraw/pull/2877) so you know which collaborators aren't active, or who's not looking at the canvas at all (their browser tab isn't focused).

https://twitter.com/excalidraw/status/1357285585937981441

We've upgraded the collaboration server for smoother experience, and fixed some annoying bugs like [layers syncing](https://github.com/excalidraw/excalidraw/pull/4076) issues.

More love will be shown to collaboration support this year!

### Excalidraw for Cisco Webex

We've started to integrate Excalidraw into other great platforms, and first one we've partnered with is Cisco Webex. This allows you to collaborate with people on Excalidraw drawings right within your Cisco meetings!

Read more in our previous [blog post](/webex-meetings-integration).

### npm improvements

Excalidraw isn't just about https://excalidraw.com, or the Excalidraw+ app. We want to empower teams (and individuals) to create great and novel experiences on top of the Excalidraw editor. For this purpose, Excalidraw is not only [open-source](https://github.com/excalidraw/excalidraw) and licensed under MIT, but also available as an [npm package](https://www.npmjs.com/package/@excalidraw/excalidraw).

Since last year, [we've begun](https://github.com/excalidraw/excalidraw/pull/3614) to publish each commit deployed to the production branch so you don't have to wait for a new stable release. You can check out the package at [@excalidraw/excalidraw-next](https://www.npmjs.com/package/@excalidraw/excalidraw-next).

There have been multiple improvements to the Excalidraw React component. Probably the biggest was Aakansha's adding support for [multiple Excalidraw components on the same page](https://github.com/excalidraw/excalidraw/issues/3043), and [rendering inside scrollable containers](https://github.com/excalidraw/excalidraw/pull/3018).

Among many other changes, we've started [exporting TypeScript definitions](https://github.com/excalidraw/excalidraw/pull/3337), allowed you to change the [static assets path](https://github.com/excalidraw/excalidraw/pull/3068), while Arun added support [customizing canvas actions](https://github.com/excalidraw/excalidraw/pull/3364), and David made it easier for third party apps to [install libraries](https://github.com/excalidraw/excalidraw/pull/3227).

# What's next

While the last year was big, let's make 2022 bigger still.

Tell us your wishes, complaints, or better yet, come [help us](https://github.com/excalidraw/excalidraw/issues) make Excalidraw even better! 🚀

Thank you for a great year, and here's to the next one!

Excalidraw Team


================================================
FILE: gatsby-browser.js
================================================
// custom typefaces
import "typeface-montserrat";
import "typeface-merriweather";
import "./src/blog.css";
import "./src/code.css";
import "./src/styles.css";


================================================
FILE: gatsby-config.js
================================================
module.exports = {
  siteMetadata: {
    title: "Excalidraw Blog",
    description:
      "Get up to speed on the latest news and dive deep into inner workings of Excalidraw",
    image: "/og-image-3.png",
    siteUrl: "https://blog.excalidraw.com",
    social: {
      twitter: "excalidraw",
      github: "excalidraw",
    },
  },
  plugins: [
    {
      resolve: "gatsby-source-filesystem",
      options: {
        path: `${__dirname}/content/blog`,
        name: "blog",
      },
    },
    {
      resolve: "gatsby-source-filesystem",
      options: {
        path: `${__dirname}/content/assets`,
        name: "assets",
      },
    },
    {
      resolve: "gatsby-transformer-remark",
      options: {
        excerpt_separator: "<!-- end -->",
        plugins: [
          {
            resolve: "gatsby-remark-prismjs",
            options: {
              classPrefix: "language-",
              inlineCodeMarker: null,
              aliases: {},
              showLineNumbers: false,
              noInlineHighlight: false,
              prompt: {
                user: "root",
                host: "localhost",
                global: false,
              },
              escapeEntities: {},
            },
          },
          {
            resolve: "gatsby-remark-images",
            options: {
              maxWidth: 590,
            },
          },
          {
            resolve: "gatsby-remark-responsive-iframe",
            options: {
              wrapperStyle: "margin-bottom: 1.0725rem",
            },
          },
          "gatsby-remark-prismjs",
          "gatsby-remark-copy-linked-files",
          "gatsby-remark-smartypants",
          "@weknow/gatsby-remark-twitter",
          // {
          //   resolve: "gatsby-remark-embedder",
          //   options: {
          //     customTransformers: [require("gatsby-embedder-excalidraw")],
          //   },
          // },
        ],
      },
    },
    "gatsby-plugin-dark-mode",
    "gatsby-transformer-sharp",
    "gatsby-plugin-sitemap",
    "gatsby-plugin-sharp",
    {
      resolve: "gatsby-plugin-google-analytics",
      options: {
        trackingId: "UA-387204-13",
      },
    },
    "gatsby-plugin-feed",
    {
      resolve: "gatsby-plugin-manifest",
      options: {
        name: "Excalidraw Blog",
        short_name: "Excalidraw",
        start_url: "/",
        background_color: "#ffffff",
        theme_color: "#663399",
        display: "minimal-ui",
        icon: "content/assets/logo.png",
      },
    },
    "gatsby-plugin-offline",
    "gatsby-plugin-react-helmet",
    {
      resolve: "gatsby-plugin-typography",
      options: {
        pathToConfigModule: "src/utils/typography",
      },
    },
    "gatsby-plugin-twitter",
    "gatsby-plugin-zeit-now",
    {
      resolve: `gatsby-plugin-canonical-urls`,
      options: {
        siteUrl: `https://blog.excalidraw.com`,
        stripQueryString: true,
      },
    },
  ],
};


================================================
FILE: gatsby-node.js
================================================
const path = require(`path`);
const { createFilePath } = require(`gatsby-source-filesystem`);

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions;

  const blogPost = path.resolve(`./src/templates/blog-post.js`);
  const result = await graphql(
    `
      {
        allMarkdownRemark(
          sort: { fields: [frontmatter___date], order: DESC }
          limit: 1000
        ) {
          edges {
            node {
              fields {
                slug
              }
              frontmatter {
                title
              }
            }
          }
        }
      }
    `,
  );

  if (result.errors) {
    throw result.errors;
  }

  // Create blog posts pages.
  const posts = result.data.allMarkdownRemark.edges;

  posts.forEach((post, index) => {
    const previous = index === posts.length - 1 ? null : posts[index + 1].node;
    const next = index === 0 ? null : posts[index - 1].node;

    createPage({
      path: post.node.fields.slug,
      component: blogPost,
      context: {
        slug: post.node.fields.slug,
        previous,
        next,
      },
    });
  });
};

exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions;

  if (node.internal.type === `MarkdownRemark`) {
    const value = createFilePath({ node, getNode });
    createNodeField({
      name: `slug`,
      node,
      value,
    });
  }
};


================================================
FILE: package.json
================================================
{
  "author": "Christopher Chedeau <vjeuxx@gmail.com>",
  "dependencies": {
    "@weknow/gatsby-remark-twitter": "0.2.3",
    "gatsby": "2.32.12",
    "gatsby-embedder-excalidraw": "1.1.4",
    "gatsby-image": "3.11.0",
    "gatsby-plugin-canonical-urls": "2.10.0",
    "gatsby-plugin-dark-mode": "1.1.2",
    "gatsby-plugin-feed": "2.13.1",
    "gatsby-plugin-google-analytics": "2.11.0",
    "gatsby-plugin-manifest": "2.12.1",
    "gatsby-plugin-offline": "3.10.2",
    "gatsby-plugin-react-helmet": "3.10.0",
    "gatsby-plugin-sharp": "2.14.3",
    "gatsby-plugin-sitemap": "2.12.0",
    "gatsby-plugin-twitter": "2.10.0",
    "gatsby-plugin-typography": "2.12.0",
    "gatsby-plugin-zeit-now": "0.3.0",
    "gatsby-remark-copy-linked-files": "2.10.0",
    "gatsby-remark-embedder": "4.2.0",
    "gatsby-remark-images": "3.11.1",
    "gatsby-remark-prismjs": "3.13.0",
    "gatsby-remark-responsive-iframe": "2.11.0",
    "gatsby-remark-smartypants": "2.10.0",
    "gatsby-source-filesystem": "2.11.1",
    "gatsby-transformer-remark": "2.16.1",
    "gatsby-transformer-sharp": "2.12.1",
    "prismjs": "1.23.0",
    "react": "16.14.0",
    "react-dom": "16.14.0",
    "react-helmet": "6.1.0",
    "react-typography": "0.16.19",
    "typeface-merriweather": "1.1.13",
    "typeface-montserrat": "1.1.13",
    "typography": "0.16.19",
    "typography-theme-wordpress-2016": "0.16.19"
  },
  "description": "All the news about Excalidraw",
  "devDependencies": {
    "@excalidraw/prettier-config": "1.0.2",
    "husky": "4.3.8",
    "lint-staged": "10.5.4",
    "prettier": "2.2.1",
    "sharp": "0.28.1"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "license": "MIT",
  "lint-staged": {
    "*.{js,jsx,json,md,yml}": [
      "prettier --write"
    ]
  },
  "name": "excalidraw-blog",
  "prettier": "@excalidraw/prettier-config",
  "private": true,
  "scripts": {
    "build": "export SET NODE_OPTIONS=--openssl-legacy-provider && gatsby build",
    "develop": "gatsby develop",
    "fix": "yarn prettier --write",
    "prettier": "prettier \"**/*.{js,jsx,json,md,yml}\"",
    "serve": "gatsby serve",
    "start": "yarn develop",
    "test": "yarn prettier --list-different"
  },
  "version": "1.0.0",
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}


================================================
FILE: src/blog.css
================================================
.blog-post h3 {
  margin-top: 1rem;
  margin-bottom: .5rem;
}

.blog-post svg,
.blog-post img {
  max-width: 100%;
  margin: 4px auto;
  display: block;
}

.blog-post table {
  width: 60%;
  margin: 0 auto 1.75rem;
}

.blog-post th {
  font-weight: 700;
  font-size: 1.1em;
  border-bottom-width: 3px;
}

.blog-post td[align="right"],
.blog-post th[align="right"] {
  text-align: right;
}

.blog-post td[align="center"],
.blog-post th[align="center"] {
  text-align: center;
}

.blog-post td,
.blog-post th {
  padding: 4px 0;
}

.blog-post .twitter-tweet {
  margin-left: auto;
  margin-right: auto;
}

th,
td {
  border-color: #adb5bd;
}

@media only screen and (max-width: 640px) {
  .blog-post table {
    width: 90%;
  }
}


================================================
FILE: src/code.css
================================================
code[class*="language-"],
pre[class*="language-"] {
  color: #657b83; /* base00 */
  font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
  text-align: left;
  white-space: pre;
  word-spacing: normal;
  word-break: normal;
  word-wrap: normal;

  line-height: 1.5;

  -moz-tab-size: 4;
  -o-tab-size: 4;
  tab-size: 4;

  -webkit-hyphens: none;
  -ms-hyphens: none;
  hyphens: none;
}

/* Code blocks */
pre[class*="language-"] {
  padding: 1em;
  margin: 0.5em 0;
  overflow: auto;
  border-radius: 0.3em;
}

:not(pre) > code[class*="language-"],
pre[class*="language-"] {
  background-color: #fdf6e3; /* base3 */
}

/* Inline code */
:not(pre) > code[class*="language-"] {
  padding: 0.1em;
  border-radius: 0.3em;
}

.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
  color: #93a1a1; /* base1 */
}

.token.punctuation {
  color: #586e75; /* base01 */
}

.token.namespace {
  opacity: 0.7;
}

.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
  color: #268bd2; /* blue */
}

.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.url,
.token.inserted {
  color: #2aa198; /* cyan */
}

.token.entity {
  color: #657b83; /* base00 */
  background: #eee8d5; /* base2 */
}

.token.atrule,
.token.attr-value,
.token.keyword {
  color: #859900; /* green */
}

.token.function,
.token.class-name {
  color: #b58900; /* yellow */
}

.token.regex,
.token.important,
.token.variable {
  color: #cb4b16; /* orange */
}

.token.important,
.token.bold {
  font-weight: bold;
}
.token.italic {
  font-style: italic;
}

.token.entity {
  cursor: help;
}


================================================
FILE: src/components/Toggle.css
================================================
/*
 * Copyright (c) 2015 instructure-react
 * Forked from https://github.com/aaronshaf/react-toggle/
**/

.react-toggle {
  touch-action: pan-x;
  z-index: 1;
  display: inline-block;
  position: relative;
  cursor: pointer;
  background-color: transparent;
  border: 0;
  padding: 0;

  -webkit-touch-callout: none;
  user-select: none;

  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  -webkit-tap-highlight-color: transparent;
}

.react-toggle-screenreader-only {
  border: 0;
  clip: rect(0 0 0 0);
  height: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
  width: 1px;
}

.react-toggle-track {
  width: 50px;
  height: 24px;
  padding: 0;
  border-radius: 30px;
  background-color: hsl(222, 14%, 7%);
  transition: all 0.2s ease;
}

.react-toggle-track-check {
  position: absolute;
  width: 17px;
  height: 17px;
  left: 5px;
  top: 0px;
  bottom: 0px;
  margin-top: auto;
  margin-bottom: auto;
  line-height: 0;
  opacity: 0;
  transition: opacity 0.25s ease;
}

.react-toggle--checked .react-toggle-track-check {
  opacity: 1;
  transition: opacity 0.25s ease;
}

.react-toggle-track-x {
  position: absolute;
  width: 17px;
  height: 17px;
  right: 5px;
  top: 0px;
  bottom: 0px;
  margin-top: auto;
  margin-bottom: auto;
  line-height: 0;
  opacity: 1;
  transition: opacity 0.25s ease;
}

.react-toggle--checked .react-toggle-track-x {
  opacity: 0;
}

.react-toggle-thumb {
  position: absolute;
  top: 1px;
  left: 1px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background-color: #fafafa;
  box-sizing: border-box;
  transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
  transform: translateX(0);
}

.react-toggle--checked .react-toggle-thumb {
  transform: translateX(26px);
  border-color: #19ab27;
}

.react-toggle--focus .react-toggle-thumb {
  /* OC Blue 6 */
  box-shadow: 0px 0px 2px 3px #228be6;
}

.react-toggle:active .react-toggle-thumb {
  /* OC Blue 6 */
  box-shadow: 0px 0px 5px 5px #228be6;
}


================================================
FILE: src/components/Toggle.js
================================================
/*
 * Copyright (c) 2015 instructure-react
 * Copied From Dan Abramov's bloghttps://raw.githubusercontent.com/gaearon/overreacted.io/f08afa0bbdf7612e855e7ac6aabf47f7f7ab8a04/src/components/Toggle.css
 **/

import React, { PureComponent } from "react";
import "./Toggle.css";

// Copyright 2015-present Drifty Co.
// http://drifty.com/
// from: https://github.com/driftyco/ionic/blob/master/src/util/dom.ts
function pointerCoord(event) {
  // get coordinates for either a mouse click
  // or a touch depending on the given event
  if (event) {
    const changedTouches = event.changedTouches;
    if (changedTouches && changedTouches.length > 0) {
      const touch = changedTouches[0];
      return { x: touch.clientX, y: touch.clientY };
    }
    const pageX = event.pageX;
    if (pageX !== undefined) {
      return { x: pageX, y: event.pageY };
    }
  }
  return { x: 0, y: 0 };
}

export default class Toggle extends PureComponent {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
    this.handleTouchStart = this.handleTouchStart.bind(this);
    this.handleTouchMove = this.handleTouchMove.bind(this);
    this.handleTouchEnd = this.handleTouchEnd.bind(this);
    this.handleTouchCancel = this.handleTouchCancel.bind(this);
    this.handleFocus = this.handleFocus.bind(this);
    this.handleBlur = this.handleBlur.bind(this);
    this.previouslyChecked = !!(props.checked || props.defaultChecked);
    this.state = {
      checked: !!(props.checked || props.defaultChecked),
      hasFocus: false,
    };
  }

  componentWillReceiveProps(nextProps) {
    if ("checked" in nextProps) {
      this.setState({ checked: !!nextProps.checked });
      this.previouslyChecked = !!nextProps.checked;
    }
  }

  handleClick(event) {
    const checkbox = this.input;
    this.previouslyChecked = checkbox.checked;
    if (event.target !== checkbox && !this.moved) {
      event.preventDefault();
      checkbox.focus();
      checkbox.click();
      return;
    }

    this.setState({ checked: checkbox.checked });
  }

  handleTouchStart(event) {
    this.startX = pointerCoord(event).x;
    this.touchStarted = true;
    this.hadFocusAtTouchStart = this.state.hasFocus;
    this.setState({ hasFocus: true });
  }

  handleTouchMove(event) {
    if (!this.touchStarted) return;
    this.touchMoved = true;

    if (this.startX != null) {
      let currentX = pointerCoord(event).x;
      if (this.state.checked && currentX + 15 < this.startX) {
        this.setState({ checked: false });
        this.startX = currentX;
      } else if (!this.state.checked && currentX - 15 > this.startX) {
        this.setState({ checked: true });
        this.startX = currentX;
      }
    }
  }

  handleTouchEnd(event) {
    if (!this.touchMoved) return;
    const checkbox = this.input;
    event.preventDefault();

    if (this.startX != null) {
      if (this.previouslyChecked !== this.state.checked) {
        checkbox.click();
      }

      this.touchStarted = false;
      this.startX = null;
      this.touchMoved = false;
    }

    if (!this.hadFocusAtTouchStart) {
      this.setState({ hasFocus: false });
    }
  }

  handleTouchCancel(event) {
    if (this.startX != null) {
      this.touchStarted = false;
      this.startX = null;
      this.touchMoved = false;
    }

    if (!this.hadFocusAtTouchStart) {
      this.setState({ hasFocus: false });
    }
  }

  handleFocus(event) {
    const { onFocus } = this.props;

    if (onFocus) {
      onFocus(event);
    }

    this.hadFocusAtTouchStart = true;
    this.setState({ hasFocus: true });
  }

  handleBlur(event) {
    const { onBlur } = this.props;

    if (onBlur) {
      onBlur(event);
    }

    this.hadFocusAtTouchStart = false;
    this.setState({ hasFocus: false });
  }

  getIcon(type) {
    const { icons } = this.props;
    if (!icons) {
      return null;
    }
    return icons[type] === undefined
      ? Toggle.defaultProps.icons[type]
      : icons[type];
  }

  render() {
    const { className, icons: _icons, ...inputProps } = this.props;
    const classes =
      "react-toggle" +
      (this.state.checked ? " react-toggle--checked" : "") +
      (this.state.hasFocus ? " react-toggle--focus" : "") +
      (this.props.disabled ? " react-toggle--disabled" : "") +
      (className ? " " + className : "");
    return (
      <div
        className={classes}
        onClick={this.handleClick}
        onTouchStart={this.handleTouchStart}
        onTouchMove={this.handleTouchMove}
        onTouchEnd={this.handleTouchEnd}
        onTouchCancel={this.handleTouchCancel}
      >
        <div className="react-toggle-track">
          <div className="react-toggle-track-check">
            {this.getIcon("checked")}
          </div>
          <div className="react-toggle-track-x">
            {this.getIcon("unchecked")}
          </div>
        </div>
        <div className="react-toggle-thumb" />

        <input
          {...inputProps}
          ref={(ref) => {
            this.input = ref;
          }}
          onFocus={this.handleFocus}
          onBlur={this.handleBlur}
          className="react-toggle-screenreader-only"
          type="checkbox"
          aria-label="Switch between Dark and Light mode"
        />
      </div>
    );
  }
}


================================================
FILE: src/components/excalidraw.js
================================================
import React from "react";

import logoPath from "../../content/assets/logo.png";
import { rhythm } from "../utils/typography";

const Excalidraw = () => {
  const logo = (
    <img
      src={logoPath}
      style={{
        height: rhythm(1),
        verticalAlign: "middle",
        paddingRight: rhythm(0.05),
        margin: 0,
      }}
      alt="excalidraw"
    />
  );
  return (
    <a
      className="excalidraw-button"
      style={{
        padding: `${rhythm(0.4)} ${rhythm(0.5)} ${rhythm(0.4)} ${rhythm(0.35)}`,
      }}
      href="https://excalidraw.com"
      target="_blank"
      rel="noopener noreferrer"
    >
      {logo} Open Excalidraw
    </a>
  );
};

export default Excalidraw;


================================================
FILE: src/components/layout.js
================================================
import { Link } from "gatsby";
import { ThemeToggler } from "gatsby-plugin-dark-mode";
import React from "react";
import moon from "../assets/moon.png";
import sun from "../assets/sun.png";
import { rhythm } from "../utils/typography";
import Excalidraw from "./excalidraw";
import "./layoutStyles.css";
import Toggle from "./Toggle";

const Layout = ({ location, title, children, parentClassName }) => {
  const rootPath = `${__PATH_PREFIX__}/`;
  return (
    <div
      style={{
        marginLeft: `auto`,
        marginRight: `auto`,
        maxWidth: rhythm(26),
        padding: `${rhythm(1.5)} ${rhythm(3 / 4)}`,
      }}
    >
      <div
        style={{
          position: "fixed",
          right: "8px",
          top: "8px",
        }}
      >
        <ThemeToggler>
          {({ theme, toggleTheme }) => {
            return (
              <Toggle
                icons={{
                  checked: (
                    <img
                      src={moon}
                      width="16"
                      height="16"
                      alt="presentation"
                      style={{ pointerEvents: "none" }}
                    />
                  ),
                  unchecked: (
                    <img
                      src={sun}
                      width="16"
                      height="16"
                      alt="presentation"
                      style={{ pointerEvents: "none" }}
                    />
                  ),
                }}
                checked={theme === "dark"}
                onChange={() => {
                  toggleTheme(theme === "light" ? "dark" : "light");
                }}
              />
            );
          }}
        </ThemeToggler>
      </div>
      <header>
        <div style={{ fontFamily: "var(--ui-font)", textAlign: "right" }}>
          {location.pathname !== rootPath && (
            <span style={{ float: "left" }}>
              <Link to="/">All posts</Link>
            </span>
          )}
          <Excalidraw />
        </div>
        <span style={{ clear: "both" }} />
        {location.pathname === rootPath ? <h1>{title}</h1> : null}
      </header>
      <main className={parentClassName}>{children}</main>
      <footer
        style={{
          textAlign: "center",
          padding: `${rhythm(2)} 0`,
        }}
      >
        <span>
          © {new Date().getFullYear()} Excalidraw
          {" • "}
          <a href="https://twitter.com/excalidraw">Twitter</a>
          {" • "}
          <a href="https://github.com/excalidraw/excalidraw-blog">
            Source Code
          </a>
          {" • "}
          <a href="https://github.com/excalidraw/excalidraw-blog/blob/master/LICENSE">
            MIT Licensed
          </a>
        </span>
      </footer>
    </div>
  );
};

export default Layout;


================================================
FILE: src/components/layoutStyles.css
================================================
body {
  --bg: #fff;
  /* OC Gray 9 */
  --textNormal: #212529;
  --textTitle: #212529;
  /* OC Blue 6 */
  --textLink: #228be6;
  --hr: hsla(0, 0%, 0%, 0.2);

  background-color: var(--bg);
  font-family: system-ui, sans-serif;
}

body.dark {
  -webkit-font-smoothing: antialiased;
  /* OC Grey 9 */
  --bg: #212529;
  --textNormal: #dbe4ff;
  --textTitle: #fff;
  --hr: hsla(0, 0%, 100%, 0.2);
}

body.dark h1,
h2,
h3,
h4,
h5,
h6 {
  color: var(--textTitle);
}

h1,
h2,
h3 {
  line-height: 1.4;
}

h1, h1 code {
  font-size: 2.5rem;
}

body.dark,
ul,
p,
blockquote {
  color: var(--textNormal);
}

blockquote {
  border-left-color: var(--textNormal);
}

figcaption {
  text-align: center;
  font-size: 0.9rem;
}

body.dark a {
  color: var(--textLink);
}


================================================
FILE: src/components/seo.js
================================================
/**
 * SEO component that queries for data with
 *  Gatsby's useStaticQuery React hook
 *
 * See: https://www.gatsbyjs.org/docs/use-static-query/
 */

import { graphql, useStaticQuery } from "gatsby";
import React from "react";
import Helmet from "react-helmet";

const SEO = ({ description = "", lang = "en", meta = [], title, image }) => {
  const { site } = useStaticQuery(
    graphql`
      query SEO {
        site {
          siteMetadata {
            title
            description
            image
            siteUrl
          }
        }
      }
    `,
  );

  const metaDescription = description || site.siteMetadata.description;
  const metaTitle = title
    ? `${title} | ${site.siteMetadata.title}`
    : site.siteMetadata.title;
  let metaImage = image || site.siteMetadata.image;
  if (!metaImage.includes("http")) {
    metaImage = `${site.siteMetadata.siteUrl}${metaImage}`;
  }

  return (
    <Helmet
      htmlAttributes={{
        lang,
      }}
      title={metaTitle}
      meta={[
        {
          name: "description",
          content: metaDescription,
        },
        {
          property: "og:title",
          content: metaTitle,
        },
        {
          property: "og:description",
          content: metaDescription,
        },
        {
          property: "og:image",
          content: metaImage,
        },
        {
          property: "og:type",
          content: "website",
        },
        {
          name: "twitter:card",
          content: "summary_large_image",
        },
        {
          name: "twitter:title",
          content: metaTitle,
        },
        {
          name: "twitter:description",
          content: metaDescription,
        },
        {
          name: "twitter:image",
          content: metaImage,
        },
      ].concat(meta)}
    />
  );
};

export default SEO;


================================================
FILE: src/pages/404.js
================================================
import React from "react";
import { graphql } from "gatsby";

import Layout from "../components/layout";
import SEO from "../components/seo";

const NotFoundPage = ({ data, location }) => {
  const siteTitle = data.site.siteMetadata.title;

  return (
    <Layout location={location} title={siteTitle}>
      <SEO title="404: Not Found" />
      <h1>Not Found</h1>
      <p>You just hit a route that doesn&#39;t exist... the sadness.</p>
    </Layout>
  );
};

export default NotFoundPage;

export const pageQuery = graphql`
  query NotFoundPage {
    site {
      siteMetadata {
        title
      }
    }
  }
`;


================================================
FILE: src/pages/index.js
================================================
import React from "react";
import { Link, graphql } from "gatsby";

import Layout from "../components/layout";
import SEO from "../components/seo";
import { rhythm } from "../utils/typography";

function BlogIndex({ data, location }) {
  const posts = data.allMarkdownRemark.edges;
  const title = data.site.siteMetadata.title;

  return (
    <Layout location={location} title={title}>
      <SEO />
      {posts.map(({ node }) => {
        const title = node.frontmatter.title || node.fields.slug;
        const authors =
          node.frontmatter.author
            ?.split(",")
            .filter((author) => author.trim()) || [];
        const authorLinks =
          node.frontmatter.link?.split(",").filter((link) => link.trim()) || [];

        return (
          <div key={node.fields.slug}>
            <h3
              style={{
                marginBottom: rhythm(1 / 4),
              }}
            >
              <Link style={{ boxShadow: "none" }} to={node.fields.slug}>
                {title}
              </Link>
            </h3>
            <p style={{ marginBottom: "4px" }}>
              <strong>{node.frontmatter.date}</strong>
              {authors.length && (
                <span style={{ opacity: 0.75, fontStyle: "italic" }}>
                  {", by "}
                  {authors.map((author, idx) => (
                    <>
                      {authorLinks[idx] || authorLinks[0] ? (
                        <a href={authorLinks[idx] || authorLinks[0]}>
                          {author}
                        </a>
                      ) : (
                        <>{author}</>
                      )}
                      {idx < authors.length - 1 && ", "}
                    </>
                  ))}
                </span>
              )}
            </p>
            <p style={{ opacity: 0.75 }}>{node.excerpt}</p>
          </div>
        );
      })}
    </Layout>
  );
}

export default BlogIndex;

export const pageQuery = graphql`
  query BlogIndex {
    site {
      siteMetadata {
        title
        description
      }
    }
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
      edges {
        node {
          excerpt
          fields {
            slug
          }
          frontmatter {
            date(formatString: "MMMM DD, YYYY")
            title
            author
            link
          }
        }
      }
    }
  }
`;


================================================
FILE: src/styles.css
================================================
.excalidraw-button {
  background-color: #fff;
  border-radius: 8px;
  border: 1px solid #868e96;
  box-shadow: none;
  font-weight: 600;
  transition: all 0.2s;
}
.excalidraw-button:hover {
  background-color: #f1f3f5;
}
svg {
  max-width: 100%;
  height: initial;
  width: initial;
}
video {
  max-width: 100%;
}


================================================
FILE: src/templates/blog-post.js
================================================
import { graphql, Link } from "gatsby";
import React from "react";
import Layout from "../components/layout";
import SEO from "../components/seo";
import { rhythm } from "../utils/typography";

function BlogPostTemplate({ data, pageContext: { previous, next }, location }) {
  const post = data.markdownRemark;
  const siteTitle = data.site.siteMetadata.title;
  const editUrl = `https://github.com/excalidraw/excalidraw-blog/edit/master/${post.fileAbsolutePath.substr(
    post.fileAbsolutePath.indexOf("content/blog"),
  )}`;
  const discussUrl = `https://mobile.twitter.com/search?q=${encodeURIComponent(
    `https://blog.excalidraw.com${post.fields.slug}`,
  )}`;

  const authors =
    post.frontmatter.author?.split(",").filter((author) => author.trim()) || [];
  const authorLinks =
    post.frontmatter.link?.split(",").filter((link) => link.trim()) || [];

  let postHTML = post.html;

  if (postHTML.includes("<!-- end -->")) {
    postHTML = postHTML.split("<!-- end -->")[1];
  }

  return (
    <Layout location={location} title={siteTitle} parentClassName={"blog-post"}>
      <SEO
        title={post.frontmatter.title}
        description={post.excerpt}
        image={post.frontmatter.image && post.frontmatter.image.publicURL}
      />
      <h1
        style={{
          marginBottom: 0,
        }}
      >
        {post.frontmatter.title}
      </h1>
      <p
        style={{
          marginBottom: rhythm(1),
          fontFamily: "var(--ui-font)",
        }}
      >
        <strong>{post.frontmatter.date}</strong>
        {authors.length && (
          <span style={{ opacity: 0.75, fontStyle: "italic" }}>
            {", by "}
            {authors.map((author, idx) => (
              <>
                {authorLinks[idx] || authorLinks[0] ? (
                  <a href={authorLinks[idx] || authorLinks[0]}>{author}</a>
                ) : (
                  <>{author}</>
                )}
                {idx < authors.length - 1 && ", "}
              </>
            ))}
          </span>
        )}
        {post.frontmatter.note ? (
          <>
            {" • "}
            <span dangerouslySetInnerHTML={{ __html: post.frontmatter.note }} />
          </>
        ) : null}
      </p>
      <div dangerouslySetInnerHTML={{ __html: postHTML }} />
      <p style={{ fontFamily: "var(--ui-font)", marginBottom: 0 }}>
        <a href={discussUrl}>Discuss on Twitter</a>
        {" • "}
        <a href={editUrl}>Edit on GitHub</a>
      </p>
      {previous || next ? (
        <>
          <hr
            style={{
              margin: `${rhythm(1)} 0`,
            }}
          />
          <ul
            style={{
              display: `flex`,
              flexWrap: `wrap`,
              justifyContent: `space-between`,
              listStyle: `none`,
              padding: 0,
            }}
          >
            <li>
              {previous && (
                <Link to={previous.fields.slug} rel="prev">
                  ← {previous.frontmatter.title}
                </Link>
              )}
            </li>
            <li>
              {next && (
                <Link to={next.fields.slug} rel="next">
                  {next.frontmatter.title} →
                </Link>
              )}
            </li>
          </ul>
        </>
      ) : null}
    </Layout>
  );
}

export default BlogPostTemplate;

export const pageQuery = graphql`
  query BlogPostBySlug($slug: String!) {
    site {
      siteMetadata {
        title
      }
    }
    markdownRemark(fields: { slug: { eq: $slug } }) {
      id
      excerpt
      html
      fileAbsolutePath
      frontmatter {
        title
        date(formatString: "MMMM DD, YYYY")
        note
        author
        link
        image {
          id
          publicURL
        }
      }
      fields {
        slug
      }
    }
  }
`;


================================================
FILE: src/utils/typography.js
================================================
import Typography from "typography";
import Wordpress2016 from "typography-theme-wordpress-2016";

Wordpress2016.headerFontFamily = "BlinkMacSystemFont, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif".split(
  ", ",
);
Wordpress2016.headerWeight = 700;

Wordpress2016.overrideThemeStyles = () => {
  return {
    ":root": {
      "--ui-font": Wordpress2016.headerFontFamily.join(","),
    },
    h1: {
      fontFamily: "var(--ui-font)",
      fontWeight: 700,
    },

    "a.gatsby-resp-image-link": {
      boxShadow: "none",
    },
    ".gatsby-highlight": {
      fontSize: "0.9em",
    },
  };
};

delete Wordpress2016.googleFonts;

const typography = new Typography(Wordpress2016);

// Hot reload typography in development.
if (process.env.NODE_ENV !== "production") {
  typography.injectStyles();
}

export default typography;
export const rhythm = typography.rhythm;
export const scale = typography.scale;


================================================
FILE: static/robots.txt
================================================
User-agent: *
Disallow:


================================================
FILE: vercel.json
================================================
{
  "cleanUrls": true,
  "redirects": [
    {
      "source": "/sitemap.xml",
      "destination": "https://plus.excalidraw.com/blog-sitemap.xml",
      "permanent": true
    },
    {
      "source": "/",
      "destination": "https://plus.excalidraw.com/blog",
      "permanent": true
    },
    {
      "source": "/(.*)/",
      "destination": "https://plus.excalidraw.com/blog/$1",
      "permanent": true
    },
    {
      "source": "/(.*)",
      "destination": "https://plus.excalidraw.com/blog/$1",
      "permanent": true
    }
  ]
}
Download .txt
gitextract_g20e0di6/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .prettierignore
├── .vscode/
│   └── settings.json
├── .yarnrc
├── LICENSE
├── README.md
├── content/
│   └── blog/
│       ├── browser-fs-access/
│       │   └── index.md
│       ├── building-excalidraw-p2p-collaboration-feature/
│       │   └── index.md
│       ├── deprecating-excalidraw-electron/
│       │   └── index.md
│       ├── enabling-translations/
│       │   └── index.md
│       ├── end-to-end-encryption/
│       │   └── index.md
│       ├── excalidraw-and-fugu/
│       │   └── index.md
│       ├── introducing-excalidraw-plus/
│       │   └── index.md
│       ├── one-year-of-excalidraw/
│       │   └── index.md
│       ├── open-colors/
│       │   └── index.md
│       ├── redesigning-editor-api/
│       │   └── index.md
│       ├── reflections-on-excalidraw/
│       │   └── index.md
│       ├── rethinking-virtual-whiteboard/
│       │   └── index.md
│       ├── tell-your-story-with-charts/
│       │   └── index.md
│       ├── webex-meetings-integration/
│       │   └── index.md
│       ├── year-three/
│       │   └── index.md
│       └── year-two/
│           └── index.md
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── package.json
├── src/
│   ├── blog.css
│   ├── code.css
│   ├── components/
│   │   ├── Toggle.css
│   │   ├── Toggle.js
│   │   ├── excalidraw.js
│   │   ├── layout.js
│   │   ├── layoutStyles.css
│   │   └── seo.js
│   ├── pages/
│   │   ├── 404.js
│   │   └── index.js
│   ├── styles.css
│   ├── templates/
│   │   └── blog-post.js
│   └── utils/
│       └── typography.js
├── static/
│   └── robots.txt
└── vercel.json
Download .txt
SYMBOL INDEX (15 symbols across 3 files)

FILE: src/components/Toggle.js
  function pointerCoord (line 12) | function pointerCoord(event) {
  class Toggle (line 29) | class Toggle extends PureComponent {
    method constructor (line 30) | constructor(props) {
    method componentWillReceiveProps (line 46) | componentWillReceiveProps(nextProps) {
    method handleClick (line 53) | handleClick(event) {
    method handleTouchStart (line 66) | handleTouchStart(event) {
    method handleTouchMove (line 73) | handleTouchMove(event) {
    method handleTouchEnd (line 89) | handleTouchEnd(event) {
    method handleTouchCancel (line 109) | handleTouchCancel(event) {
    method handleFocus (line 121) | handleFocus(event) {
    method handleBlur (line 132) | handleBlur(event) {
    method getIcon (line 143) | getIcon(type) {
    method render (line 153) | render() {

FILE: src/pages/index.js
  function BlogIndex (line 8) | function BlogIndex({ data, location }) {

FILE: src/templates/blog-post.js
  function BlogPostTemplate (line 7) | function BlogPostTemplate({ data, pageContext: { previous, next }, locat...
Condensed preview — 43 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (187K chars).
[
  {
    "path": ".github/dependabot.yml",
    "chars": 139,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: npm\n    directory: /\n    schedule:\n      interval: weekly\n      day: sunday\n "
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 248,
    "preview": "name: Test formatting\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\njobs:\n  test:\n    runs-on: ubuntu-lates"
  },
  {
    "path": ".gitignore",
    "chars": 139,
    "preview": ".cache\n.DS_Store\n.yarn-integrity\n*.log\nnode_modules\nnpm-debug.log*\npackage-lock.json\npublic\nyarn-debug.log*\nyarn-error.l"
  },
  {
    "path": ".prettierignore",
    "chars": 14,
    "preview": ".cache\npublic\n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 29,
    "preview": "{\n  \"cSpell.enabled\": true\n}\n"
  },
  {
    "path": ".yarnrc",
    "chars": 17,
    "preview": "--add.exact true\n"
  },
  {
    "path": "LICENSE",
    "chars": 1077,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2020 Excalidraw\n\nPermission is hereby granted, free of charge, to any person obtain"
  },
  {
    "path": "README.md",
    "chars": 1056,
    "preview": "# Excalidraw Blog\n\n> For news and updates visit: https://blog.excalidraw.com.\n\n## Develop\n\nWe are using [Gatsby](https:/"
  },
  {
    "path": "content/blog/browser-fs-access/index.md",
    "chars": 13643,
    "preview": "---\ntitle: Reading and writing files and directories with the browser-fs-access library\ndate: 2020-12-09\nnote: 'This pos"
  },
  {
    "path": "content/blog/building-excalidraw-p2p-collaboration-feature/index.md",
    "chars": 8588,
    "preview": "---\ntitle: Building Excalidraw's P2P Collaboration Feature\ndate: 2020-03-29\nauthor: idlewinn\nlink: https://twitter.com/e"
  },
  {
    "path": "content/blog/deprecating-excalidraw-electron/index.md",
    "chars": 13608,
    "preview": "---\ntitle: Deprecating Excalidraw Electron in favor of the Web version\ndate: 2020-12-17\nauthor: tomayac\nlink: https://tw"
  },
  {
    "path": "content/blog/enabling-translations/index.md",
    "chars": 4365,
    "preview": "---\ntitle: Enabling Translations\ndate: 2020-04-16\nauthor: Lipis\nlink: https://twitter.com/lipis\n---\n\nFrom the early days"
  },
  {
    "path": "content/blog/end-to-end-encryption/index.md",
    "chars": 5448,
    "preview": "---\ntitle: End-to-End Encryption in the Browser\ndate: 2020-03-21\nauthor: vjeux\nlink: https://twitter.com/vjeux\nimage: og"
  },
  {
    "path": "content/blog/excalidraw-and-fugu/index.md",
    "chars": 25090,
    "preview": "---\ntitle: \"Excalidraw and Fugu: Improving Core User Journeys\"\ndate: 2021-05-21\nnote: 'This post appeared first on <a hr"
  },
  {
    "path": "content/blog/introducing-excalidraw-plus/index.md",
    "chars": 3851,
    "preview": "---\ntitle: Introducing Excalidraw+\ndate: 2021-05-03\nauthor: Excalidraw Team\nlink: https://twitter.com/excalidraw\nimage: "
  },
  {
    "path": "content/blog/one-year-of-excalidraw/index.md",
    "chars": 13021,
    "preview": "---\ntitle: One Year of Excalidraw\ndate: 2021-01-01\nauthor: Excalidraw Team\nlink: https://github.com/orgs/excalidraw/peop"
  },
  {
    "path": "content/blog/open-colors/index.md",
    "chars": 1884,
    "preview": "---\ntitle: Open Colors\ndate: 2020-11-10\nnote: Part of a series where we pick apart various libraries and projects we use"
  },
  {
    "path": "content/blog/redesigning-editor-api/index.md",
    "chars": 9912,
    "preview": "---\ntitle: Rethinking the Component API\ndate: 2023-01-13\nauthor: Excalidraw Team\nlink: https://github.com/orgs/excalidra"
  },
  {
    "path": "content/blog/reflections-on-excalidraw/index.md",
    "chars": 10373,
    "preview": "---\ntitle: Reflections on Excalidraw\ndate: 2020-01-15\nnote: 'This post appeared first on <a href=\"https://blog.vjeux.com"
  },
  {
    "path": "content/blog/rethinking-virtual-whiteboard/index.md",
    "chars": 3768,
    "preview": "---\ntitle: Rethinking Virtual Whiteboard\ndate: 2020-03-28\nauthor: vjeux\nlink: https://twitter.com/vjeux\n---\n\n[Excalidraw"
  },
  {
    "path": "content/blog/tell-your-story-with-charts/index.md",
    "chars": 2961,
    "preview": "---\ntitle: Tell your story with Charts\ndate: 2020-12-20\nauthor: Lipis\nlink: https://twitter.com/lipis\nimage: og.png\n---\n"
  },
  {
    "path": "content/blog/webex-meetings-integration/index.md",
    "chars": 3090,
    "preview": "---\ntitle: Introducing Webex Integration\ndate: 2021-10-28\nauthor: Aakansha, David\nlink: https://twitter.com/aakansha1216"
  },
  {
    "path": "content/blog/year-three/index.md",
    "chars": 15768,
    "preview": "---\ntitle: Three for three\ndate: 2023-01-04\nauthor: Excalidraw Team\nlink: https://github.com/orgs/excalidraw/people\nimag"
  },
  {
    "path": "content/blog/year-two/index.md",
    "chars": 10335,
    "preview": "---\ntitle: Year two of Excalidraw\ndate: 2022-01-03\nauthor: Excalidraw Team\nlink: https://github.com/orgs/excalidraw/peop"
  },
  {
    "path": "gatsby-browser.js",
    "chars": 159,
    "preview": "// custom typefaces\nimport \"typeface-montserrat\";\nimport \"typeface-merriweather\";\nimport \"./src/blog.css\";\nimport \"./src"
  },
  {
    "path": "gatsby-config.js",
    "chars": 2951,
    "preview": "module.exports = {\n  siteMetadata: {\n    title: \"Excalidraw Blog\",\n    description:\n      \"Get up to speed on the latest"
  },
  {
    "path": "gatsby-node.js",
    "chars": 1427,
    "preview": "const path = require(`path`);\nconst { createFilePath } = require(`gatsby-source-filesystem`);\n\nexports.createPages = asy"
  },
  {
    "path": "package.json",
    "chars": 2413,
    "preview": "{\n  \"author\": \"Christopher Chedeau <vjeuxx@gmail.com>\",\n  \"dependencies\": {\n    \"@weknow/gatsby-remark-twitter\": \"0.2.3\""
  },
  {
    "path": "src/blog.css",
    "chars": 728,
    "preview": ".blog-post h3 {\n  margin-top: 1rem;\n  margin-bottom: .5rem;\n}\n\n.blog-post svg,\n.blog-post img {\n  max-width: 100%;\n  mar"
  },
  {
    "path": "src/code.css",
    "chars": 1664,
    "preview": "code[class*=\"language-\"],\npre[class*=\"language-\"] {\n  color: #657b83; /* base00 */\n  font-family: Consolas, Monaco, \"And"
  },
  {
    "path": "src/components/Toggle.css",
    "chars": 1975,
    "preview": "/*\n * Copyright (c) 2015 instructure-react\n * Forked from https://github.com/aaronshaf/react-toggle/\n**/\n\n.react-toggle "
  },
  {
    "path": "src/components/Toggle.js",
    "chars": 5288,
    "preview": "/*\n * Copyright (c) 2015 instructure-react\n * Copied From Dan Abramov's bloghttps://raw.githubusercontent.com/gaearon/ov"
  },
  {
    "path": "src/components/excalidraw.js",
    "chars": 706,
    "preview": "import React from \"react\";\n\nimport logoPath from \"../../content/assets/logo.png\";\nimport { rhythm } from \"../utils/typog"
  },
  {
    "path": "src/components/layout.js",
    "chars": 2838,
    "preview": "import { Link } from \"gatsby\";\nimport { ThemeToggler } from \"gatsby-plugin-dark-mode\";\nimport React from \"react\";\nimport"
  },
  {
    "path": "src/components/layoutStyles.css",
    "chars": 757,
    "preview": "body {\n  --bg: #fff;\n  /* OC Gray 9 */\n  --textNormal: #212529;\n  --textTitle: #212529;\n  /* OC Blue 6 */\n  --textLink: "
  },
  {
    "path": "src/components/seo.js",
    "chars": 1856,
    "preview": "/**\n * SEO component that queries for data with\n *  Gatsby's useStaticQuery React hook\n *\n * See: https://www.gatsbyjs.o"
  },
  {
    "path": "src/pages/404.js",
    "chars": 615,
    "preview": "import React from \"react\";\nimport { graphql } from \"gatsby\";\n\nimport Layout from \"../components/layout\";\nimport SEO from"
  },
  {
    "path": "src/pages/index.js",
    "chars": 2434,
    "preview": "import React from \"react\";\nimport { Link, graphql } from \"gatsby\";\n\nimport Layout from \"../components/layout\";\nimport SE"
  },
  {
    "path": "src/styles.css",
    "chars": 315,
    "preview": ".excalidraw-button {\n  background-color: #fff;\n  border-radius: 8px;\n  border: 1px solid #868e96;\n  box-shadow: none;\n  "
  },
  {
    "path": "src/templates/blog-post.js",
    "chars": 3852,
    "preview": "import { graphql, Link } from \"gatsby\";\nimport React from \"react\";\nimport Layout from \"../components/layout\";\nimport SEO"
  },
  {
    "path": "src/utils/typography.js",
    "chars": 929,
    "preview": "import Typography from \"typography\";\nimport Wordpress2016 from \"typography-theme-wordpress-2016\";\n\nWordpress2016.headerF"
  },
  {
    "path": "static/robots.txt",
    "chars": 24,
    "preview": "User-agent: *\nDisallow:\n"
  },
  {
    "path": "vercel.json",
    "chars": 543,
    "preview": "{\n  \"cleanUrls\": true,\n  \"redirects\": [\n    {\n      \"source\": \"/sitemap.xml\",\n      \"destination\": \"https://plus.excalid"
  }
]

About this extraction

This page contains the full source code of the excalidraw/excalidraw-blog GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 43 files (175.7 KB), approximately 45.3k tokens, and a symbol index with 15 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!