[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: npm\n    directory: /\n    schedule:\n      interval: weekly\n      day: sunday\n      time: \"01:00\"\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test formatting\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v1\n      - name: Install and test\n        run: |\n          yarn\n          yarn test\n"
  },
  {
    "path": ".gitignore",
    "content": ".cache\n.DS_Store\n.yarn-integrity\n*.log\nnode_modules\nnpm-debug.log*\npackage-lock.json\npublic\nyarn-debug.log*\nyarn-error.log\nyarn-error.log*\n"
  },
  {
    "path": ".prettierignore",
    "content": ".cache\npublic\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"cSpell.enabled\": true\n}\n"
  },
  {
    "path": ".yarnrc",
    "content": "--add.exact true\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020 Excalidraw\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Excalidraw Blog\n\n> For news and updates visit: https://blog.excalidraw.com.\n\n## Develop\n\nWe are using [Gatsby](https://www.gatsbyjs.com/) and in order to run it locally, execute the following from the root:\n\n```\nyarn\nyarn start\n```\n\nVisit [`localhost:8000`](http://localhost:8000) to test it.\n\n## Writing a Blog Post\n\n- Create a new folder inside the `content/blog/`\n  - The folder name should follow `kebab-case`\n  - Use the slug of the title of the post to name it\n- Create `index.md` inside that folder\n- Create `og.png` inside that folder for the open graph image\n- Add a frontmatter\n  - `title`: Use Title Case for Titles\n  - `date`: Date in ISO format. Example: `2020-03-12`\n  - `note`: Optional. Displayed next to the date when reading a post.\n  - `image`: Filename of the open graph image. Example: `og.png`\n- The `note` field is usually used to link to the original post when reposting ([example](https://blog.excalidraw.com/reflections-on-excalidraw/))\n- Add somewhere the `<!-- end -->` to declare your `excerpt` (it's used on the front page)\n"
  },
  {
    "path": "content/blog/browser-fs-access/index.md",
    "content": "---\ntitle: Reading and writing files and directories with the browser-fs-access library\ndate: 2020-12-09\nnote: 'This post appeared first on <a href=\"https://web.dev/browser-fs-access/\">web.dev</a>.'\nauthor: tomayac\nlink: https://twitter.com/tomayac\nimage: chrome-save-as.png\n---\n\nBrowsers 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.\n\n<!-- end -->\n\n## The traditional way of dealing with files\n\n<blockquote>\n  If you know how it used to work the old way, you can\n  <a href=\"#the-file-system-access-api\">jump down straight to the new way</a>.\n</blockquote>\n\n### Opening files\n\nAs 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.\n\n```js\nconst openFile = async () => {\n  return new Promise((resolve) => {\n    const input = document.createElement(\"input\");\n    input.type = \"file\";\n    input.addEventListener(\"change\", () => {\n      resolve(input.files[0]);\n    });\n    input.click();\n  });\n};\n```\n\n### Opening directories\n\nFor 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.\n\n### Saving (rather: downloading) files\n\nFor 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.\n\n<blockquote>\n  To prevent memory leaks, always revoke the URL after the download.\n</blockquote>\n\n```js\nconst saveFile = async (blob) => {\n  const a = document.createElement(\"a\");\n  a.download = \"my-file.txt\";\n  a.href = URL.createObjectURL(blob);\n  a.addEventListener(\"click\", (e) => {\n    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);\n  });\n  a.click();\n};\n```\n\n### The problem\n\nA 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\".\n\n<h2 id=\"the-file-system-access-api\">The File System Access API</h2>\n\nThe 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.\n\n<blockquote>\n  For a more thorough introduction to the File System Access API, see the article\n  <a href=\"https://web.dev/file-system-access/\">The File System Access API: simplifying access to local files</a>.\n</blockquote>\n\n### Opening files\n\nWith 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.\n\n```js\nconst openFile = async () => {\n  try {\n    // Always returns an array.\n    const [handle] = await window.showOpenFilePicker();\n    return handle.getFile();\n  } catch (err) {\n    console.error(err.name, err.message);\n  }\n};\n```\n\n### Opening directories\n\nOpen a directory by calling `window.showDirectoryPicker()` that makes directories selectable in the file dialog box.\n\n### Saving files\n\nSaving 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.\n\n```js\nconst saveFile = async (blob) => {\n  try {\n    const handle = await window.showSaveFilePicker({\n      types: [\n        {\n          accept: {\n            // Omitted\n          },\n        },\n      ],\n    });\n    const writable = await handle.createWritable();\n    await writable.write(blob);\n    await writable.close();\n    return handle;\n  } catch (err) {\n    console.error(err.name, err.message);\n  }\n};\n```\n\n## Introducing browser-fs-access\n\nAs perfectly fine as the File System Access API is, it's [not yet widely available](https://caniuse.com/native-filesystem-api).\n\n<figure>\n  <img src=\"caniuse.png\"\n       alt=\"Browser support table for the File System Access API. All browsers are marked as 'no support' or 'behind a flag'.\">\n  <figcaption>\n    Browser support table for the File System Access API.\n    (<a href=\"https://caniuse.com/native-filesystem-api\">Source</a>)\n  </figcaption>\n</figure>\n\nThis 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.\n\n### Design philosophy\n\nSince 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.\n\n### Using the browser-fs-access library\n\nThe 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.\n\n```js\n// The imported methods will use the File\n// System Access API or a fallback implementation.\nimport {\n  fileOpen,\n  directoryOpen,\n  fileSave,\n} from \"https://unpkg.com/browser-fs-access\";\n\n(async () => {\n  // Open an image file.\n  const blob = await fileOpen({\n    mimeTypes: [\"image/*\"],\n  });\n\n  // Open multiple image files.\n  const blobs = await fileOpen({\n    mimeTypes: [\"image/*\"],\n    multiple: true,\n  });\n\n  // Open all files in a directory,\n  // recursively including subdirectories.\n  const blobsInDirectory = await directoryOpen({\n    recursive: true,\n  });\n\n  // Save a file.\n  await fileSave(blob, {\n    fileName: \"Untitled.png\",\n  });\n})();\n```\n\n### Demo\n\nYou 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.\n\n## The browser-fs-access library in the wild\n\nIn 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.\n\nI 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.\n\n<figure>\n  <img src=\"iphone-original.png\" width=\"300\" alt=\"An Excalidraw drawing on an iPhone.\">\n  <figcaption>\n    Starting an Excalidraw drawing on an iPhone where the File System Access API is not supported,\n    but where a file can be saved (downloaded) to the Downloads folder.\n  </figcaption>\n</figure>\n\n<figure>\n  <img src=\"chrome-modify.png\" alt=\"The modified Excalidraw drawing on Chrome on the desktop.\">\n  <figcaption>\n    Opening and modifying the Excalidraw drawing on the desktop where the File System Access API is supported\n    and thus the file can be accessed via the API.\n  </figcaption>\n</figure>\n\n<figure>\n  <img src=\"chrome-oversave.png\" alt=\"Overwriting the original file with the modifications.\">\n  <figcaption>\n    Overwriting the original file with the modifications to the original Excalidraw drawing file.\n    The browser shows a dialog asking me whether this is fine.\n  </figcaption>\n</figure>\n\n<figure>\n  <img src=\"chrome-save-as.png\" alt=\"Saving the modifications to a new Excalidraw drawing file.\">\n  <figcaption>\n    Saving the modifications to a new Excalidraw file. The original file remains untouched.\n  </figcaption>\n</figure>\n\n### Real life code sample\n\nBelow, 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.\n\n```js\nexport const saveAsJSON = async (\n  elements: readonly ExcalidrawElement[],\n  appState: AppState,\n  fileHandle: any,\n) => {\n  const serialized = serializeAsJSON(elements, appState);\n  const blob = new Blob([serialized], {\n    type: \"application/json\",\n  });\n  const name = `${appState.name}.excalidraw`;\n  (window as any).handle = await fileSave(\n    blob,\n    {\n      fileName: name,\n      description: \"Excalidraw file\",\n      extensions: [\"excalidraw\"],\n    },\n    fileHandle || null,\n  );\n};\n\nexport const loadFromJSON = async () => {\n  const blob = await fileOpen({\n    description: \"Excalidraw files\",\n    extensions: [\"json\", \"excalidraw\"],\n    mimeTypes: [\"application/json\"],\n  });\n  return loadFromBlob(blob);\n};\n```\n\n### UI considerations\n\nWhether 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.\n\n<figure>\n  <img src=\"save.png\" alt=\"Excalidraw app toolbar on iPhone with just a 'Save' button.\" width=\"300\">\n  <figcaption>\n    Excalidraw app toolbar on iPhone with just a <strong>Save</strong> button.\n  </figcaption>\n</figure>\n\n<figure>\n  <img src=\"save-save-as.png\" alt=\"Excalidraw app toolbar on Chrome desktop with a 'Save' and a 'Save As' button.\" width=\"300\">\n  <figcaption>\n    Excalidraw app toolbar on Chrome  with a <strong>Save</strong> and a focused <strong>Save As</strong> button.\n  </figcaption>\n</figure>\n\n## Conclusions\n\nWorking 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.\n\n## Acknowledgements\n\nThis 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.\n"
  },
  {
    "path": "content/blog/building-excalidraw-p2p-collaboration-feature/index.md",
    "content": "---\ntitle: Building Excalidraw's P2P Collaboration Feature\ndate: 2020-03-29\nauthor: idlewinn\nlink: https://twitter.com/edwinlin1987\nimage: og.png\n---\n\n[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.\n\n<!-- end -->\n\nThe 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.\n\n## The requirements\n\n- 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/).\n- 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.\n\n## Excalidraw's single-player architecture\n\nOriginally, Excalidraw kept an array of all the different drawn shapes -- called `ExcalidrawElement`s -- in Z-index order. The `ExcalidrawElement` interface looked something like this:\n\n```typescript\ninterface ExcalidrawElement {\n  id: string;\n  type: \"square\" | \"circle\" | \"arrow\";\n  width: number;\n  height: number;\n  // ... other fields describing the shape ...\n  canvas: HTMLCanvasElement;\n  isSelected: boolean;\n}\n```\n\nThis architecture was very easy to use client-side, but as we'll see, presented some challenges as we moved to multiplayer.\n\n## Moving to multiplayer\n\nWe 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.\n\nConceptually, 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.\n\n## Socket.IO\n\nWhen 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).\n\nSome features of Socket.IO that are particularly beneficial to multiplayer:\n\n- **Auto-reconnection:** if you get disconnected for any reason, you don't need to reload the page to see updates.\n- **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.\n- **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.\n\n## Dealing with conflicts: adding new elements\n\nOnce we started sharing state between peers, we immediately ran into issues. The first was when elements were added.\n\nIn 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.\n\nhttps://excalidraw.com/#json=5068269564198912,PmL8fegqNyHb0fKxoG6YAA\n\nTo 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.\n\n## Dealing with conflicts: deleting elements\n\nThere'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.\n\nTo 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.\n\nhttps://excalidraw.com/#json=5148123005452288,FnrZbAe4qkHQCSd2BSkUIQ\n\nThe 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.\n\n## Dealing with conflicts: merging concurrent edits\n\nThere 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.\n\nTo 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.\n\nhttps://excalidraw.com/#json=5147452789227520,QaCOJixahz7VLHs3eG1s7g\n\nThis algorithm is simple but effective, and solved most of our collaboration problems.\n\n## Dealing with conflicts: breaking ties\n\nThe version number only solves race conditions when players are editing different elements concurrently. What if they're editing the same element concurrently?\n\nFor 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.\n\nWith 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!\n\nIn 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.\n\n## Future work\n\nOne 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.\n\nMaybe you can help us solve this problem! Go to [our GitHub](https://github.com/excalidraw/excalidraw), fork the repo, and contribute!\n"
  },
  {
    "path": "content/blog/deprecating-excalidraw-electron/index.md",
    "content": "---\ntitle: Deprecating Excalidraw Electron in favor of the Web version\ndate: 2020-12-17\nauthor: tomayac\nlink: https://twitter.com/tomayac\nimage: excalidraw-icon.png\n---\n\nOn 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.\n\n<!-- end -->\n\n## How Excalidraw Desktop came into being\n\nSoon 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):\n\n> Would be great to wrap Excalidraw within Electron (or equivalent) and publish it as a [platform-specific] application to the various app stores.\n\nThe immediate reaction by [@voluntadpear](https://github.com/voluntadpear) was to suggest:\n\n> 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.\n\nThe decision that [@vjeux](https://github.com/vjeux) took in the end was simple:\n\n> We should do both :)\n\nWhile 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.\n\nTo 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.\n\n## What is Electron?\n\nThe 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.\n\n- 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\"_.\n\n- 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'`.\n\n- 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].\n\nLooking 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.\n\n## Where Excalidraw Desktop left off\n\nExcalidraw 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.\n\n<figure>\n  <img src=\"excalidraw-desktop.png\" alt=\"The Excalidraw Desktop application running in an Electron wrapper.\">\n  <figcaption>Excalidraw Desktop is almost indistinguishable from the Web version</figcaption>\n</figure>\n\n<figure>\n  <img src=\"about-excalidraw.png\" alt=\"The Excalidraw Desktop 'About' window displaying the version of the Electron wrapper and the Web app.\">\n  <figcaption>The <strong>About Excalidraw</strong> menu providing insights into the versions</figcaption>\n</figure>\n\nOn 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.\n\n<figure>\n  <img src=\"menu.png\" alt=\"The Excalidraw Desktop menu bar on macOS with the 'File', 'Close Window' menu item selected.\">\n  <figcaption>The menu bar of Excalidraw Desktop on macOS</figcaption>\n</figure>\n\nWe 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:\n\n```json\n{\n  \"fileAssociations\": [\n    {\n      \"ext\": \"excalidraw\",\n      \"name\": \"Excalidraw\",\n      \"description\": \"Excalidraw file\",\n      \"role\": \"Editor\",\n      \"mimeType\": \"application/json\"\n    }\n  ]\n}\n```\n\nUnfortunately, 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.\n\nThese 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.\n\n## How the web serves us today and in the future\n\nEven 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.\n\n### Installable Progressive Web App\n\nExcalidraw 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.\n\n<figure>\n  <img src=\"excalidraw-cache.png\" alt=\"Chrome DevTools Application tab showing the two Excalidraw caches.\">\n  <figcaption>Excalidraw's cache contents</figcaption>\n</figure>\n\nThis 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.\n\n<figure>\n  <img src=\"install-excalidraw.png\" alt=\"Excalidraw prompting the user to install the app in Chrome on macOS.\">\n  <figcaption>The Excalidraw install dialog in Chrome</figcaption>\n</figure>\n\nExcalidraw 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.\n\n<figure>\n  <img src=\"excalidraw-pwa.png\" alt=\"Excalidraw running in its own window.\">\n  <figcaption>The Excalidraw PWA in a standalone window</figcaption>\n</figure>\n\n<figure>\n  <img src=\"excalidraw-icon.png\" alt=\"Excalidraw icon on the macOS Dock.\">\n  <figcaption>The Excalidraw icon on the macOS Dock</figcaption>\n</figure>\n\n### File system access\n\nExcalidraw 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/).\n\n### Drag and drop support\n\nFiles 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.\n\n### Clipboard access\n\nExcalidraw 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/).\n\n<figure>\n  <img src=\"clipboard.png\" alt=\"Excalidraw context menu showing the 'copy to clipboard as SVG' and 'copy to clipboard as PNG' menu items.\">\n  <figcaption>The Excalidraw context menu offering clipboard actions</figcaption>\n</figure>\n\n### File handling\n\nExcalidraw 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.\n\n### Declarative link capturing\n\nExcalidraw 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.\n\n## Conclusion\n\nThe 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.\n\nElectron 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/).\n\n## Acknowledgements\n\nThis article was reviewed by [@lipis](https://github.com/lipis), [@dwelle](https://github.com/dwelle), and [Joe Medley](https://github.com/jpmedley).\n"
  },
  {
    "path": "content/blog/enabling-translations/index.md",
    "content": "---\ntitle: Enabling Translations\ndate: 2020-04-16\nauthor: Lipis\nlink: https://twitter.com/lipis\n---\n\nFrom 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.\n\n<!-- end -->\n\n## Technical Infrastructure\n\nThe 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.\n\n```js\nimport { t } from \"../i18n\";\n\n<span>{t(\"labels.paste\")}</span>;\n```\n\nThe translated strings are stored as JSON in a file per language like this.\n\n```js\n// en.json\n{\n  \"labels\": {\n    \"paste\": \"Paste\",\n    \"selectAll\": \"Select all\",\n    \"copy\": \"Copy\",\n    \"copyAsPng\": \"Copy to clipboard as PNG\",\n    // ...\n  }\n}\n```\n\nWe 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.\n\nSince 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`.\n\n```js\nexport const languages = [\n  { lng: \"en\", label: \"English\", data: require(\"./locales/en.json\") },\n  { lng: \"bg-BG\", label: \"Български\", data: require(\"./locales/bg-BG.json\") },\n  { lng: \"de-DE\", label: \"Deutsch\", data: require(\"./locales/de-DE.json\") },\n  // ...\n];\n```\n\nThe `t` function is pretty simple, where it splits the path by `.` and looks up both the current and fallback languages.\n\n```ts\n// t(\"labels.paste\")\n//   + current  {\"labels\": {\"paste\": \"Coller\"}}\n//   + fallback {\"labels\": {\"paste\": \"Paste\"}}\n// -> \"Coller\"\nexport function t(path: string, replacement?: { [key: string]: string }) {\n  const parts = path.split(\".\");\n  return (\n    findPartsForData(currentLanguage.data, parts) ||\n    findPartsForData(fallbackLanguage.data, parts)\n  );\n}\n```\n\nThe 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.\n\nFinally the last piece of the puzzle is to be able to change the language. We opted for a simple `<select>` element.\n\n![Language selector](language-selector.png)\n\n## Enter Crowdin\n\nPreviously, 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.\n\nThe 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.\n\n[![Pull Request](pull-request.png)](https://github.com/excalidraw/excalidraw/pull/1416/commits)\n\nThe 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!\n\n[![Language Completion](language-completion.png)](https://crowdin.com/project/excalidraw)\n\nWhere 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.\n\n![Translation Help](translation-help.png)\n\nFinally, 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!\n"
  },
  {
    "path": "content/blog/end-to-end-encryption/index.md",
    "content": "---\ntitle: End-to-End Encryption in the Browser\ndate: 2020-03-21\nauthor: vjeux\nlink: https://twitter.com/vjeux\nimage: og.png\n---\n\n[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.\n\n<!-- end -->\n\nBy 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.\n\nYou 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.\n\n## Traditional Website Architecture\n\nIn 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.\n\nIn 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.\n\nhttps://excalidraw.com/#json=5649116445016064,yOfExolZoMhtGnysT3-LWA\n\nThis 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.\n\n## End-to-End Encryption\n\nWhatsApp 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.\n\nThe 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.\n\nhttps://excalidraw.com/#json=5645858175451136,8w-G0ZXiOfRYAn7VWpANxw\n\nThe 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.\n\nThankfully, 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.\n\nhttps://excalidraw.com/#json=5660568841093120,vki3y9xuEulFVHDqt-PBMw\n\n## Show me the code\n\nFortunately, 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.\n\n### Upload\n\nWe generate a random key that will be used to encrypt the data.\n\n```javascript\nconst key = await window.crypto.subtle.generateKey(\n  { name: \"AES-GCM\", length: 128 },\n  true, // extractable\n  [\"encrypt\", \"decrypt\"],\n);\n```\n\nWe 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)).\n\n```javascript\nconst encrypted = await window.crypto.subtle.encrypt(\n  { name: \"AES-GCM\", iv: new Uint8Array(12) /* don't reuse key! */ },\n  key,\n  new TextEncoder().encode(JSON.stringify(content)),\n);\n```\n\nWe upload the encrypted content to the server. Note that we don't send the key to the server!\n\n```javascript\nconst response = await (\n  await fetch(\"/upload\", {\n    method: \"POST\",\n    body: encrypted,\n  })\n).json();\n```\n\nWe 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.\n\n```javascript\nconst objectURL = response.url;\nconst objectKey = (await window.crypto.subtle.exportKey(\"jwk\", key)).k;\nconst url = objectURL + \"#key=\" + objectKey;\n// Example: https://excalidraw.com/?scene=1234#key=BQ1moYESmTEXgtA1KozyVw\n```\n\n## Download\n\nIn the opposite direction, we download the file back from the server.\n\n```javascript\nconst response = await fetch(`/download?id={id}`);\nconst encrypted = await response.arrayBuffer();\n```\n\nThe 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!\n\n```javascript\nconst objectKey = window.location.hash.slice(\"#key=\".length);\nconst key = await window.crypto.subtle.importKey(\n  \"jwk\",\n  {\n    k: objectKey,\n    alg: \"A128GCM\",\n    ext: true,\n    key_ops: [\"encrypt\", \"decrypt\"],\n    kty: \"oct\",\n  },\n  { name: \"AES-GCM\", length: 128 },\n  false, // extractable\n  [\"decrypt\"],\n);\n```\n\nWe decrypt the message, decode it to string and parse it back as JSON.\n\n```javascript\nconst decrypted = await window.crypto.subtle.decrypt(\n  { name: \"AES-GCM\", iv: new Uint8Array(12) },\n  key,\n  encrypted,\n);\nconst decoded = new window.TextDecoder().decode(new Uint8Array(decrypted));\nconst content = JSON.parse(decoded);\n```\n\n## Conclusion\n\nAs 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.\n\nIt also gives me the peace of mind to use **Excalidraw** for work related projects knowing that nothing will leak.\n\nIf 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!\n"
  },
  {
    "path": "content/blog/excalidraw-and-fugu/index.md",
    "content": "---\ntitle: \"Excalidraw and Fugu: Improving Core User Journeys\"\ndate: 2021-05-21\nnote: 'This post appeared first on <a href=\"https://web.dev/excalidraw-and-fugu/\">web.dev</a>.'\nauthor: tomayac\nlink: https://twitter.com/tomayac\nimage: FcDeDjh1bW8zAHIzA2BF\n---\n\nAny 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\n\n<!-- end -->\n\n<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>\n\n## How I came to Excalidraw\n\nI 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.\n\n<img src=\"VbicbA7xj5azVcDUBSKt.png\" alt=\"Screenshot of the Excalidraw prototype application showing that it supported rectangles, arrows, ellipses, and text.\" width=\"800\" height=\"600\">\n\nOn 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:\n\n- 12K unique active users\n- 1.5K stars on GitHub\n- 26 contributors\n\nFor 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.\n\n<img src=\"9VJ9EqPzKdzUpxFeM5wH.png\" alt=\"Screenshot of the tweet where I announce my PR.\" width=\"550\" height=\"424\">\n\nMy 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.\n\nToday, [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.\n\n<img src=\"Wzz6UELRpcvkKZQtmVmc.png\" alt=\"Screenshot of the Excalidraw PWA in today's state.\" width=\"800\" height=\"537\">\n\n## Lipis on why he dedicates so much of his time to Excalidraw\n\nSo 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:\n\n> 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.\n\nI fully agree with lipis. Whoever tried Excalidraw is looking to find excuses to use it again.\n\n## Excalidraw in action\n\nI 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.\n\n<video autoplay loop muted playsinline src=\"wK9jDdHG7A7qT5ViOuEQ.mp4\"></video>\n\nI 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.\n\n<video autoplay loop muted playsinline src=\"HvKcKNk8Q3bbaVe36E3T.mp4\"></video>\n\nLet 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.\n\n<video autoplay loop muted playsinline src=\"XzlUi88cPDYl8YFAH1J8.mp4\"></video>\n\n## Working with files\n\nOn 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.\n\n<video autoplay loop muted playsinline src=\"1oVPIESBNhoL4AhOSNli.mp4\"></video>\n\n### Opening files\n\nSo 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()`.\n\n```js\nexport const loadFromJSON = async (localAppState: AppState) => {\n  const blob = await fileOpen({\n    description: \"Excalidraw files\",\n    extensions: [\".json\", \".excalidraw\", \".png\", \".svg\"],\n    mimeTypes: [\"application/json\", \"image/png\", \"image/svg+xml\"],\n  });\n  return loadFromBlob(blob, localAppState);\n};\n```\n\nThe `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.\n\nLet 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.\n\n```js\nexport default async (options = {}) => {\n  const accept = {};\n  // Not shown: deal with extensions and MIME types.\n  const handleOrHandles = await window.showOpenFilePicker({\n    types: [\n      {\n        description: options.description || \"\",\n        accept: accept,\n      },\n    ],\n    multiple: options.multiple || false,\n  });\n  const files = await Promise.all(handleOrHandles.map(getFileWithHandle));\n  if (options.multiple) return files;\n  return files[0];\n  const getFileWithHandle = async (handle) => {\n    const file = await handle.getFile();\n    file.handle = handle;\n    return file;\n  };\n};\n```\n\nThe 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.\n\n```js\nexport default async (options = {}) => {\n  return new Promise((resolve) => {\n    const input = document.createElement(\"input\");\n    input.type = \"file\";\n    const accept = [\n      ...(options.mimeTypes ? options.mimeTypes : []),\n      options.extensions ? options.extensions : [],\n    ].join();\n    input.multiple = options.multiple || false;\n    input.accept = accept || \"*/*\";\n    input.addEventListener(\"change\", () => {\n      resolve(input.multiple ? Array.from(input.files) : input.files[0]);\n    });\n    input.click();\n  });\n};\n```\n\n### Saving files\n\nNow 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.\n\n```js\nexport const saveAsJSON = async (\n  elements: readonly ExcalidrawElement[],\n  appState: AppState,\n) => {\n  const serialized = serializeAsJSON(elements, appState);\n  const blob = new Blob([serialized], {\n    type: 'application/vnd.excalidraw+json',\n  });\n  const fileHandle = await fileSave(\n    blob,\n    {\n      fileName: appState.name,\n      description: 'Excalidraw file',\n      extensions: ['.excalidraw'],\n    },\n    appState.fileHandle,\n  );\n  return { fileHandle };\n};\n```\n\nAgain 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/).\n\n```js\nexport default async (blob, options = {}, handle = null) => {\n  options.fileName = options.fileName || \"Untitled\";\n  const accept = {};\n  // Not shown: deal with extensions and MIME types.\n  handle =\n    handle ||\n    (await window.showSaveFilePicker({\n      suggestedName: options.fileName,\n      types: [\n        {\n          description: options.description || \"\",\n          accept: accept,\n        },\n      ],\n    }));\n  const writable = await handle.createWritable();\n  await writable.write(blob);\n  await writable.close();\n  return handle;\n};\n```\n\n#### The \"save as\" feature\n\nIf 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.\n\n<video autoplay loop muted playsinline src=\"oTNuosQmoMBP2G7XR8Wb.mp4\"></video>\n\nThe 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.\n\n```js\nexport default async (blob, options = {}) => {\n  const a = document.createElement(\"a\");\n  a.download = options.fileName || \"Untitled\";\n  a.href = URL.createObjectURL(blob);\n  a.addEventListener(\"click\", () => {\n    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);\n  });\n  a.click();\n};\n```\n\nThe 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.\n\n<video autoplay loop muted playsinline src=\"1oVPIESBNhoL4AhOSNli.mp4\"></video>\n\n## Drag and drop\n\nOne 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.\n\n<video autoplay loop muted playsinline src=\"aOPKhOOe20od8uOzehdy.mp4\"></video>\n\nThe 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.\n\n```js\nconst file = event.dataTransfer?.files[0];\nif (file?.type === 'application/json' || file?.name.endsWith('.excalidraw')) {\n  this.setState({ isLoading: true });\n  // Provided by browser-fs-access.\n  if (supported) {\n    try {\n      const item = event.dataTransfer.items[0];\n      file as any.handle = await item as any\n        .getAsFileSystemHandle();\n    } catch (error) {\n      console.warn(error.name, error.message);\n    }\n  }\n  loadFromBlob(file, this.state).then(({ elements, appState }) =>\n    // Load from blob\n  ).catch((error) => {\n    this.setState({ isLoading: false, errorMessage: error.message });\n  });\n}\n```\n\n## Sharing files\n\nAnother 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.\n\n<video autoplay loop muted playsinline src=\"x93JgKGcp1o8at5P7exv.mp4\"></video>\n\n## Lipis on the deprecated Electron version\n\nOne 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.\n\nExcalidraw [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:\n\n> 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!\n\nOne 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!\n\n## File handling\n\nWhen I say \"the web has become good enough\", it's because of features like the upcoming File Handling.\n\nThis 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.\n\n<video autoplay loop muted playsinline src=\"Gz1w0Gey1XerN86sIF01.mp4\"></video>\n\nSo 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.\n\n```json\n{\n  \"name\": \"Excalidraw\",\n  \"description\": \"Excalidraw is a whiteboard tool...\",\n  \"start_url\": \"/\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\",\n  \"file_handlers\": [\n    {\n      \"action\": \"/\",\n      \"accept\": {\n        \"application/vnd.excalidraw+json\": [\".excalidraw\"]\n      }\n    }\n  ]\n}\n```\n\nThe 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()`.\n\n```js\nif ('launchQueue' in window && 'LaunchParams' in window) {\n  window as any.launchQueue\n    .setConsumer(async (launchParams: { files: any[] }) => {\n      if (!launchParams.files.length) return;\n      const fileHandle = launchParams.files[0];\n      const blob: Blob = await fileHandle.getFile();\n      blob.handle = fileHandle;\n      loadFromBlob(blob, this.state).then(({ elements, appState }) =>\n        // Initialize app state.\n      ).catch((error) => {\n        this.setState({ isLoading: false, errorMessage: error.message });\n      });\n    });\n}\n```\n\nAgain, 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.\n\n## Clipboard integration\n\nAnother 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.\n\n<video autoplay loop muted playsinline src=\"EHHQS78y6RJf21J1wD7y.mp4\"></video>\n\nThe 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/).\n\n```js\nexport const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) => {\n  const blob = await canvasToBlob(canvas);\n  await navigator.clipboard.write([\n    new window.ClipboardItem({\n      \"image/png\": blob,\n    }),\n  ]);\n};\n\nexport const canvasToBlob = async (\n  canvas: HTMLCanvasElement,\n): Promise<Blob> => {\n  return new Promise((resolve, reject) => {\n    try {\n      canvas.toBlob((blob) => {\n        if (!blob) {\n          return reject(\n            new CanvasError(\n              t(\"canvasError.canvasTooBig\"),\n              \"CANVAS_POSSIBLY_TOO_BIG\",\n            ),\n          );\n        }\n        resolve(blob);\n      });\n    } catch (error) {\n      reject(error);\n    }\n  });\n};\n```\n\n## Collaborating with others\n\n### Sharing a session URL\n\nDid 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.\n\n<video autoplay loop muted playsinline src=\"7tbl5j0jrVZd3ffxhpoX.mp4\"></video>\n\n### Live collaboration\n\nI 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.\n\nI 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.\n\n<video autoplay loop muted playsinline src=\"7muh13F0CjvKBntVrUTp.mp4\"></video>\n\n### Seeing collaborator statuses\n\nTo 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.\n\n<video autoplay loop muted playsinline src=\"Y7vEI1qHTDJpHNdXjteS.mp4\"></video>\n\nAvid 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.\n\n<img src=\"SudM7tqa3ZooUJYx7aBB.png\" alt=\"Screenshot of the Idle Detection feedback filed on the WICG Idle Detection repo.\" width=\"800\" height=\"685\">\n\nWe 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!\n\n## Lipis on what is holding back Excalidraw\n\nTalking of which, I asked lipis one last question regarding what he thinks is missing from the web platform that holds back Excalidraw:\n\n> 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.\n\n[👆 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.]\n\nI fully agree with lipis, I live in the cloud, too. Here's hoping that this will be implemented soon.\n\n## Tabbed application mode\n\nWow! 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.\n\n<video autoplay loop muted playsinline src=\"h8zrwaB8jBXVnQuxglpS.mp4\"></video>\n\nI 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.\n\nTabbed 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/).\n\n## Closing\n\nTo 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/).\n\nI 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.\n"
  },
  {
    "path": "content/blog/introducing-excalidraw-plus/index.md",
    "content": "---\ntitle: Introducing Excalidraw+\ndate: 2021-05-03\nauthor: Excalidraw Team\nlink: https://twitter.com/excalidraw\nimage: excalidraw-plus.png\n---\n\nExcalidraw 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!\n\n<!-- end -->\n\n<hr/>\n\nExcalidraw, 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!\n\nWhile 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?\n\nWe'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+**.\n\n## Going beyond Excalidraw\n\nIn 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.\n\nWith 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.\n\nIn the coming weeks we'll focus on making collaboration more robust, especially when collaborating in multiple continents.\n\nWe didn't want to keep you waiting any longer, but we are hard at work to bring many more awesome features to Excalidraw+.\n\n**[Try Excalidraw+ for free](https://plus.excalidraw.com/?utm_source=excalidraw&utm_medium=blog&utm_campaign=launch)**, and then for $7/month.\n\n<video src=\"./organize.mp4\" autoplay playsinline loop muted style=\"width: 100%; height: auto;\"></video>\n\n## Going forward\n\nOne 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.\n\nIn 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.\n\nAs for the [excalidraw.com](https://excalidraw.com) website, it will of course continue to be free and actively developed.\n\nWe 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.\n\n## We’re just beginning\n\nThink big, start small. Excalidraw has a bright future ahead, and we extend an invitation for you to come and shape that future with us!\n\n<center>\n<a href=\"https://plus.excalidraw.com/?utm_source=excalidraw&utm_medium=blog&utm_campaign=launch\">Join Excalidraw+</a>\n</center>\n\n<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>\n"
  },
  {
    "path": "content/blog/one-year-of-excalidraw/index.md",
    "content": "---\ntitle: One Year of Excalidraw\ndate: 2021-01-01\nauthor: Excalidraw Team\nlink: https://github.com/orgs/excalidraw/people\nimage: og.png\n---\n\n> It's been kind of a different year, but it was the first year and pretty amazing for [Excalidraw](https://excalidraw.com).\n\n<!-- end -->\n\nExcalidraw 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.\n\nWe are so incredibly proud to have built something that is being used by **20k weekly active people**.\n\nhttps://excalidraw.com/#json=6443031091740672,amxJZJxlZAlUBLADWIukFg\n\n## Tech Stack\n\nExcalidraw 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.\n\nIn 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.\n\nFinally, 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.\n\nhttps://excalidraw.com/#json=6671570797854720,bqkIFBlioHfMMLYHtcdGjA\n\n## Some of our coolest features\n\n### 🤝 Collaboration\n\nWhen 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/).\n\nhttps://twitter.com/Vjeux/status/1238907727906127872\n\n### 🔒 Your data is encrypted\n\nMany 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/).\n\nhttps://excalidraw.com/#json=5645858175451136,8w-G0ZXiOfRYAn7VWpANxw\n\n### 🇺🇳 Translations\n\nIt 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/)!\n\n### 📱 Mobile first\n\nTouch 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.\n\n### 📚 Library\n\nThe 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.\n\nhttps://twitter.com/dbs_sticky/status/1340349749086580736\n\n### 📊 Excalicharts\n\nWhile 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).\n\n### 🏹 Lines and Arrows\n\nPossibly 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).\n\nhttps://twitter.com/excalidraw/status/1292403762427039744\n\nRelated, 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).\n\nhttps://twitter.com/excalidraw/status/1260287781596794880\n\n### 🔄 More powerful editing\n\nWe 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.\n\nhttps://twitter.com/dai_shi/status/1245273872053579776\n\n### #️⃣ Grid and Stats\n\nFrom 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.\n\n### 💾 File system integration and file handling\n\nIn 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.\n\n### ⚙️ Gatsby plugin\n\nWe 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).\n\nhttps://twitter.com/Vjeux/status/1257906664239333376\n\n### 🌒 Dark Mode\n\nTo help protect your eyes, [@xixixao](https://twitter.com/xixixao) added dark mode, effectively turning Excalidraw into an actual blackboard.\n\nhttps://twitter.com/Msieur_Jo/status/1245288337897914373\n\n### 📦 npm package\n\nOne 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.\n\n## Excalidraw in the news\n\n- Featured a few times on Hacker News:\n  - https://news.ycombinator.com/item?id=23525648\n  - https://news.ycombinator.com/item?id=22663435\n  - https://news.ycombinator.com/item?id=25608336\n- As an example on **web.dev** article: https://web.dev/browser-fs-access/\n- A few blog posts:\n  - https://pakstech.com/blog/draw-diagrams/\n  - https://dev.to/ndsn/why-excalidraw-is-mightier-than-the-pen-and-the-sword-329f\n- On Product Hunt: https://www.producthunt.com/posts/excalidraw\n- React Europe talk: https://www.youtube.com/watch?v=fix2-SynPGE\n- Used to illustrate an O'Reilly Book: https://www.amazon.com/dp/1492057096\n\n  https://twitter.com/wietsevenema/status/1253752608671621124\n\n- Another book: https://twitter.com/dchest/status/1264237749642637312\n- Few companies also integrated Excalidraw in the product\n  - [HackerRank](https://blog.hackerrank.com/virtual-whiteboarding-for-system-design-interviews/)\n  - [Lobelia Earth](https://twitter.com/lobeliaearth/status/1275073557484244992)\n\n## Get involved\n\nExcalidraw 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.\n\n## What's next\n\nWe 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. 💕\n\n## Some cool drawings\n\nThe 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.\n\nhttps://twitter.com/LeaVerou/status/1306001020636540934\n\nhttps://twitter.com/_cloudmu/status/1318288738422824962\n\nhttps://twitter.com/elijahmanor/status/1287734485987950592\n\nhttps://twitter.com/_JessicaSachs/status/1337555805609013248\n\nhttps://twitter.com/aqandrew/status/1289275670871252995\n\nhttps://twitter.com/Clainchoupi/status/1321200109707808769\n\nhttps://twitter.com/addyosmani/status/1241801981955420160\n\nhttps://twitter.com/levsthings/status/1224104529324412929\n\nhttps://twitter.com/_RobDominguez/status/1222174661041180673\n\nhttps://twitter.com/duanebester/status/1220561761964675072\n\nhttps://twitter.com/abdellah_js/status/1225755552065769472\n\nhttps://twitter.com/ilyamkin/status/1226609908327514113\n\nhttps://twitter.com/dai_shi/status/1240494226531479552\n\nhttps://twitter.com/bartekci/status/1246270772043296768\n\nhttps://twitter.com/masbagal/status/1247763747755589633\n\nhttps://twitter.com/Pinnassog/status/1247893044231168001\n\nhttps://twitter.com/CandideTech/status/1250454449933426688\n\nhttps://twitter.com/veenusav/status/1251101998184726533\n\nhttps://twitter.com/jeudesprits/status/1264901836970098689\n\nhttps://twitter.com/gitpitch/status/1265627223610056707\n\nhttps://twitter.com/caroso1222/status/1278397651592122371\n\nhttps://twitter.com/pomber/status/1281339741682753542\n\nhttps://twitter.com/Vjeux/status/1282909088733511680\n\nhttps://twitter.com/anas_aito/status/1283487054018600960\n\nhttps://twitter.com/xnimorz/status/1300065301552390146\n\nhttps://twitter.com/patak_js/status/1317097465158553600\n\nhttps://twitter.com/wietsevenema/status/1343593994895417344\n"
  },
  {
    "path": "content/blog/open-colors/index.md",
    "content": "---\ntitle: Open Colors\ndate: 2020-11-10\nnote: Part of a series where we pick apart various libraries and projects we use on Excalidraw.\nauthor: Lipis\nlink: https://twitter.com/lipis\nimage: og.png\n---\n\nOne 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.\n\n<!-- end -->\n\nThe [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).\n\n![Color pickers](color-pickers.png)\n\nI 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.\n\nThey say a picture is worth a thousand words. (Click on the image below to open it in Excalidraw.)\n\nhttps://excalidraw.com/#json=5120999011909632,Y57VloPaA1LSKT4-1NTgNA\n\nEventually 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).\n\n---\n\n**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.\n"
  },
  {
    "path": "content/blog/redesigning-editor-api/index.md",
    "content": "---\ntitle: Rethinking the Component API\ndate: 2023-01-13\nauthor: Excalidraw Team\nlink: https://github.com/orgs/excalidraw/people\n# image: og3.jpg\n---\n\n<!-- end -->\n\nSince 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?\n\nhttps://twitter.com/excalidraw/status/1587483527804854277\n\nThe 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.\n\n![Custom UI on excalidraw.com](./excalidraw-custom-ui.png)\n\nToday, 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.\n\nLet's take a closer look at the changes, and how or why we've implemented them that way.\n\n---\n\nThe 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.\n\n![Main components to customize](./main-components.png)\n\nIt 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.\n\nUp 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.\n\n# Getting rid of render props\n\nWe 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:\n\n```jsx\nimport { Excalidraw, MainMenu, Footer } from '@excalidraw/excalidraw';\nimport { MyCustomButton } from './MyCustomButton';\n\nexport const App = () => (\n  <Excalidraw>\n    <MainMenu>\n      {/* menu items */}\n    </MainMenu>\n    <Footer>\n      <MyCustomButton>\n    </Footer>\n  </Excalidraw>\n)\n```\n\nIn the future, we may even expose plugins as components, so you will end up doing this:\n\n```jsx\nimport { Excalidraw, MinimapPlugin } from \"@excalidraw/excalidraw\";\n\nexport const App = () => (\n  <Excalidraw>\n    <MainMenu>{/* menu items */}</MainMenu>\n    <MinimapPlugin />\n  </Excalidraw>\n);\n```\n\nAt 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.\n\nAnother 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.\n\nBut, 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.\n\n# `<Footer/>`\n\nHere's what the footer looks like in the editor:\n\n![footer area](./footer-area.png)\n\nWe 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.\n\n```jsx\nimport { Excalidraw, Footer } from \"@excalidraw/excalidraw\";\n\nconst App = () => (\n  <Excalidraw>\n    <Footer>\n      <button onClick={() => console.log(\"Clicked!\")}>Click me</button>\n    </Footer>\n  </Excalidraw>\n);\n```\n\nSo 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?\n\nFor 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 :).\n\n![React.Children](./react-children.png)\n\nIn 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:\n\n```tsx\nexport const getReactChildren = <\n  ExpectedChildren extends {\n    [k in string]?: React.ReactNode;\n  }\n>(\n  children: React.ReactNode,\n) => {\n  return React.Children.toArray(children).reduce(\n    (acc: Partial<ExpectedChildren>, child) => {\n      if (React.isValidElement(child)) {\n        acc[child.type.displayName] = child;\n      }\n      return acc;\n    },\n    {},\n  );\n};\n```\n\nIn practice we also validate against expected children names, and render the rest as is.\n\nThis 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.\n\nBut it also has some downsides.\n\nFor 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:\n\n```jsx\nconst MyFooter = () => {\n  return <Footer />;\n};\n\nconst App = () => (\n  <Excalidraw>\n    {/* nope :( */}\n    <MyFooter />\n  </Excalidraw>\n);\n```\n\nLet's move on to the next component.\n\n# `<MainMenu/>`\n\nThe top-left dropdown menu was introduced in the new editor design, and we wanted you to be able to customize it to your needs.\n\nBelow is what the menu looks like on excalidraw.com (left), vs what we render by default in the package (right).\n\n![main menu differences](./main-menu-differences.png)\n\nBut, 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.\n\n```jsx\nimport { Excalidraw, MainMenu } from \"@excalidraw/excalidraw\";\n\nconst App = () => (\n  return (\n    <Excalidraw>\n      <MainMenu>\n        <MainMenu.DefaultItems.LoadScene />\n        <MainMenu.DefaultItems.Export />\n        <MainMenu.DefaultItems.SaveAsImage />\n        <MainMenu.Separator />\n        <MainMenu.Item onSelect={() => alert(\"Hello to you too!\")}>\n          Hello!\n        </MainMenu.Item>\n      </MainMenu>\n    </Excalidraw>\n  );\n);\n```\n\nAs with `Footer`, you'll need to make sure it's the top-level child of the `Excalidraw` component.\n\n# `<WelcomeScreen/>`\n\nAnother 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.\n\nThe 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.\n\n![welcome screen](./welcome-screen-overview.png)\n\nYou 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.\n\nOne 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>`.\n\n```jsx\nimport { Excalidraw, WelcomeScreen } from \"@excalidraw/excalidraw\";\n\nconst App = () => (\n  return (\n    <Excalidraw>\n      <WelcomeScreen>\n        <WelcomeScreen.Hints.ToolbarHint />\n        <WelcomeScreen.Center>\n          <WelcomeScreen.Center.Logo />\n          <WelcomeScreen.Center.Heading>\n            You can draw anything you want!\n          </WelcomeScreen.Center.Heading>\n          <WelcomeScreen.Center.Menu>\n            <WelcomeScreen.Center.MenuItemHelp />\n            <WelcomeScreen.Center.MenuItemLiveCollaborationTrigger\n              onSelect={() => setCollabDialogShown(true)}\n            />\n            {!isExcalidrawPlusSignedUser && (\n              <WelcomeScreen.Center.MenuItem\n                onSelect={() => console.log(\"doing something!\")}\n              >\n                Do something\n              </WelcomeScreen.Center.MenuItem>\n            )}\n          </WelcomeScreen.Center.Menu>\n        </WelcomeScreen.Center>\n      </WelcomeScreen>\n    </Excalidraw>\n  );\n);\n```\n\n# Wrapping up\n\nSo 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)!\n\nFor 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! 💜\n\nIn the meantime, you can also let us know what you'd like us to cover!\n\nhttps://twitter.com/excalidraw/status/1613207731799834625\n"
  },
  {
    "path": "content/blog/reflections-on-excalidraw/index.md",
    "content": "---\ntitle: Reflections on Excalidraw\ndate: 2020-01-15\nnote: 'This post appeared first on <a href=\"https://blog.vjeux.com/2020/uncategorized/reflections-on-excalidraw.html\">Vjeux’s blog</a>.'\nauthor: vjeux\nlink: https://twitter.com/vjeux\nimage: s-curve.png\n---\n\nOn 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/).\n\n<!-- end -->\n\nMany 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.\n\n## S Curve\n\nBefore 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:\n\n- 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\n- the second phase is the exponential part where everything is growing tremendously\n- the third phase is when the growth flattens and you're doing smaller improvements (which can still be huge if the baseline is huge)\n\nThe 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.\n\n![S Curve](s-curve.png)\n\nThe 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.\n\n## Proven Value Proposition\n\nExcalidraw 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.\n\n![Example of image drawn with Zwibbler](zwibbler.png)\n\nSo 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.\n\n## Make Some Noise\n\nThe 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.\n\nhttps://twitter.com/Vjeux/status/1212503324982792193\n\n## Convert Attention to Action\n\nI 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.\n\nPeople that wanted to contribute could just skim through the list of things to be done and start hacking. That worked really well!\n\n![GitHub issues](issues.jpeg)\n\n## Who is Contributing?\n\nWhen 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.\n\nThis 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).\n\n## Keeping People Engaged\n\nThe 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.\n\nFor 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.\n\nI'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.\n\nA 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).\n\n## Be Decisive\n\nPeople 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...).\n\nIf 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.\n\nOn 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.\n\n## Keeper of Quality\n\nWith 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.\n\nEvery 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.\n\n![GitHub Issue](issue.png)\n\nI'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.\n\n## Celebrate Success\n\nPosting 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.\n\nThe 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.\n\nThis 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.\n\n## Empty Canvas\n\nWhat 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!)\n\nI'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.\n\n## Things That Went My Way\n\nI 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.\n\n- 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.\n- 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.\n- 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.\n\n## Conclusion\n\nThis 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!).\n\nNow, 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)\n"
  },
  {
    "path": "content/blog/rethinking-virtual-whiteboard/index.md",
    "content": "---\ntitle: Rethinking Virtual Whiteboard\ndate: 2020-03-28\nauthor: vjeux\nlink: https://twitter.com/vjeux\n---\n\n[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.\n\n<!-- end -->\n\n## Predefined Shapes\n\nHand-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.\n\n![Writing \"whiteboard\" on a physical whiteboard](whiteboard-physical.gif)\n\nWriting 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.\n\n![Writing \"whiteboard\" on Excalidraw](whiteboard-excalidraw.gif)\n\nFortunately, 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.\n\n![Drawing shapes on a physical whiteboard](shapes-physical.gif)\n\nSpeed 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.\n\nBecause those shapes are so commonly drawn, Excalidraw provides them by default. Here's the same diagram drawn with Excalidraw:\n\n![Drawing shapes on Excalidraw](shapes-excalidraw.gif)\n\n## Modifications\n\nI'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.\n\n![Erasing on a physical whiteboard](erase-physical.gif)\n\nExcalidraw lets you select the shapes you've previously drawn and use backspace key to get rid of them.\n\n![Erasing on Excalidraw](erase-excalidraw.gif)\n\nOnce 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.\n\n![Operations on Excalidraw](operations-excalidraw.gif)\n\n## Infinite Canvas\n\nAnother 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...\n\nHave 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?\n\n![Finite physical whiteboard](finite-physical.gif)\n\nIf we remove this physical limitation, I can just scroll a bit and continue the drawing.\n\n![Infinite virtual whiteboard on Excalidraw](infinite-excalidraw.gif)\n\nA 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...\n\n## Conclusion\n\nIf 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...\n\nIf 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.\n"
  },
  {
    "path": "content/blog/tell-your-story-with-charts/index.md",
    "content": "---\ntitle: Tell your story with Charts\ndate: 2020-12-20\nauthor: Lipis\nlink: https://twitter.com/lipis\nimage: og.png\n---\n\nOne 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.\n\n<!-- end -->\n\n## Telling a story\n\nCharts 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.\n\nIn 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...\n\n## Examples\n\n![Copy pasting charts](charts.gif)\n\nCopy 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.\n\nhttps://excalidraw.com/#json=6035723371151360,_YC8ms6v1fhghy3SCLYljQ\n\n| Month | Accounts |\n| ----- | -------: |\n| Jan   |      653 |\n| Feb   |      751 |\n| Mar   |      941 |\n| Apr   |      116 |\n| May   |      828 |\n| Jun   |       85 |\n| Jul   |      169 |\n| Aug   |      666 |\n| Sep   |      127 |\n| Oct   |      484 |\n| Nov   |      288 |\n| Dec   |      687 |\n\nBut 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.\n\nhttps://excalidraw.com/#json=4659903914311680,mBoVCGfah7dPzNI90_8JcA\n\n```\nDay,Commits\nSun,143\nMon,167\nTue,92\nWed,114\nThu,128\nFri,155\nSat,193\n```\n\n## Modifying the chart\n\nOnce 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.\n\nhttps://excalidraw.com/#json=6443031091740672,amxJZJxlZAlUBLADWIukFg\n\n## Implementation\n\nThe 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!\n"
  },
  {
    "path": "content/blog/webex-meetings-integration/index.md",
    "content": "---\ntitle: Introducing Webex Integration\ndate: 2021-10-28\nauthor: Aakansha, David\nlink: https://twitter.com/aakansha1216, https://twitter.com/dluzar\nimage: excalidraw_webex.png\n---\n\nCollaborate using Excalidraw whiteboard directly in your Webex meetings.\n\n<!-- end -->\n\n## Excalidraw ❤️ Meetings\n\nListening 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.\n\n## Excalidraw in Webex\n\nWhether 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.\n\n<video src=\"./webex-blog-promo.mp4\" autoplay playsinline loop muted style=\"width: 100%; height: auto;\"></video>\n\n## How to use\n\nFor 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.\n\nWhen 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).\n\nYour 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).\n\nNote that the collaboration room is active only for the duration of the meeting. Each meeting creates its own room.\n\n## More to come\n\nSome 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).\n\nAlso, 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.\n\nAs 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! ❤️\n\n<div style=\"margin-bottom: 2em; text-align: center\">\nVisit <a href=\"https://webex.excalidraw.com/#how-to-install\" target=\"_blank\" rel=\"noopener noreferer\">webex.excalidraw.com</a> for more.\n</div>\n"
  },
  {
    "path": "content/blog/year-three/index.md",
    "content": "---\ntitle: Three for three\ndate: 2023-01-04\nauthor: Excalidraw Team\nlink: https://github.com/orgs/excalidraw/people\nimage: og3.jpg\n---\n\nHow did Excalidraw fare in its third year of existence, and what's to come? Let's find out!\n\n<!-- end -->\n\nGreetings Excalidraw users! Thank you all for the support throughout the year, and for being a part of our growing user base! ❤️\n\n![users in 2022](./users.jpg)\n\nWith 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!\n\n![bestofjs](./bestofjs2.png)\n\nLet's have a look at what we and you have been up to the past year!\n\n(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)! 🚀)\n\n# What we’ve shipped in the editor\n\nOne 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!\n\nThis 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!\n\nThere 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!\n\nhttps://twitter.com/excalidraw/status/1587483527804854277\n\n### What may have you missed in 2022?\n\nIf 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 👀).\n\n---\n\nFreedrawing is fun, but sometimes it can get annoying if drawing gets slow, or unwanted gestures start happening, especially when using a stylus.\n\nPalm 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.\n\nIn the future we'll want to investigate better palm rejection so that if nothing else, we can re-enable touch-zooming.\n\nhttps://twitter.com/zsviczian/status/1488879818305384449\n\nOther 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.\n\nhttps://twitter.com/excalidraw/status/1491044664731856900\n\nWhile 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).\n\nWe'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.\n\nMobile 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! ❤️\n\nhttps://twitter.com/Biernacki/status/1584990065729888256\n\nDo 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 😋).\n\nSupport for in-diagram linking coming this year!\n\nhttps://twitter.com/aakansha1216/status/1489256535817854977\n\nOne missing feature was to add background to freedraw shapes, which was correct by [Arun](https://twitter.com/node_monk)!\n\nhttps://twitter.com/excalidraw/status/1491456843742605313\n\nUntil 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.\n\nYou'll be able to pick from more shades and better colors in the upcoming color picker. 🎨\n\nhttps://twitter.com/aakansha1216/status/1498292311381655554\n\nNow 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!\n\nhttps://twitter.com/aakansha1216/status/1502296876405891074\n\n[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.\n\nIt'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.\n\nhttps://twitter.com/excalidraw/status/1512104367213658122\n\nLibraries 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:\n\nhttps://twitter.com/excalidraw/status/1539269812253335553\n\nAnd little known feature is the ability to add multiple libraries to your canvas at once:\n\nhttps://twitter.com/excalidraw/status/1524423937542795265\n\nYou 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.\n\nFirst, Aakansha added support for adding a midpoint to lines outside the line editor:\n\nhttps://twitter.com/aakansha1216/status/1557748574712041473\n\nAnd 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.\n\nhttps://twitter.com/excalidraw/status/1570059398881636354\n\n[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.\n\nhttps://twitter.com/excalidraw/status/1600883623980171265\n\nTowards the end of the year, Aakansha added the long awaited ability to add text labels to arrows!\n\nhttps://twitter.com/excalidraw/status/1599792132725669888\n\nBut, 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.\n\nhttps://twitter.com/excalidraw/status/1559205594782990345\n\nWant 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.\n\n# What's happening on the Plus side?\n\nIf 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!\n\nWe 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.\n\n---\n\nWe'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:\n\nhttps://twitter.com/excalidraw/status/1529106689600921600\n\nAn important addition was the introduction of the Dashboard. Get around quickly and view who's drawing right now, and where.\n\nhttps://twitter.com/excalidrawPlus/status/1540091685874634754\n\nAs 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).\n\nWe'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!\n\nhttps://twitter.com/excalidrawPlus/status/1526973455475671040\n\nOn 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?\n\nhttps://twitter.com/excalidraw/status/1559936348508884994\n\nTo 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.\n\nhttps://twitter.com/excalidraw/status/1571886450903220232\n\nNext, we've added support for sorting your collections, and choosing the sort order of your drawings.\n\nhttps://twitter.com/excalidraw/status/1508847388517089292\n\nhttps://twitter.com/excalidrawPlus/status/1549790547606388736\n\nTo please the accountants, we've added yearly billing (it's cheaper, too 😉).\n\nhttps://twitter.com/excalidraw/status/1545109954616508416\n\nCommenting is easier in Plus now, too!\n\nhttps://twitter.com/excalidrawPlus/status/1552670831402225664\n\n# VS Code extension & GitHub\n\nAnother 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). 💜\n\nIf 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!\n\nhttps://twitter.com/excalidraw/status/1511332566216884237\n\nAnd you'll be able to use Excalidraw in the upcoming [GitHub Blocks](https://blocks.githubnext.com/):\n\nhttps://twitter.com/excalidraw/status/1590828212615102464\n\n# Community\n\n## Discord\n\nWe restructured the Excalidraw Discord server to encourage engagement and interaction within our community!\n\nIf 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! 😉\n\n## In the wild\n\nExcalidraw is being used far and wide.\n\nFrom company docs...\n\nhttps://twitter.com/willmcgugan/status/1584174595023609857\n\nTo illustrating whole books!\n\nhttps://twitter.com/Pragmatic_Eng/status/1575167020139970562\n\nOr making awesome illustrations in general:\n\nhttps://twitter.com/jamesspurin/status/1605154429170229249\n\nhttps://twitter.com/victor_bigfield/status/1608796818225127424\n\nAt conferences...\n\nhttps://twitter.com/erikras/status/1522342340664303616\n\nhttps://twitter.com/excalidraw/status/1573305116333338626\n\nLook, here's our Aakansha! ❤️\n\nhttps://twitter.com/aakansha1216/status/1555559465508040705\n\nThose slides must have taken a lot of work! (Or maybe not, with Excalidraw 😋).\n\nhttps://twitter.com/FUSAKLA/status/1538248868885893120\n\nAnd not to forget [Chris's](https://twitter.com/Vjeux) talk at Next.js Conf!\n\nhttps://twitter.com/Vjeux/status/1586062861662507008\n\nExcalidraw 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)!\n\nSpeaking of YouTube, Aakansha recently explained how to build your own Excalidraw over at [Chirag's channel](https://www.youtube.com/watch?v=1lNJVDfsTSo).\n\nhttps://twitter.com/aakansha1216/status/1562817277258371073\n\nYou rock hard at Excalidraw! 🍉\n\n# Coming up\n\n## Excalidraw\n\nLast 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.\n\nWe'll also be focusing on performance so that even huge drawings can be edited smoothly.\n\nAnd we're not forgetting [libraries](https://libraries.excalidraw.com). More on that soon!\n\n## Package\n\nNext 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.\n\nThere 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.\n\n## Plus\n\nFor [Plus](https://plus.excalidraw.com), we'll be making strides on three fronts. Features, friction, and management.\n\nWe'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.\n\nAt 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.\n\nAnd 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.\n\n# Join the Excalidraw team!\n\nAs 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)! 💪\n\nIf 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!\n\n---\n\nIt's going to be a packed year for Excalidraw across the board!\n\nHappy New Year everyone! 🎉\n\nExcalidraw Team\n"
  },
  {
    "path": "content/blog/year-two/index.md",
    "content": "---\ntitle: Year two of Excalidraw\ndate: 2022-01-03\nauthor: Excalidraw Team\nlink: https://github.com/orgs/excalidraw/people\nimage: og.png\n---\n\nExcalidraw celebrated its second birthday! What happened during the last year and what's next?\n\n<!-- end -->\n\nIt'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/).\n\nThe 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!**\n\n![Excalidraw monthly active users](graph-mau.png)\n\nBut it's not just the times. We are hearing far and wide how much you love Excalidraw. Thank you!\n\n# Excalidraw+\n\nProbably the most commonly requested feature from people using Excalidraw was to be able to manage all their drawings and teams.\n\n[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.\n\nhttps://twitter.com/excalidraw/status/1389253752742350858\n\nWe'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. ❤️\n\nWe 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.\n\nIf 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).\n\n# Excalidraw\n\nAs part of our review, let's celebrate and look back on some of the contributions of the past year.\n\n### The editor\n\nWe'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.\n\nTwo of the most heavily requested features were added towards the end of the year.\n\n[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.\n\nhttps://twitter.com/excalidraw/status/1451280455647563783\n\nNeed 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!\n\nhttps://twitter.com/aakansha1216/status/1471509326674030592\n\nThe 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. 🙏\n\nhttps://twitter.com/excalidraw/status/1391443782516740096\n\nAt 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.\n\nhttps://twitter.com/excalidraw/status/1356350401944158211\n\n[Riley Schnee](https://twitter.com/rileyschnee) [imlemented](https://github.com/excalidraw/excalidraw/pull/2520) for object flipping.\n\nhttps://twitter.com/excalidraw/status/1375479950535458819\n\nCreating 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.\n\nhttps://twitter.com/dluzar/status/1470389942262054920\n\n### Keeping things simple\n\nThis 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.\n\nWe'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.\n\nhttps://twitter.com/excalidraw/status/1350099747709841410\n\nThe 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.\n\nRecently 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.\n\nhttps://twitter.com/excalidraw/status/1471126947145072649\n\n### Library improvements\n\nWhile 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.\n\nFirst, 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.\n\nInstalling 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.\n\nAnd Arun [made sure](https://github.com/excalidraw/excalidraw-libraries/pull/106) the library page supports dark theme same as Excalidraw does.\n\nhttps://twitter.com/excalidraw/status/1402374369670832131\n\nMany more goodies are coming next year, but the above made installing already a pleasant experience.\n\nEveryone 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.\n\nTowards 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.\n\nhttps://twitter.com/aakansha1216/status/1461045987678453760\n\n### Collaboration\n\nLive-collaborating with people has been an important pillar right from the start and it remains so.\n\n[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).\n\nhttps://twitter.com/excalidraw/status/1357285585937981441\n\nWe've upgraded the collaboration server for smoother experience, and fixed some annoying bugs like [layers syncing](https://github.com/excalidraw/excalidraw/pull/4076) issues.\n\nMore love will be shown to collaboration support this year!\n\n### Excalidraw for Cisco Webex\n\nWe'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!\n\nRead more in our previous [blog post](/webex-meetings-integration).\n\n### npm improvements\n\nExcalidraw 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).\n\nSince 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).\n\nThere 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).\n\nAmong 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).\n\n# What's next\n\nWhile the last year was big, let's make 2022 bigger still.\n\nTell us your wishes, complaints, or better yet, come [help us](https://github.com/excalidraw/excalidraw/issues) make Excalidraw even better! 🚀\n\nThank you for a great year, and here's to the next one!\n\nExcalidraw Team\n"
  },
  {
    "path": "gatsby-browser.js",
    "content": "// custom typefaces\nimport \"typeface-montserrat\";\nimport \"typeface-merriweather\";\nimport \"./src/blog.css\";\nimport \"./src/code.css\";\nimport \"./src/styles.css\";\n"
  },
  {
    "path": "gatsby-config.js",
    "content": "module.exports = {\n  siteMetadata: {\n    title: \"Excalidraw Blog\",\n    description:\n      \"Get up to speed on the latest news and dive deep into inner workings of Excalidraw\",\n    image: \"/og-image-3.png\",\n    siteUrl: \"https://blog.excalidraw.com\",\n    social: {\n      twitter: \"excalidraw\",\n      github: \"excalidraw\",\n    },\n  },\n  plugins: [\n    {\n      resolve: \"gatsby-source-filesystem\",\n      options: {\n        path: `${__dirname}/content/blog`,\n        name: \"blog\",\n      },\n    },\n    {\n      resolve: \"gatsby-source-filesystem\",\n      options: {\n        path: `${__dirname}/content/assets`,\n        name: \"assets\",\n      },\n    },\n    {\n      resolve: \"gatsby-transformer-remark\",\n      options: {\n        excerpt_separator: \"<!-- end -->\",\n        plugins: [\n          {\n            resolve: \"gatsby-remark-prismjs\",\n            options: {\n              classPrefix: \"language-\",\n              inlineCodeMarker: null,\n              aliases: {},\n              showLineNumbers: false,\n              noInlineHighlight: false,\n              prompt: {\n                user: \"root\",\n                host: \"localhost\",\n                global: false,\n              },\n              escapeEntities: {},\n            },\n          },\n          {\n            resolve: \"gatsby-remark-images\",\n            options: {\n              maxWidth: 590,\n            },\n          },\n          {\n            resolve: \"gatsby-remark-responsive-iframe\",\n            options: {\n              wrapperStyle: \"margin-bottom: 1.0725rem\",\n            },\n          },\n          \"gatsby-remark-prismjs\",\n          \"gatsby-remark-copy-linked-files\",\n          \"gatsby-remark-smartypants\",\n          \"@weknow/gatsby-remark-twitter\",\n          // {\n          //   resolve: \"gatsby-remark-embedder\",\n          //   options: {\n          //     customTransformers: [require(\"gatsby-embedder-excalidraw\")],\n          //   },\n          // },\n        ],\n      },\n    },\n    \"gatsby-plugin-dark-mode\",\n    \"gatsby-transformer-sharp\",\n    \"gatsby-plugin-sitemap\",\n    \"gatsby-plugin-sharp\",\n    {\n      resolve: \"gatsby-plugin-google-analytics\",\n      options: {\n        trackingId: \"UA-387204-13\",\n      },\n    },\n    \"gatsby-plugin-feed\",\n    {\n      resolve: \"gatsby-plugin-manifest\",\n      options: {\n        name: \"Excalidraw Blog\",\n        short_name: \"Excalidraw\",\n        start_url: \"/\",\n        background_color: \"#ffffff\",\n        theme_color: \"#663399\",\n        display: \"minimal-ui\",\n        icon: \"content/assets/logo.png\",\n      },\n    },\n    \"gatsby-plugin-offline\",\n    \"gatsby-plugin-react-helmet\",\n    {\n      resolve: \"gatsby-plugin-typography\",\n      options: {\n        pathToConfigModule: \"src/utils/typography\",\n      },\n    },\n    \"gatsby-plugin-twitter\",\n    \"gatsby-plugin-zeit-now\",\n    {\n      resolve: `gatsby-plugin-canonical-urls`,\n      options: {\n        siteUrl: `https://blog.excalidraw.com`,\n        stripQueryString: true,\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "gatsby-node.js",
    "content": "const path = require(`path`);\nconst { createFilePath } = require(`gatsby-source-filesystem`);\n\nexports.createPages = async ({ graphql, actions }) => {\n  const { createPage } = actions;\n\n  const blogPost = path.resolve(`./src/templates/blog-post.js`);\n  const result = await graphql(\n    `\n      {\n        allMarkdownRemark(\n          sort: { fields: [frontmatter___date], order: DESC }\n          limit: 1000\n        ) {\n          edges {\n            node {\n              fields {\n                slug\n              }\n              frontmatter {\n                title\n              }\n            }\n          }\n        }\n      }\n    `,\n  );\n\n  if (result.errors) {\n    throw result.errors;\n  }\n\n  // Create blog posts pages.\n  const posts = result.data.allMarkdownRemark.edges;\n\n  posts.forEach((post, index) => {\n    const previous = index === posts.length - 1 ? null : posts[index + 1].node;\n    const next = index === 0 ? null : posts[index - 1].node;\n\n    createPage({\n      path: post.node.fields.slug,\n      component: blogPost,\n      context: {\n        slug: post.node.fields.slug,\n        previous,\n        next,\n      },\n    });\n  });\n};\n\nexports.onCreateNode = ({ node, actions, getNode }) => {\n  const { createNodeField } = actions;\n\n  if (node.internal.type === `MarkdownRemark`) {\n    const value = createFilePath({ node, getNode });\n    createNodeField({\n      name: `slug`,\n      node,\n      value,\n    });\n  }\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"author\": \"Christopher Chedeau <vjeuxx@gmail.com>\",\n  \"dependencies\": {\n    \"@weknow/gatsby-remark-twitter\": \"0.2.3\",\n    \"gatsby\": \"2.32.12\",\n    \"gatsby-embedder-excalidraw\": \"1.1.4\",\n    \"gatsby-image\": \"3.11.0\",\n    \"gatsby-plugin-canonical-urls\": \"2.10.0\",\n    \"gatsby-plugin-dark-mode\": \"1.1.2\",\n    \"gatsby-plugin-feed\": \"2.13.1\",\n    \"gatsby-plugin-google-analytics\": \"2.11.0\",\n    \"gatsby-plugin-manifest\": \"2.12.1\",\n    \"gatsby-plugin-offline\": \"3.10.2\",\n    \"gatsby-plugin-react-helmet\": \"3.10.0\",\n    \"gatsby-plugin-sharp\": \"2.14.3\",\n    \"gatsby-plugin-sitemap\": \"2.12.0\",\n    \"gatsby-plugin-twitter\": \"2.10.0\",\n    \"gatsby-plugin-typography\": \"2.12.0\",\n    \"gatsby-plugin-zeit-now\": \"0.3.0\",\n    \"gatsby-remark-copy-linked-files\": \"2.10.0\",\n    \"gatsby-remark-embedder\": \"4.2.0\",\n    \"gatsby-remark-images\": \"3.11.1\",\n    \"gatsby-remark-prismjs\": \"3.13.0\",\n    \"gatsby-remark-responsive-iframe\": \"2.11.0\",\n    \"gatsby-remark-smartypants\": \"2.10.0\",\n    \"gatsby-source-filesystem\": \"2.11.1\",\n    \"gatsby-transformer-remark\": \"2.16.1\",\n    \"gatsby-transformer-sharp\": \"2.12.1\",\n    \"prismjs\": \"1.23.0\",\n    \"react\": \"16.14.0\",\n    \"react-dom\": \"16.14.0\",\n    \"react-helmet\": \"6.1.0\",\n    \"react-typography\": \"0.16.19\",\n    \"typeface-merriweather\": \"1.1.13\",\n    \"typeface-montserrat\": \"1.1.13\",\n    \"typography\": \"0.16.19\",\n    \"typography-theme-wordpress-2016\": \"0.16.19\"\n  },\n  \"description\": \"All the news about Excalidraw\",\n  \"devDependencies\": {\n    \"@excalidraw/prettier-config\": \"1.0.2\",\n    \"husky\": \"4.3.8\",\n    \"lint-staged\": \"10.5.4\",\n    \"prettier\": \"2.2.1\",\n    \"sharp\": \"0.28.1\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"lint-staged\"\n    }\n  },\n  \"license\": \"MIT\",\n  \"lint-staged\": {\n    \"*.{js,jsx,json,md,yml}\": [\n      \"prettier --write\"\n    ]\n  },\n  \"name\": \"excalidraw-blog\",\n  \"prettier\": \"@excalidraw/prettier-config\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"export SET NODE_OPTIONS=--openssl-legacy-provider && gatsby build\",\n    \"develop\": \"gatsby develop\",\n    \"fix\": \"yarn prettier --write\",\n    \"prettier\": \"prettier \\\"**/*.{js,jsx,json,md,yml}\\\"\",\n    \"serve\": \"gatsby serve\",\n    \"start\": \"yarn develop\",\n    \"test\": \"yarn prettier --list-different\"\n  },\n  \"version\": \"1.0.0\",\n  \"packageManager\": \"yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e\"\n}\n"
  },
  {
    "path": "src/blog.css",
    "content": ".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  margin: 4px auto;\n  display: block;\n}\n\n.blog-post table {\n  width: 60%;\n  margin: 0 auto 1.75rem;\n}\n\n.blog-post th {\n  font-weight: 700;\n  font-size: 1.1em;\n  border-bottom-width: 3px;\n}\n\n.blog-post td[align=\"right\"],\n.blog-post th[align=\"right\"] {\n  text-align: right;\n}\n\n.blog-post td[align=\"center\"],\n.blog-post th[align=\"center\"] {\n  text-align: center;\n}\n\n.blog-post td,\n.blog-post th {\n  padding: 4px 0;\n}\n\n.blog-post .twitter-tweet {\n  margin-left: auto;\n  margin-right: auto;\n}\n\nth,\ntd {\n  border-color: #adb5bd;\n}\n\n@media only screen and (max-width: 640px) {\n  .blog-post table {\n    width: 90%;\n  }\n}\n"
  },
  {
    "path": "src/code.css",
    "content": "code[class*=\"language-\"],\npre[class*=\"language-\"] {\n  color: #657b83; /* base00 */\n  font-family: Consolas, Monaco, \"Andale Mono\", \"Ubuntu Mono\", monospace;\n  text-align: left;\n  white-space: pre;\n  word-spacing: normal;\n  word-break: normal;\n  word-wrap: normal;\n\n  line-height: 1.5;\n\n  -moz-tab-size: 4;\n  -o-tab-size: 4;\n  tab-size: 4;\n\n  -webkit-hyphens: none;\n  -ms-hyphens: none;\n  hyphens: none;\n}\n\n/* Code blocks */\npre[class*=\"language-\"] {\n  padding: 1em;\n  margin: 0.5em 0;\n  overflow: auto;\n  border-radius: 0.3em;\n}\n\n:not(pre) > code[class*=\"language-\"],\npre[class*=\"language-\"] {\n  background-color: #fdf6e3; /* base3 */\n}\n\n/* Inline code */\n:not(pre) > code[class*=\"language-\"] {\n  padding: 0.1em;\n  border-radius: 0.3em;\n}\n\n.token.comment,\n.token.prolog,\n.token.doctype,\n.token.cdata {\n  color: #93a1a1; /* base1 */\n}\n\n.token.punctuation {\n  color: #586e75; /* base01 */\n}\n\n.token.namespace {\n  opacity: 0.7;\n}\n\n.token.property,\n.token.tag,\n.token.boolean,\n.token.number,\n.token.constant,\n.token.symbol,\n.token.deleted {\n  color: #268bd2; /* blue */\n}\n\n.token.selector,\n.token.attr-name,\n.token.string,\n.token.char,\n.token.builtin,\n.token.url,\n.token.inserted {\n  color: #2aa198; /* cyan */\n}\n\n.token.entity {\n  color: #657b83; /* base00 */\n  background: #eee8d5; /* base2 */\n}\n\n.token.atrule,\n.token.attr-value,\n.token.keyword {\n  color: #859900; /* green */\n}\n\n.token.function,\n.token.class-name {\n  color: #b58900; /* yellow */\n}\n\n.token.regex,\n.token.important,\n.token.variable {\n  color: #cb4b16; /* orange */\n}\n\n.token.important,\n.token.bold {\n  font-weight: bold;\n}\n.token.italic {\n  font-style: italic;\n}\n\n.token.entity {\n  cursor: help;\n}\n"
  },
  {
    "path": "src/components/Toggle.css",
    "content": "/*\n * Copyright (c) 2015 instructure-react\n * Forked from https://github.com/aaronshaf/react-toggle/\n**/\n\n.react-toggle {\n  touch-action: pan-x;\n  z-index: 1;\n  display: inline-block;\n  position: relative;\n  cursor: pointer;\n  background-color: transparent;\n  border: 0;\n  padding: 0;\n\n  -webkit-touch-callout: none;\n  user-select: none;\n\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n  -webkit-tap-highlight-color: transparent;\n}\n\n.react-toggle-screenreader-only {\n  border: 0;\n  clip: rect(0 0 0 0);\n  height: 1px;\n  margin: -1px;\n  overflow: hidden;\n  padding: 0;\n  position: absolute;\n  width: 1px;\n}\n\n.react-toggle-track {\n  width: 50px;\n  height: 24px;\n  padding: 0;\n  border-radius: 30px;\n  background-color: hsl(222, 14%, 7%);\n  transition: all 0.2s ease;\n}\n\n.react-toggle-track-check {\n  position: absolute;\n  width: 17px;\n  height: 17px;\n  left: 5px;\n  top: 0px;\n  bottom: 0px;\n  margin-top: auto;\n  margin-bottom: auto;\n  line-height: 0;\n  opacity: 0;\n  transition: opacity 0.25s ease;\n}\n\n.react-toggle--checked .react-toggle-track-check {\n  opacity: 1;\n  transition: opacity 0.25s ease;\n}\n\n.react-toggle-track-x {\n  position: absolute;\n  width: 17px;\n  height: 17px;\n  right: 5px;\n  top: 0px;\n  bottom: 0px;\n  margin-top: auto;\n  margin-bottom: auto;\n  line-height: 0;\n  opacity: 1;\n  transition: opacity 0.25s ease;\n}\n\n.react-toggle--checked .react-toggle-track-x {\n  opacity: 0;\n}\n\n.react-toggle-thumb {\n  position: absolute;\n  top: 1px;\n  left: 1px;\n  width: 22px;\n  height: 22px;\n  border-radius: 50%;\n  background-color: #fafafa;\n  box-sizing: border-box;\n  transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;\n  transform: translateX(0);\n}\n\n.react-toggle--checked .react-toggle-thumb {\n  transform: translateX(26px);\n  border-color: #19ab27;\n}\n\n.react-toggle--focus .react-toggle-thumb {\n  /* OC Blue 6 */\n  box-shadow: 0px 0px 2px 3px #228be6;\n}\n\n.react-toggle:active .react-toggle-thumb {\n  /* OC Blue 6 */\n  box-shadow: 0px 0px 5px 5px #228be6;\n}\n"
  },
  {
    "path": "src/components/Toggle.js",
    "content": "/*\n * Copyright (c) 2015 instructure-react\n * Copied From Dan Abramov's bloghttps://raw.githubusercontent.com/gaearon/overreacted.io/f08afa0bbdf7612e855e7ac6aabf47f7f7ab8a04/src/components/Toggle.css\n **/\n\nimport React, { PureComponent } from \"react\";\nimport \"./Toggle.css\";\n\n// Copyright 2015-present Drifty Co.\n// http://drifty.com/\n// from: https://github.com/driftyco/ionic/blob/master/src/util/dom.ts\nfunction pointerCoord(event) {\n  // get coordinates for either a mouse click\n  // or a touch depending on the given event\n  if (event) {\n    const changedTouches = event.changedTouches;\n    if (changedTouches && changedTouches.length > 0) {\n      const touch = changedTouches[0];\n      return { x: touch.clientX, y: touch.clientY };\n    }\n    const pageX = event.pageX;\n    if (pageX !== undefined) {\n      return { x: pageX, y: event.pageY };\n    }\n  }\n  return { x: 0, y: 0 };\n}\n\nexport default class Toggle extends PureComponent {\n  constructor(props) {\n    super(props);\n    this.handleClick = this.handleClick.bind(this);\n    this.handleTouchStart = this.handleTouchStart.bind(this);\n    this.handleTouchMove = this.handleTouchMove.bind(this);\n    this.handleTouchEnd = this.handleTouchEnd.bind(this);\n    this.handleTouchCancel = this.handleTouchCancel.bind(this);\n    this.handleFocus = this.handleFocus.bind(this);\n    this.handleBlur = this.handleBlur.bind(this);\n    this.previouslyChecked = !!(props.checked || props.defaultChecked);\n    this.state = {\n      checked: !!(props.checked || props.defaultChecked),\n      hasFocus: false,\n    };\n  }\n\n  componentWillReceiveProps(nextProps) {\n    if (\"checked\" in nextProps) {\n      this.setState({ checked: !!nextProps.checked });\n      this.previouslyChecked = !!nextProps.checked;\n    }\n  }\n\n  handleClick(event) {\n    const checkbox = this.input;\n    this.previouslyChecked = checkbox.checked;\n    if (event.target !== checkbox && !this.moved) {\n      event.preventDefault();\n      checkbox.focus();\n      checkbox.click();\n      return;\n    }\n\n    this.setState({ checked: checkbox.checked });\n  }\n\n  handleTouchStart(event) {\n    this.startX = pointerCoord(event).x;\n    this.touchStarted = true;\n    this.hadFocusAtTouchStart = this.state.hasFocus;\n    this.setState({ hasFocus: true });\n  }\n\n  handleTouchMove(event) {\n    if (!this.touchStarted) return;\n    this.touchMoved = true;\n\n    if (this.startX != null) {\n      let currentX = pointerCoord(event).x;\n      if (this.state.checked && currentX + 15 < this.startX) {\n        this.setState({ checked: false });\n        this.startX = currentX;\n      } else if (!this.state.checked && currentX - 15 > this.startX) {\n        this.setState({ checked: true });\n        this.startX = currentX;\n      }\n    }\n  }\n\n  handleTouchEnd(event) {\n    if (!this.touchMoved) return;\n    const checkbox = this.input;\n    event.preventDefault();\n\n    if (this.startX != null) {\n      if (this.previouslyChecked !== this.state.checked) {\n        checkbox.click();\n      }\n\n      this.touchStarted = false;\n      this.startX = null;\n      this.touchMoved = false;\n    }\n\n    if (!this.hadFocusAtTouchStart) {\n      this.setState({ hasFocus: false });\n    }\n  }\n\n  handleTouchCancel(event) {\n    if (this.startX != null) {\n      this.touchStarted = false;\n      this.startX = null;\n      this.touchMoved = false;\n    }\n\n    if (!this.hadFocusAtTouchStart) {\n      this.setState({ hasFocus: false });\n    }\n  }\n\n  handleFocus(event) {\n    const { onFocus } = this.props;\n\n    if (onFocus) {\n      onFocus(event);\n    }\n\n    this.hadFocusAtTouchStart = true;\n    this.setState({ hasFocus: true });\n  }\n\n  handleBlur(event) {\n    const { onBlur } = this.props;\n\n    if (onBlur) {\n      onBlur(event);\n    }\n\n    this.hadFocusAtTouchStart = false;\n    this.setState({ hasFocus: false });\n  }\n\n  getIcon(type) {\n    const { icons } = this.props;\n    if (!icons) {\n      return null;\n    }\n    return icons[type] === undefined\n      ? Toggle.defaultProps.icons[type]\n      : icons[type];\n  }\n\n  render() {\n    const { className, icons: _icons, ...inputProps } = this.props;\n    const classes =\n      \"react-toggle\" +\n      (this.state.checked ? \" react-toggle--checked\" : \"\") +\n      (this.state.hasFocus ? \" react-toggle--focus\" : \"\") +\n      (this.props.disabled ? \" react-toggle--disabled\" : \"\") +\n      (className ? \" \" + className : \"\");\n    return (\n      <div\n        className={classes}\n        onClick={this.handleClick}\n        onTouchStart={this.handleTouchStart}\n        onTouchMove={this.handleTouchMove}\n        onTouchEnd={this.handleTouchEnd}\n        onTouchCancel={this.handleTouchCancel}\n      >\n        <div className=\"react-toggle-track\">\n          <div className=\"react-toggle-track-check\">\n            {this.getIcon(\"checked\")}\n          </div>\n          <div className=\"react-toggle-track-x\">\n            {this.getIcon(\"unchecked\")}\n          </div>\n        </div>\n        <div className=\"react-toggle-thumb\" />\n\n        <input\n          {...inputProps}\n          ref={(ref) => {\n            this.input = ref;\n          }}\n          onFocus={this.handleFocus}\n          onBlur={this.handleBlur}\n          className=\"react-toggle-screenreader-only\"\n          type=\"checkbox\"\n          aria-label=\"Switch between Dark and Light mode\"\n        />\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "src/components/excalidraw.js",
    "content": "import React from \"react\";\n\nimport logoPath from \"../../content/assets/logo.png\";\nimport { rhythm } from \"../utils/typography\";\n\nconst Excalidraw = () => {\n  const logo = (\n    <img\n      src={logoPath}\n      style={{\n        height: rhythm(1),\n        verticalAlign: \"middle\",\n        paddingRight: rhythm(0.05),\n        margin: 0,\n      }}\n      alt=\"excalidraw\"\n    />\n  );\n  return (\n    <a\n      className=\"excalidraw-button\"\n      style={{\n        padding: `${rhythm(0.4)} ${rhythm(0.5)} ${rhythm(0.4)} ${rhythm(0.35)}`,\n      }}\n      href=\"https://excalidraw.com\"\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n    >\n      {logo} Open Excalidraw\n    </a>\n  );\n};\n\nexport default Excalidraw;\n"
  },
  {
    "path": "src/components/layout.js",
    "content": "import { Link } from \"gatsby\";\nimport { ThemeToggler } from \"gatsby-plugin-dark-mode\";\nimport React from \"react\";\nimport moon from \"../assets/moon.png\";\nimport sun from \"../assets/sun.png\";\nimport { rhythm } from \"../utils/typography\";\nimport Excalidraw from \"./excalidraw\";\nimport \"./layoutStyles.css\";\nimport Toggle from \"./Toggle\";\n\nconst Layout = ({ location, title, children, parentClassName }) => {\n  const rootPath = `${__PATH_PREFIX__}/`;\n  return (\n    <div\n      style={{\n        marginLeft: `auto`,\n        marginRight: `auto`,\n        maxWidth: rhythm(26),\n        padding: `${rhythm(1.5)} ${rhythm(3 / 4)}`,\n      }}\n    >\n      <div\n        style={{\n          position: \"fixed\",\n          right: \"8px\",\n          top: \"8px\",\n        }}\n      >\n        <ThemeToggler>\n          {({ theme, toggleTheme }) => {\n            return (\n              <Toggle\n                icons={{\n                  checked: (\n                    <img\n                      src={moon}\n                      width=\"16\"\n                      height=\"16\"\n                      alt=\"presentation\"\n                      style={{ pointerEvents: \"none\" }}\n                    />\n                  ),\n                  unchecked: (\n                    <img\n                      src={sun}\n                      width=\"16\"\n                      height=\"16\"\n                      alt=\"presentation\"\n                      style={{ pointerEvents: \"none\" }}\n                    />\n                  ),\n                }}\n                checked={theme === \"dark\"}\n                onChange={() => {\n                  toggleTheme(theme === \"light\" ? \"dark\" : \"light\");\n                }}\n              />\n            );\n          }}\n        </ThemeToggler>\n      </div>\n      <header>\n        <div style={{ fontFamily: \"var(--ui-font)\", textAlign: \"right\" }}>\n          {location.pathname !== rootPath && (\n            <span style={{ float: \"left\" }}>\n              <Link to=\"/\">All posts</Link>\n            </span>\n          )}\n          <Excalidraw />\n        </div>\n        <span style={{ clear: \"both\" }} />\n        {location.pathname === rootPath ? <h1>{title}</h1> : null}\n      </header>\n      <main className={parentClassName}>{children}</main>\n      <footer\n        style={{\n          textAlign: \"center\",\n          padding: `${rhythm(2)} 0`,\n        }}\n      >\n        <span>\n          © {new Date().getFullYear()} Excalidraw\n          {\" • \"}\n          <a href=\"https://twitter.com/excalidraw\">Twitter</a>\n          {\" • \"}\n          <a href=\"https://github.com/excalidraw/excalidraw-blog\">\n            Source Code\n          </a>\n          {\" • \"}\n          <a href=\"https://github.com/excalidraw/excalidraw-blog/blob/master/LICENSE\">\n            MIT Licensed\n          </a>\n        </span>\n      </footer>\n    </div>\n  );\n};\n\nexport default Layout;\n"
  },
  {
    "path": "src/components/layoutStyles.css",
    "content": "body {\n  --bg: #fff;\n  /* OC Gray 9 */\n  --textNormal: #212529;\n  --textTitle: #212529;\n  /* OC Blue 6 */\n  --textLink: #228be6;\n  --hr: hsla(0, 0%, 0%, 0.2);\n\n  background-color: var(--bg);\n  font-family: system-ui, sans-serif;\n}\n\nbody.dark {\n  -webkit-font-smoothing: antialiased;\n  /* OC Grey 9 */\n  --bg: #212529;\n  --textNormal: #dbe4ff;\n  --textTitle: #fff;\n  --hr: hsla(0, 0%, 100%, 0.2);\n}\n\nbody.dark h1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  color: var(--textTitle);\n}\n\nh1,\nh2,\nh3 {\n  line-height: 1.4;\n}\n\nh1, h1 code {\n  font-size: 2.5rem;\n}\n\nbody.dark,\nul,\np,\nblockquote {\n  color: var(--textNormal);\n}\n\nblockquote {\n  border-left-color: var(--textNormal);\n}\n\nfigcaption {\n  text-align: center;\n  font-size: 0.9rem;\n}\n\nbody.dark a {\n  color: var(--textLink);\n}\n"
  },
  {
    "path": "src/components/seo.js",
    "content": "/**\n * SEO component that queries for data with\n *  Gatsby's useStaticQuery React hook\n *\n * See: https://www.gatsbyjs.org/docs/use-static-query/\n */\n\nimport { graphql, useStaticQuery } from \"gatsby\";\nimport React from \"react\";\nimport Helmet from \"react-helmet\";\n\nconst SEO = ({ description = \"\", lang = \"en\", meta = [], title, image }) => {\n  const { site } = useStaticQuery(\n    graphql`\n      query SEO {\n        site {\n          siteMetadata {\n            title\n            description\n            image\n            siteUrl\n          }\n        }\n      }\n    `,\n  );\n\n  const metaDescription = description || site.siteMetadata.description;\n  const metaTitle = title\n    ? `${title} | ${site.siteMetadata.title}`\n    : site.siteMetadata.title;\n  let metaImage = image || site.siteMetadata.image;\n  if (!metaImage.includes(\"http\")) {\n    metaImage = `${site.siteMetadata.siteUrl}${metaImage}`;\n  }\n\n  return (\n    <Helmet\n      htmlAttributes={{\n        lang,\n      }}\n      title={metaTitle}\n      meta={[\n        {\n          name: \"description\",\n          content: metaDescription,\n        },\n        {\n          property: \"og:title\",\n          content: metaTitle,\n        },\n        {\n          property: \"og:description\",\n          content: metaDescription,\n        },\n        {\n          property: \"og:image\",\n          content: metaImage,\n        },\n        {\n          property: \"og:type\",\n          content: \"website\",\n        },\n        {\n          name: \"twitter:card\",\n          content: \"summary_large_image\",\n        },\n        {\n          name: \"twitter:title\",\n          content: metaTitle,\n        },\n        {\n          name: \"twitter:description\",\n          content: metaDescription,\n        },\n        {\n          name: \"twitter:image\",\n          content: metaImage,\n        },\n      ].concat(meta)}\n    />\n  );\n};\n\nexport default SEO;\n"
  },
  {
    "path": "src/pages/404.js",
    "content": "import React from \"react\";\nimport { graphql } from \"gatsby\";\n\nimport Layout from \"../components/layout\";\nimport SEO from \"../components/seo\";\n\nconst NotFoundPage = ({ data, location }) => {\n  const siteTitle = data.site.siteMetadata.title;\n\n  return (\n    <Layout location={location} title={siteTitle}>\n      <SEO title=\"404: Not Found\" />\n      <h1>Not Found</h1>\n      <p>You just hit a route that doesn&#39;t exist... the sadness.</p>\n    </Layout>\n  );\n};\n\nexport default NotFoundPage;\n\nexport const pageQuery = graphql`\n  query NotFoundPage {\n    site {\n      siteMetadata {\n        title\n      }\n    }\n  }\n`;\n"
  },
  {
    "path": "src/pages/index.js",
    "content": "import React from \"react\";\nimport { Link, graphql } from \"gatsby\";\n\nimport Layout from \"../components/layout\";\nimport SEO from \"../components/seo\";\nimport { rhythm } from \"../utils/typography\";\n\nfunction BlogIndex({ data, location }) {\n  const posts = data.allMarkdownRemark.edges;\n  const title = data.site.siteMetadata.title;\n\n  return (\n    <Layout location={location} title={title}>\n      <SEO />\n      {posts.map(({ node }) => {\n        const title = node.frontmatter.title || node.fields.slug;\n        const authors =\n          node.frontmatter.author\n            ?.split(\",\")\n            .filter((author) => author.trim()) || [];\n        const authorLinks =\n          node.frontmatter.link?.split(\",\").filter((link) => link.trim()) || [];\n\n        return (\n          <div key={node.fields.slug}>\n            <h3\n              style={{\n                marginBottom: rhythm(1 / 4),\n              }}\n            >\n              <Link style={{ boxShadow: \"none\" }} to={node.fields.slug}>\n                {title}\n              </Link>\n            </h3>\n            <p style={{ marginBottom: \"4px\" }}>\n              <strong>{node.frontmatter.date}</strong>\n              {authors.length && (\n                <span style={{ opacity: 0.75, fontStyle: \"italic\" }}>\n                  {\", by \"}\n                  {authors.map((author, idx) => (\n                    <>\n                      {authorLinks[idx] || authorLinks[0] ? (\n                        <a href={authorLinks[idx] || authorLinks[0]}>\n                          {author}\n                        </a>\n                      ) : (\n                        <>{author}</>\n                      )}\n                      {idx < authors.length - 1 && \", \"}\n                    </>\n                  ))}\n                </span>\n              )}\n            </p>\n            <p style={{ opacity: 0.75 }}>{node.excerpt}</p>\n          </div>\n        );\n      })}\n    </Layout>\n  );\n}\n\nexport default BlogIndex;\n\nexport const pageQuery = graphql`\n  query BlogIndex {\n    site {\n      siteMetadata {\n        title\n        description\n      }\n    }\n    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {\n      edges {\n        node {\n          excerpt\n          fields {\n            slug\n          }\n          frontmatter {\n            date(formatString: \"MMMM DD, YYYY\")\n            title\n            author\n            link\n          }\n        }\n      }\n    }\n  }\n`;\n"
  },
  {
    "path": "src/styles.css",
    "content": ".excalidraw-button {\n  background-color: #fff;\n  border-radius: 8px;\n  border: 1px solid #868e96;\n  box-shadow: none;\n  font-weight: 600;\n  transition: all 0.2s;\n}\n.excalidraw-button:hover {\n  background-color: #f1f3f5;\n}\nsvg {\n  max-width: 100%;\n  height: initial;\n  width: initial;\n}\nvideo {\n  max-width: 100%;\n}\n"
  },
  {
    "path": "src/templates/blog-post.js",
    "content": "import { graphql, Link } from \"gatsby\";\nimport React from \"react\";\nimport Layout from \"../components/layout\";\nimport SEO from \"../components/seo\";\nimport { rhythm } from \"../utils/typography\";\n\nfunction BlogPostTemplate({ data, pageContext: { previous, next }, location }) {\n  const post = data.markdownRemark;\n  const siteTitle = data.site.siteMetadata.title;\n  const editUrl = `https://github.com/excalidraw/excalidraw-blog/edit/master/${post.fileAbsolutePath.substr(\n    post.fileAbsolutePath.indexOf(\"content/blog\"),\n  )}`;\n  const discussUrl = `https://mobile.twitter.com/search?q=${encodeURIComponent(\n    `https://blog.excalidraw.com${post.fields.slug}`,\n  )}`;\n\n  const authors =\n    post.frontmatter.author?.split(\",\").filter((author) => author.trim()) || [];\n  const authorLinks =\n    post.frontmatter.link?.split(\",\").filter((link) => link.trim()) || [];\n\n  let postHTML = post.html;\n\n  if (postHTML.includes(\"<!-- end -->\")) {\n    postHTML = postHTML.split(\"<!-- end -->\")[1];\n  }\n\n  return (\n    <Layout location={location} title={siteTitle} parentClassName={\"blog-post\"}>\n      <SEO\n        title={post.frontmatter.title}\n        description={post.excerpt}\n        image={post.frontmatter.image && post.frontmatter.image.publicURL}\n      />\n      <h1\n        style={{\n          marginBottom: 0,\n        }}\n      >\n        {post.frontmatter.title}\n      </h1>\n      <p\n        style={{\n          marginBottom: rhythm(1),\n          fontFamily: \"var(--ui-font)\",\n        }}\n      >\n        <strong>{post.frontmatter.date}</strong>\n        {authors.length && (\n          <span style={{ opacity: 0.75, fontStyle: \"italic\" }}>\n            {\", by \"}\n            {authors.map((author, idx) => (\n              <>\n                {authorLinks[idx] || authorLinks[0] ? (\n                  <a href={authorLinks[idx] || authorLinks[0]}>{author}</a>\n                ) : (\n                  <>{author}</>\n                )}\n                {idx < authors.length - 1 && \", \"}\n              </>\n            ))}\n          </span>\n        )}\n        {post.frontmatter.note ? (\n          <>\n            {\" • \"}\n            <span dangerouslySetInnerHTML={{ __html: post.frontmatter.note }} />\n          </>\n        ) : null}\n      </p>\n      <div dangerouslySetInnerHTML={{ __html: postHTML }} />\n      <p style={{ fontFamily: \"var(--ui-font)\", marginBottom: 0 }}>\n        <a href={discussUrl}>Discuss on Twitter</a>\n        {\" • \"}\n        <a href={editUrl}>Edit on GitHub</a>\n      </p>\n      {previous || next ? (\n        <>\n          <hr\n            style={{\n              margin: `${rhythm(1)} 0`,\n            }}\n          />\n          <ul\n            style={{\n              display: `flex`,\n              flexWrap: `wrap`,\n              justifyContent: `space-between`,\n              listStyle: `none`,\n              padding: 0,\n            }}\n          >\n            <li>\n              {previous && (\n                <Link to={previous.fields.slug} rel=\"prev\">\n                  ← {previous.frontmatter.title}\n                </Link>\n              )}\n            </li>\n            <li>\n              {next && (\n                <Link to={next.fields.slug} rel=\"next\">\n                  {next.frontmatter.title} →\n                </Link>\n              )}\n            </li>\n          </ul>\n        </>\n      ) : null}\n    </Layout>\n  );\n}\n\nexport default BlogPostTemplate;\n\nexport const pageQuery = graphql`\n  query BlogPostBySlug($slug: String!) {\n    site {\n      siteMetadata {\n        title\n      }\n    }\n    markdownRemark(fields: { slug: { eq: $slug } }) {\n      id\n      excerpt\n      html\n      fileAbsolutePath\n      frontmatter {\n        title\n        date(formatString: \"MMMM DD, YYYY\")\n        note\n        author\n        link\n        image {\n          id\n          publicURL\n        }\n      }\n      fields {\n        slug\n      }\n    }\n  }\n`;\n"
  },
  {
    "path": "src/utils/typography.js",
    "content": "import Typography from \"typography\";\nimport Wordpress2016 from \"typography-theme-wordpress-2016\";\n\nWordpress2016.headerFontFamily = \"BlinkMacSystemFont, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif\".split(\n  \", \",\n);\nWordpress2016.headerWeight = 700;\n\nWordpress2016.overrideThemeStyles = () => {\n  return {\n    \":root\": {\n      \"--ui-font\": Wordpress2016.headerFontFamily.join(\",\"),\n    },\n    h1: {\n      fontFamily: \"var(--ui-font)\",\n      fontWeight: 700,\n    },\n\n    \"a.gatsby-resp-image-link\": {\n      boxShadow: \"none\",\n    },\n    \".gatsby-highlight\": {\n      fontSize: \"0.9em\",\n    },\n  };\n};\n\ndelete Wordpress2016.googleFonts;\n\nconst typography = new Typography(Wordpress2016);\n\n// Hot reload typography in development.\nif (process.env.NODE_ENV !== \"production\") {\n  typography.injectStyles();\n}\n\nexport default typography;\nexport const rhythm = typography.rhythm;\nexport const scale = typography.scale;\n"
  },
  {
    "path": "static/robots.txt",
    "content": "User-agent: *\nDisallow:\n"
  },
  {
    "path": "vercel.json",
    "content": "{\n  \"cleanUrls\": true,\n  \"redirects\": [\n    {\n      \"source\": \"/sitemap.xml\",\n      \"destination\": \"https://plus.excalidraw.com/blog-sitemap.xml\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/\",\n      \"destination\": \"https://plus.excalidraw.com/blog\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/(.*)/\",\n      \"destination\": \"https://plus.excalidraw.com/blog/$1\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/(.*)\",\n      \"destination\": \"https://plus.excalidraw.com/blog/$1\",\n      \"permanent\": true\n    }\n  ]\n}\n"
  }
]