[
  {
    "path": ".changeset/config.json",
    "content": "{\n    \"$schema\": \"https://unpkg.com/@changesets/config@3.0.0/schema.json\",\n    \"changelog\": [\n        \"@changesets/changelog-github\",\n        {\n            \"repo\": \"steven-tey/novel\"\n        }\n    ],\n    \"commit\": false,\n    \"fixed\": [],\n    \"linked\": [],\n    \"access\": \"public\",\n    \"baseBranch\": \"main\",\n    \"updateInternalDependencies\": \"patch\",\n    \"ignore\": [\n        \"novel-next-app\"\n    ]\n}"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: andrewdoro\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 🐞 Bug Report\ndescription: Create a bug report to help us improve\ntitle: \"bug: \"\nlabels: [\"🐞❔ unconfirmed bug\"]\nbody:\n  - type: textarea\n    attributes:\n      label: Provide environment information\n      description: |\n        Run this command in your project root and paste the results in a code block:\n        ```bash\n        npx envinfo --system --binaries\n        ```\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Describe the bug\n      description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Link to reproduction\n      description: Please provide a link to a reproduction of the bug. Issues without a reproduction repo may be ignored.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: To reproduce\n      description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Additional information\n      description: Add any other information related to the bug here, screenshots if applicable.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "# This template is heavily inspired by the Next.js's template:\n# See here: https://github.com/vercel/next.js/tree/canary/.github/ISSUE_TEMPLATE\n\nname: 🛠 Feature Request\ndescription: Create a feature request for the core packages\ntitle: \"feat: \"\nlabels: [\"✨ enhancement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for taking the time to file a feature request. Please fill out this form as completely as possible.\n  - type: textarea\n    attributes:\n      label: Describe the feature you'd like to request\n      description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Describe the solution you'd like to see\n      description: Please describe the solution you would like to see. Adding example usage is a good way to provide context.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Additional information\n      description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here.\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "# This workflow will release the packages with Changesets\n\nname: 🚀 Release\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\nenv:\n  GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}\n  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n\njobs:\n  release:\n    name: 🚀 Release\n    strategy:\n      matrix:\n        os: [ubuntu-latest]\n        node-version: [lts/*]\n        pnpm-version: [latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: ⬇️ Checkout\n        id: checkout\n        uses: actions/checkout@v2.3.3\n        with:\n          token: ${{ env.GITHUB_TOKEN }}\n          fetch-depth: 0\n\n      - name: 🟢 Setup node\n        id: setup-node\n        uses: actions/setup-node@v2\n        with:\n          node-version: ${{ matrix.node-version }}\n\n      - name: 🥡 Setup pnpm\n        id: setup-pnpm\n        uses: pnpm/action-setup@v2.1.0\n        with:\n          version: ${{ matrix.pnpm-version }}\n          run_install: false\n\n      - name: 🎈 Get pnpm store directory\n        id: get-pnpm-cache-dir\n        run: |\n          echo \"::set-output name=pnpm_cache_dir::$(pnpm store path)\"\n\n      - name: 🔆 Cache pnpm modules\n        uses: actions/cache@v3\n        id: pnpm-cache\n        with:\n          path: ${{ steps.get-pnpm-cache-dir.outputs.pnpm_cache_dir }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - name: 🧩 Install Dependencies\n        id: install-dependencies\n        run: pnpm install\n\n      - name: 🏗️ Build\n        id: build-the-mono-repo\n        run: pnpm build\n\n      - name: 📣 Create Release Pull Request or Publish to npm\n        id: changesets\n        uses: changesets/action@v1\n        with:\n          title: \"chore(release): version packages 🦋\"\n          publish: pnpm publish:packages\n          version: pnpm version:packages\n          commit: \"chore(release): version packages 🦋 [skip ci]\"\n        env:\n          GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}\n          NPM_TOKEN: ${{ env.NPM_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\nnode_modules\npackages/*/node_modules\napps/*/node_modules\n.next\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\n.pnpm-debug.log*\n\n# other lockfiles that's not pnpm-lock.yaml\npackage-lock.json\nyarn.lock\n\n# local env files\n.env\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n\n\n# intellij\n.idea\n\ndist/**\n/dist\npackages/*/dist\n\n.turbo\n/test-results/\n/playwright-report/\n/playwright/.cache/\n"
  },
  {
    "path": ".husky/commit-msg",
    "content": "pnpm commitlint --edit $1\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n    \"recommendations\": [\n      \"yoavbls.pretty-ts-errors\",\n      \"bradlc.vscode-tailwindcss\",\n      \"biomejs.biome\"\n    ]\n  }\n  "
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"editor.codeActionsOnSave\": {\n    \"source.organizeImports.biome\": \"explicit\",\n    \"source.fixAll.biome\": \"explicit\",\n    // \"quickfix.biome\": \"explicit\"\n  },\n  \"editor.defaultFormatter\": \"biomejs.biome\",\n  \"editor.formatOnSave\": true,\n  \"tailwindCSS.experimental.classRegex\": [\n    [\"cva\\\\(([^)]*)\\\\)\", \"[\\\"'`]([^\\\"'`]*).*?[\\\"'`]\"],\n    [\"cx\\\\(([^)]*)\\\\)\", \"(?:'|\\\"|`)([^']*)(?:'|\\\"|`)\"]\n  ],\n  \"typescript.enablePromptUseWorkspaceTsdk\": true,\n  \"typescript.tsdk\": \"node_modules/typescript/lib\",\n\n  \"typescript.preferences.autoImportFileExcludePatterns\": [\n    \"next/router.d.ts\",\n    \"next/dist/client/router.d.ts\"\n  ],\n  \"[typescriptreact]\": {\n    \"editor.defaultFormatter\": \"biomejs.biome\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"biomejs.biome\"\n  },\n  \"[json]\": {\n    \"editor.defaultFormatter\": \"vscode.json-language-features\"\n  }\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<a href=\"https://novel.sh\">\n  <img alt=\"Novel is a Notion-style WYSIWYG editor with AI-powered autocompletions.\" src=\"https://novel.sh/opengraph-image.png\">\n  <h1 align=\"center\">Novel</h1>\n</a>\n\n<p align=\"center\">\n  An open-source Notion-style WYSIWYG editor with AI-powered autocompletions. \n</p>\n\n<p align=\"center\">\n  <a href=\"https://news.ycombinator.com/item?id=36360789\"><img src=\"https://img.shields.io/badge/Hacker%20News-369-%23FF6600\" alt=\"Hacker News\"></a>\n  <a href=\"https://github.com/steven-tey/novel/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/github/license/steven-tey/novel?label=license&logo=github&color=f80&logoColor=fff\" alt=\"License\" />\n  </a>\n  <a href=\"https://github.com/steven-tey/novel\"><img src=\"https://img.shields.io/github/stars/steven-tey/novel?style=social\" alt=\"Novel.sh's GitHub repo\"></a>\n</p>\n\n<p align=\"center\">\n  <a href=\"#introduction\"><strong>Introduction</strong></a> ·\n  <a href=\"#deploy-your-own\"><strong>Deploy Your Own</strong></a> ·\n  <a href=\"#setting-up-locally\"><strong>Setting Up Locally</strong></a> ·\n  <a href=\"#tech-stack\"><strong>Tech Stack</strong></a> ·\n  <a href=\"#contributing\"><strong>Contributing</strong></a> ·\n  <a href=\"#license\"><strong>License</strong></a>\n</p>\n<br/>\n\n## Docs (WIP)\n\nhttps://novel.sh/docs/introduction\n\n## Introduction\n\n[Novel](https://novel.sh/) is a Notion-style WYSIWYG editor with AI-powered autocompletions.\n\nhttps://github.com/steven-tey/novel/assets/28986134/2099877f-4f2b-4b1c-8782-5d803d63be5c\n\n<br />\n\n## Deploy Your Own\n\nYou can deploy your own version of Novel to Vercel with one click:\n\n[![Deploy with Vercel](https://vercel.com/button)](https://stey.me/novel-deploy)\n\n## Setting Up Locally\n\nTo set up Novel locally, you'll need to clone the repository and set up the following environment variables:\n\n- `OPENAI_API_KEY` – your OpenAI API key (you can get one [here](https://platform.openai.com/account/api-keys))\n- `BLOB_READ_WRITE_TOKEN` – your Vercel Blob read/write token (currently [still in beta](https://vercel.com/docs/storage/vercel-blob/quickstart#quickstart), but feel free to [sign up on this form](https://vercel.fyi/blob-beta) for access)\n\nIf you've deployed this to Vercel, you can also use [`vc env pull`](https://vercel.com/docs/cli/env#exporting-development-environment-variables) to pull the environment variables from your Vercel project.\n\nTo run the app locally, you can run the following commands:\n\n```\npnpm i\npnpm dev\n```\n\n## Cross-framework support\n\nWhile Novel is built for React, we also have a few community-maintained packages for non-React frameworks:\n\n- Svelte: https://novel.sh/svelte\n- Vue: https://novel.sh/vue\n\n## VSCode Extension\n\nThanks to @bennykok, Novel also has a VSCode Extension: https://novel.sh/vscode\n\nhttps://github.com/steven-tey/novel/assets/28986134/58ebf7e3-cdb3-43df-878b-119e304f7373\n\n## Tech Stack\n\nNovel is built on the following stack:\n\n- [Next.js](https://nextjs.org/) – framework\n- [Tiptap](https://tiptap.dev/) – text editor\n- [OpenAI](https://openai.com/) - AI completions\n- [Vercel AI SDK](https://sdk.vercel.ai/docs) – AI library\n- [Vercel](https://vercel.com) – deployments\n- [TailwindCSS](https://tailwindcss.com/) – styles\n- [Cal Sans](https://github.com/calcom/font) – font\n\n## Contributing\n\nHere's how you can contribute:\n\n- [Open an issue](https://github.com/steven-tey/novel/issues) if you believe you've encountered a bug.\n- Make a [pull request](https://github.com/steven-tey/novel/pull) to add new features/make quality-of-life improvements/fix bugs.\n\n<a href=\"https://github.com/steven-tey/novel/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=steven-tey/novel\" />\n</a>\n\n## Repo Activity\n\n![Novel.sh repo activity – generated by Axiom](https://repobeats.axiom.co/api/embed/2ebdaa143b0ad6e7c2ee23151da7b37f67da0b36.svg)\n\n## License\n\nLicensed under the [Apache-2.0 license](https://github.com/steven-tey/novel/blob/main/LICENSE).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nWe release patches for security vulnerabilities. \n\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 0.2.x   | :white_check_mark: |\n| 0.1.x   | :x:                |\n\n## Reporting a Vulnerability\n\nPlease report (suspected) security vulnerabilities to elfandreis@gmail.com. You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity.\n"
  },
  {
    "path": "apps/web/.gitignore",
    "content": ".vercel\n"
  },
  {
    "path": "apps/web/.prettierignore",
    "content": "pnpm-lock.yaml\nyarn.lock\nnode_modules\n.next"
  },
  {
    "path": "apps/web/app/api/generate/route.ts",
    "content": "import { openai } from \"@ai-sdk/openai\";\nimport { Ratelimit } from \"@upstash/ratelimit\";\nimport { kv } from \"@vercel/kv\";\nimport { streamText } from \"ai\";\nimport { match } from \"ts-pattern\";\n\n// IMPORTANT! Set the runtime to edge: https://vercel.com/docs/functions/edge-functions/edge-runtime\nexport const runtime = \"edge\";\n\nexport async function POST(req: Request): Promise<Response> {\n  // Check if the OPENAI_API_KEY is set, if not return 400\n  if (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === \"\") {\n    return new Response(\"Missing OPENAI_API_KEY - make sure to add it to your .env file.\", {\n      status: 400,\n    });\n  }\n  if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) {\n    const ip = req.headers.get(\"x-forwarded-for\");\n    const ratelimit = new Ratelimit({\n      redis: kv,\n      limiter: Ratelimit.slidingWindow(50, \"1 d\"),\n    });\n\n    const { success, limit, reset, remaining } = await ratelimit.limit(`novel_ratelimit_${ip}`);\n\n    if (!success) {\n      return new Response(\"You have reached your request limit for the day.\", {\n        status: 429,\n        headers: {\n          \"X-RateLimit-Limit\": limit.toString(),\n          \"X-RateLimit-Remaining\": remaining.toString(),\n          \"X-RateLimit-Reset\": reset.toString(),\n        },\n      });\n    }\n  }\n\n  const { prompt, option, command } = await req.json();\n  const messages = match(option)\n    .with(\"continue\", () => [\n      {\n        role: \"system\",\n        content:\n          \"You are an AI writing assistant that continues existing text based on context from prior text. \" +\n          \"Give more weight/priority to the later characters than the beginning ones. \" +\n          \"Limit your response to no more than 200 characters, but make sure to construct complete sentences.\" +\n          \"Use Markdown formatting when appropriate.\",\n      },\n      {\n        role: \"user\",\n        content: prompt,\n      },\n    ])\n    .with(\"improve\", () => [\n      {\n        role: \"system\",\n        content:\n          \"You are an AI writing assistant that improves existing text. \" +\n          \"Limit your response to no more than 200 characters, but make sure to construct complete sentences.\" +\n          \"Use Markdown formatting when appropriate.\",\n      },\n      {\n        role: \"user\",\n        content: `The existing text is: ${prompt}`,\n      },\n    ])\n    .with(\"shorter\", () => [\n      {\n        role: \"system\",\n        content:\n          \"You are an AI writing assistant that shortens existing text. \" + \"Use Markdown formatting when appropriate.\",\n      },\n      {\n        role: \"user\",\n        content: `The existing text is: ${prompt}`,\n      },\n    ])\n    .with(\"longer\", () => [\n      {\n        role: \"system\",\n        content:\n          \"You are an AI writing assistant that lengthens existing text. \" +\n          \"Use Markdown formatting when appropriate.\",\n      },\n      {\n        role: \"user\",\n        content: `The existing text is: ${prompt}`,\n      },\n    ])\n    .with(\"fix\", () => [\n      {\n        role: \"system\",\n        content:\n          \"You are an AI writing assistant that fixes grammar and spelling errors in existing text. \" +\n          \"Limit your response to no more than 200 characters, but make sure to construct complete sentences.\" +\n          \"Use Markdown formatting when appropriate.\",\n      },\n      {\n        role: \"user\",\n        content: `The existing text is: ${prompt}`,\n      },\n    ])\n    .with(\"zap\", () => [\n      {\n        role: \"system\",\n        content:\n          \"You area an AI writing assistant that generates text based on a prompt. \" +\n          \"You take an input from the user and a command for manipulating the text\" +\n          \"Use Markdown formatting when appropriate.\",\n      },\n      {\n        role: \"user\",\n        content: `For this text: ${prompt}. You have to respect the command: ${command}`,\n      },\n    ])\n    .run();\n\n  const result = await streamText({\n    prompt: messages[messages.length - 1].content,\n    maxTokens: 4096,\n    temperature: 0.7,\n    topP: 1,\n    frequencyPenalty: 0,\n    presencePenalty: 0,\n    model: openai(\"gpt-4o-mini\"),\n  });\n\n  return result.toDataStreamResponse();\n}\n"
  },
  {
    "path": "apps/web/app/api/upload/route.ts",
    "content": "import { put } from \"@vercel/blob\";\nimport { NextResponse } from \"next/server\";\n\nexport const runtime = \"edge\";\n\nexport async function POST(req: Request) {\n  if (!process.env.BLOB_READ_WRITE_TOKEN) {\n    return new Response(\"Missing BLOB_READ_WRITE_TOKEN. Don't forget to add that to your .env file.\", {\n      status: 401,\n    });\n  }\n\n  const file = req.body || \"\";\n  const filename = req.headers.get(\"x-vercel-filename\") || \"file.txt\";\n  const contentType = req.headers.get(\"content-type\") || \"text/plain\";\n  const fileType = `.${contentType.split(\"/\")[1]}`;\n\n  // construct final filename based on content-type if not provided\n  const finalName = filename.includes(fileType) ? filename : `${filename}${fileType}`;\n  const blob = await put(finalName, file, {\n    contentType,\n    access: \"public\",\n  });\n\n  return NextResponse.json(blob);\n}\n"
  },
  {
    "path": "apps/web/app/layout.tsx",
    "content": "import \"@/styles/globals.css\";\nimport \"@/styles/prosemirror.css\";\nimport 'katex/dist/katex.min.css';\n\nimport type { Metadata, Viewport } from \"next\";\nimport type { ReactNode } from \"react\";\nimport Providers from \"./providers\";\n\nconst title = \"Novel - Notion-style WYSIWYG editor with AI-powered autocompletions\";\nconst description =\n  \"Novel is a Notion-style WYSIWYG editor with AI-powered autocompletions. Built with Tiptap, OpenAI, and Vercel AI SDK.\";\n\nexport const metadata: Metadata = {\n  title,\n  description,\n  openGraph: {\n    title,\n    description,\n  },\n  twitter: {\n    title,\n    description,\n    card: \"summary_large_image\",\n    creator: \"@steventey\",\n  },\n  metadataBase: new URL(\"https://novel.sh\"),\n};\n\nexport const viewport: Viewport = {\n  themeColor: \"#ffffff\",\n};\n\nexport default function RootLayout({ children }: { children: ReactNode }) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body>\n        <Providers>{children}</Providers>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/page.tsx",
    "content": "import TailwindAdvancedEditor from \"@/components/tailwind/advanced-editor\";\nimport { Button } from \"@/components/tailwind/ui/button\";\nimport { Dialog, DialogContent, DialogTrigger } from \"@/components/tailwind/ui/dialog\";\nimport Menu from \"@/components/tailwind/ui/menu\";\nimport { ScrollArea } from \"@/components/tailwind/ui/scroll-area\";\nimport { BookOpen, GithubIcon } from \"lucide-react\";\nimport Link from \"next/link\";\n\nexport default function Page() {\n  return (\n    <div className=\"flex min-h-screen flex-col items-center gap-4 py-4 sm:px-5\">\n      <div className=\"flex w-full max-w-screen-lg items-center gap-2 px-4 sm:mb-[calc(20vh)]\">\n        <Button size=\"icon\" variant=\"outline\">\n          <a href=\"https://github.com/steven-tey/novel\" target=\"_blank\" rel=\"noreferrer\">\n            <GithubIcon />\n          </a>\n        </Button>\n        <Dialog>\n          <DialogTrigger asChild>\n            <Button className=\"ml gap-2\">\n              <BookOpen className=\"h-4 w-4\" />\n              Usage in dialog\n            </Button>\n          </DialogTrigger>\n          <DialogContent className=\"flex max-w-3xl h-[calc(100vh-24px)]\">\n            <ScrollArea className=\"max-h-screen\">\n              <TailwindAdvancedEditor />\n            </ScrollArea>\n          </DialogContent>\n        </Dialog>\n        <Link href=\"/docs\" className=\"ml-auto\">\n          <Button variant=\"ghost\">Documentation</Button>\n        </Link>\n        <Menu />\n      </div>\n\n      <TailwindAdvancedEditor />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/providers.tsx",
    "content": "\"use client\";\n\nimport { type Dispatch, type ReactNode, type SetStateAction, createContext } from \"react\";\nimport { ThemeProvider, useTheme } from \"next-themes\";\nimport { Toaster } from \"sonner\";\nimport { Analytics } from \"@vercel/analytics/react\";\nimport useLocalStorage from \"@/hooks/use-local-storage\";\n\nexport const AppContext = createContext<{\n  font: string;\n  setFont: Dispatch<SetStateAction<string>>;\n}>({\n  font: \"Default\",\n  setFont: () => {},\n});\n\nconst ToasterProvider = () => {\n  const { theme } = useTheme() as {\n    theme: \"light\" | \"dark\" | \"system\";\n  };\n  return <Toaster theme={theme} />;\n};\n\nexport default function Providers({ children }: { children: ReactNode }) {\n  const [font, setFont] = useLocalStorage<string>(\"novel__font\", \"Default\");\n\n  return (\n    <ThemeProvider attribute=\"class\" enableSystem disableTransitionOnChange defaultTheme=\"system\">\n      <AppContext.Provider\n        value={{\n          font,\n          setFont,\n        }}\n      >\n        <ToasterProvider />\n        {children}\n        <Analytics />\n      </AppContext.Provider>\n    </ThemeProvider>\n  );\n}\n"
  },
  {
    "path": "apps/web/biome.json",
    "content": "{\n  \"extends\": [\"../../biome.json\"]\n}\n"
  },
  {
    "path": "apps/web/components/tailwind/advanced-editor.tsx",
    "content": "\"use client\";\nimport { defaultEditorContent } from \"@/lib/content\";\nimport {\n  EditorCommand,\n  EditorCommandEmpty,\n  EditorCommandItem,\n  EditorCommandList,\n  EditorContent,\n  type EditorInstance,\n  EditorRoot,\n  ImageResizer,\n  type JSONContent,\n  handleCommandNavigation,\n  handleImageDrop,\n  handleImagePaste,\n} from \"novel\";\nimport { useEffect, useState } from \"react\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport { defaultExtensions } from \"./extensions\";\nimport { ColorSelector } from \"./selectors/color-selector\";\nimport { LinkSelector } from \"./selectors/link-selector\";\nimport { MathSelector } from \"./selectors/math-selector\";\nimport { NodeSelector } from \"./selectors/node-selector\";\nimport { Separator } from \"./ui/separator\";\n\nimport GenerativeMenuSwitch from \"./generative/generative-menu-switch\";\nimport { uploadFn } from \"./image-upload\";\nimport { TextButtons } from \"./selectors/text-buttons\";\nimport { slashCommand, suggestionItems } from \"./slash-command\";\n\nconst hljs = require(\"highlight.js\");\n\nconst extensions = [...defaultExtensions, slashCommand];\n\nconst TailwindAdvancedEditor = () => {\n  const [initialContent, setInitialContent] = useState<null | JSONContent>(null);\n  const [saveStatus, setSaveStatus] = useState(\"Saved\");\n  const [charsCount, setCharsCount] = useState();\n\n  const [openNode, setOpenNode] = useState(false);\n  const [openColor, setOpenColor] = useState(false);\n  const [openLink, setOpenLink] = useState(false);\n  const [openAI, setOpenAI] = useState(false);\n\n  //Apply Codeblock Highlighting on the HTML from editor.getHTML()\n  const highlightCodeblocks = (content: string) => {\n    const doc = new DOMParser().parseFromString(content, \"text/html\");\n    doc.querySelectorAll(\"pre code\").forEach((el) => {\n      // @ts-ignore\n      // https://highlightjs.readthedocs.io/en/latest/api.html?highlight=highlightElement#highlightelement\n      hljs.highlightElement(el);\n    });\n    return new XMLSerializer().serializeToString(doc);\n  };\n\n  const debouncedUpdates = useDebouncedCallback(async (editor: EditorInstance) => {\n    const json = editor.getJSON();\n    setCharsCount(editor.storage.characterCount.words());\n    window.localStorage.setItem(\"html-content\", highlightCodeblocks(editor.getHTML()));\n    window.localStorage.setItem(\"novel-content\", JSON.stringify(json));\n    window.localStorage.setItem(\"markdown\", editor.storage.markdown.getMarkdown());\n    setSaveStatus(\"Saved\");\n  }, 500);\n\n  useEffect(() => {\n    const content = window.localStorage.getItem(\"novel-content\");\n    if (content) setInitialContent(JSON.parse(content));\n    else setInitialContent(defaultEditorContent);\n  }, []);\n\n  if (!initialContent) return null;\n\n  return (\n    <div className=\"relative w-full max-w-screen-lg\">\n      <div className=\"flex absolute right-5 top-5 z-10 mb-5 gap-2\">\n        <div className=\"rounded-lg bg-accent px-2 py-1 text-sm text-muted-foreground\">{saveStatus}</div>\n        <div className={charsCount ? \"rounded-lg bg-accent px-2 py-1 text-sm text-muted-foreground\" : \"hidden\"}>\n          {charsCount} Words\n        </div>\n      </div>\n      <EditorRoot>\n        <EditorContent\n          initialContent={initialContent}\n          extensions={extensions}\n          className=\"relative min-h-[500px] w-full max-w-screen-lg border-muted bg-background sm:mb-[calc(20vh)] sm:rounded-lg sm:border sm:shadow-lg\"\n          editorProps={{\n            handleDOMEvents: {\n              keydown: (_view, event) => handleCommandNavigation(event),\n            },\n            handlePaste: (view, event) => handleImagePaste(view, event, uploadFn),\n            handleDrop: (view, event, _slice, moved) => handleImageDrop(view, event, moved, uploadFn),\n            attributes: {\n              class:\n                \"prose prose-lg dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full\",\n            },\n          }}\n          onUpdate={({ editor }) => {\n            debouncedUpdates(editor);\n            setSaveStatus(\"Unsaved\");\n          }}\n          slotAfter={<ImageResizer />}\n        >\n          <EditorCommand className=\"z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border border-muted bg-background px-1 py-2 shadow-md transition-all\">\n            <EditorCommandEmpty className=\"px-2 text-muted-foreground\">No results</EditorCommandEmpty>\n            <EditorCommandList>\n              {suggestionItems.map((item) => (\n                <EditorCommandItem\n                  value={item.title}\n                  onCommand={(val) => item.command(val)}\n                  className=\"flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm hover:bg-accent aria-selected:bg-accent\"\n                  key={item.title}\n                >\n                  <div className=\"flex h-10 w-10 items-center justify-center rounded-md border border-muted bg-background\">\n                    {item.icon}\n                  </div>\n                  <div>\n                    <p className=\"font-medium\">{item.title}</p>\n                    <p className=\"text-xs text-muted-foreground\">{item.description}</p>\n                  </div>\n                </EditorCommandItem>\n              ))}\n            </EditorCommandList>\n          </EditorCommand>\n\n          <GenerativeMenuSwitch open={openAI} onOpenChange={setOpenAI}>\n            <Separator orientation=\"vertical\" />\n            <NodeSelector open={openNode} onOpenChange={setOpenNode} />\n            <Separator orientation=\"vertical\" />\n\n            <LinkSelector open={openLink} onOpenChange={setOpenLink} />\n            <Separator orientation=\"vertical\" />\n            <MathSelector />\n            <Separator orientation=\"vertical\" />\n            <TextButtons />\n            <Separator orientation=\"vertical\" />\n            <ColorSelector open={openColor} onOpenChange={setOpenColor} />\n          </GenerativeMenuSwitch>\n        </EditorContent>\n      </EditorRoot>\n    </div>\n  );\n};\n\nexport default TailwindAdvancedEditor;\n"
  },
  {
    "path": "apps/web/components/tailwind/extensions.ts",
    "content": "import {\n  AIHighlight,\n  CharacterCount,\n  CodeBlockLowlight,\n  Color,\n  CustomKeymap,\n  GlobalDragHandle,\n  HighlightExtension,\n  HorizontalRule,\n  MarkdownExtension,\n  Mathematics,\n  Placeholder,\n  StarterKit,\n  TaskItem,\n  TaskList,\n  TextStyle,\n  TiptapImage,\n  TiptapLink,\n  TiptapUnderline,\n  Twitter,\n  UpdatedImage,\n  UploadImagesPlugin,\n  Youtube,\n} from \"novel\";\n\nimport { cx } from \"class-variance-authority\";\nimport { common, createLowlight } from \"lowlight\";\n\n//TODO I am using cx here to get tailwind autocomplete working, idk if someone else can write a regex to just capture the class key in objects\nconst aiHighlight = AIHighlight;\n//You can overwrite the placeholder with your own configuration\nconst placeholder = Placeholder;\nconst tiptapLink = TiptapLink.configure({\n  HTMLAttributes: {\n    class: cx(\n      \"text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer\",\n    ),\n  },\n});\n\nconst tiptapImage = TiptapImage.extend({\n  addProseMirrorPlugins() {\n    return [\n      UploadImagesPlugin({\n        imageClass: cx(\"opacity-40 rounded-lg border border-stone-200\"),\n      }),\n    ];\n  },\n}).configure({\n  allowBase64: true,\n  HTMLAttributes: {\n    class: cx(\"rounded-lg border border-muted\"),\n  },\n});\n\nconst updatedImage = UpdatedImage.configure({\n  HTMLAttributes: {\n    class: cx(\"rounded-lg border border-muted\"),\n  },\n});\n\nconst taskList = TaskList.configure({\n  HTMLAttributes: {\n    class: cx(\"not-prose pl-2 \"),\n  },\n});\nconst taskItem = TaskItem.configure({\n  HTMLAttributes: {\n    class: cx(\"flex gap-2 items-start my-4\"),\n  },\n  nested: true,\n});\n\nconst horizontalRule = HorizontalRule.configure({\n  HTMLAttributes: {\n    class: cx(\"mt-4 mb-6 border-t border-muted-foreground\"),\n  },\n});\n\nconst starterKit = StarterKit.configure({\n  bulletList: {\n    HTMLAttributes: {\n      class: cx(\"list-disc list-outside leading-3 -mt-2\"),\n    },\n  },\n  orderedList: {\n    HTMLAttributes: {\n      class: cx(\"list-decimal list-outside leading-3 -mt-2\"),\n    },\n  },\n  listItem: {\n    HTMLAttributes: {\n      class: cx(\"leading-normal -mb-2\"),\n    },\n  },\n  blockquote: {\n    HTMLAttributes: {\n      class: cx(\"border-l-4 border-primary\"),\n    },\n  },\n  codeBlock: {\n    HTMLAttributes: {\n      class: cx(\"rounded-md bg-muted text-muted-foreground border p-5 font-mono font-medium\"),\n    },\n  },\n  code: {\n    HTMLAttributes: {\n      class: cx(\"rounded-md bg-muted  px-1.5 py-1 font-mono font-medium\"),\n      spellcheck: \"false\",\n    },\n  },\n  horizontalRule: false,\n  dropcursor: {\n    color: \"#DBEAFE\",\n    width: 4,\n  },\n  gapcursor: false,\n});\n\nconst codeBlockLowlight = CodeBlockLowlight.configure({\n  // configure lowlight: common /  all / use highlightJS in case there is a need to specify certain language grammars only\n  // common: covers 37 language grammars which should be good enough in most cases\n  lowlight: createLowlight(common),\n});\n\nconst youtube = Youtube.configure({\n  HTMLAttributes: {\n    class: cx(\"rounded-lg border border-muted\"),\n  },\n  inline: false,\n});\n\nconst twitter = Twitter.configure({\n  HTMLAttributes: {\n    class: cx(\"not-prose\"),\n  },\n  inline: false,\n});\n\nconst mathematics = Mathematics.configure({\n  HTMLAttributes: {\n    class: cx(\"text-foreground rounded p-1 hover:bg-accent cursor-pointer\"),\n  },\n  katexOptions: {\n    throwOnError: false,\n  },\n});\n\nconst characterCount = CharacterCount.configure();\n\nconst markdownExtension = MarkdownExtension.configure({\n  html: true,\n  tightLists: true,\n  tightListClass: \"tight\",\n  bulletListMarker: \"-\",\n  linkify: false,\n  breaks: false,\n  transformPastedText: false,\n  transformCopiedText: false,\n});\n\nexport const defaultExtensions = [\n  starterKit,\n  placeholder,\n  tiptapLink,\n  tiptapImage,\n  updatedImage,\n  taskList,\n  taskItem,\n  horizontalRule,\n  aiHighlight,\n  codeBlockLowlight,\n  youtube,\n  twitter,\n  mathematics,\n  characterCount,\n  TiptapUnderline,\n  markdownExtension,\n  HighlightExtension,\n  TextStyle,\n  Color,\n  CustomKeymap,\n  GlobalDragHandle,\n];\n"
  },
  {
    "path": "apps/web/components/tailwind/generative/ai-completion-command.tsx",
    "content": "import { CommandGroup, CommandItem, CommandSeparator } from \"../ui/command\";\nimport { useEditor } from \"novel\";\nimport { Check, TextQuote, TrashIcon } from \"lucide-react\";\n\nconst AICompletionCommands = ({\n  completion,\n  onDiscard,\n}: {\n  completion: string;\n  onDiscard: () => void;\n}) => {\n  const { editor } = useEditor();\n  return (\n    <>\n      <CommandGroup>\n        <CommandItem\n          className=\"gap-2 px-4\"\n          value=\"replace\"\n          onSelect={() => {\n            const selection = editor.view.state.selection;\n\n            editor\n              .chain()\n              .focus()\n              .insertContentAt(\n                {\n                  from: selection.from,\n                  to: selection.to,\n                },\n                completion,\n              )\n              .run();\n          }}\n        >\n          <Check className=\"h-4 w-4 text-muted-foreground\" />\n          Replace selection\n        </CommandItem>\n        <CommandItem\n          className=\"gap-2 px-4\"\n          value=\"insert\"\n          onSelect={() => {\n            const selection = editor.view.state.selection;\n            editor\n              .chain()\n              .focus()\n              .insertContentAt(selection.to + 1, completion)\n              .run();\n          }}\n        >\n          <TextQuote className=\"h-4 w-4 text-muted-foreground\" />\n          Insert below\n        </CommandItem>\n      </CommandGroup>\n      <CommandSeparator />\n\n      <CommandGroup>\n        <CommandItem onSelect={onDiscard} value=\"thrash\" className=\"gap-2 px-4\">\n          <TrashIcon className=\"h-4 w-4 text-muted-foreground\" />\n          Discard\n        </CommandItem>\n      </CommandGroup>\n    </>\n  );\n};\n\nexport default AICompletionCommands;\n"
  },
  {
    "path": "apps/web/components/tailwind/generative/ai-selector-commands.tsx",
    "content": "import { ArrowDownWideNarrow, CheckCheck, RefreshCcwDot, StepForward, WrapText } from \"lucide-react\";\nimport { getPrevText, useEditor } from \"novel\";\nimport { CommandGroup, CommandItem, CommandSeparator } from \"../ui/command\";\n\nconst options = [\n  {\n    value: \"improve\",\n    label: \"Improve writing\",\n    icon: RefreshCcwDot,\n  },\n  {\n    value: \"fix\",\n    label: \"Fix grammar\",\n    icon: CheckCheck,\n  },\n  {\n    value: \"shorter\",\n    label: \"Make shorter\",\n    icon: ArrowDownWideNarrow,\n  },\n  {\n    value: \"longer\",\n    label: \"Make longer\",\n    icon: WrapText,\n  },\n];\n\ninterface AISelectorCommandsProps {\n  onSelect: (value: string, option: string) => void;\n}\n\nconst AISelectorCommands = ({ onSelect }: AISelectorCommandsProps) => {\n  const { editor } = useEditor();\n\n  return (\n    <>\n      <CommandGroup heading=\"Edit or review selection\">\n        {options.map((option) => (\n          <CommandItem\n            onSelect={(value) => {\n              const slice = editor.state.selection.content();\n              const text = editor.storage.markdown.serializer.serialize(slice.content);\n              onSelect(text, value);\n            }}\n            className=\"flex gap-2 px-4\"\n            key={option.value}\n            value={option.value}\n          >\n            <option.icon className=\"h-4 w-4 text-purple-500\" />\n            {option.label}\n          </CommandItem>\n        ))}\n      </CommandGroup>\n      <CommandSeparator />\n      <CommandGroup heading=\"Use AI to do more\">\n        <CommandItem\n          onSelect={() => {\n            const pos = editor.state.selection.from;\n            const text = getPrevText(editor, pos);\n            onSelect(text, \"continue\");\n          }}\n          value=\"continue\"\n          className=\"gap-2 px-4\"\n        >\n          <StepForward className=\"h-4 w-4 text-purple-500\" />\n          Continue writing\n        </CommandItem>\n      </CommandGroup>\n    </>\n  );\n};\n\nexport default AISelectorCommands;\n"
  },
  {
    "path": "apps/web/components/tailwind/generative/ai-selector.tsx",
    "content": "\"use client\";\n\nimport { Command, CommandInput } from \"@/components/tailwind/ui/command\";\n\nimport { useCompletion } from \"ai/react\";\nimport { ArrowUp } from \"lucide-react\";\nimport { useEditor } from \"novel\";\nimport { addAIHighlight } from \"novel\";\nimport { useState } from \"react\";\nimport Markdown from \"react-markdown\";\nimport { toast } from \"sonner\";\nimport { Button } from \"../ui/button\";\nimport CrazySpinner from \"../ui/icons/crazy-spinner\";\nimport Magic from \"../ui/icons/magic\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport AICompletionCommands from \"./ai-completion-command\";\nimport AISelectorCommands from \"./ai-selector-commands\";\n//TODO: I think it makes more sense to create a custom Tiptap extension for this functionality https://tiptap.dev/docs/editor/ai/introduction\n\ninterface AISelectorProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\nexport function AISelector({ onOpenChange }: AISelectorProps) {\n  const { editor } = useEditor();\n  const [inputValue, setInputValue] = useState(\"\");\n\n  const { completion, complete, isLoading } = useCompletion({\n    // id: \"novel\",\n    api: \"/api/generate\",\n    onResponse: (response) => {\n      if (response.status === 429) {\n        toast.error(\"You have reached your request limit for the day.\");\n        return;\n      }\n    },\n    onError: (e) => {\n      toast.error(e.message);\n    },\n  });\n\n  const hasCompletion = completion.length > 0;\n\n  return (\n    <Command className=\"w-[350px]\">\n      {hasCompletion && (\n        <div className=\"flex max-h-[400px]\">\n          <ScrollArea>\n            <div className=\"prose p-2 px-4 prose-sm\">\n              <Markdown>{completion}</Markdown>\n            </div>\n          </ScrollArea>\n        </div>\n      )}\n\n      {isLoading && (\n        <div className=\"flex h-12 w-full items-center px-4 text-sm font-medium text-muted-foreground text-purple-500\">\n          <Magic className=\"mr-2 h-4 w-4 shrink-0  \" />\n          AI is thinking\n          <div className=\"ml-2 mt-1\">\n            <CrazySpinner />\n          </div>\n        </div>\n      )}\n      {!isLoading && (\n        <>\n          <div className=\"relative\">\n            <CommandInput\n              value={inputValue}\n              onValueChange={setInputValue}\n              autoFocus\n              placeholder={hasCompletion ? \"Tell AI what to do next\" : \"Ask AI to edit or generate...\"}\n              onFocus={() => addAIHighlight(editor)}\n            />\n            <Button\n              size=\"icon\"\n              className=\"absolute right-2 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full bg-purple-500 hover:bg-purple-900\"\n              onClick={() => {\n                if (completion)\n                  return complete(completion, {\n                    body: { option: \"zap\", command: inputValue },\n                  }).then(() => setInputValue(\"\"));\n\n                const slice = editor.state.selection.content();\n                const text = editor.storage.markdown.serializer.serialize(slice.content);\n\n                complete(text, {\n                  body: { option: \"zap\", command: inputValue },\n                }).then(() => setInputValue(\"\"));\n              }}\n            >\n              <ArrowUp className=\"h-4 w-4\" />\n            </Button>\n          </div>\n          {hasCompletion ? (\n            <AICompletionCommands\n              onDiscard={() => {\n                editor.chain().unsetHighlight().focus().run();\n                onOpenChange(false);\n              }}\n              completion={completion}\n            />\n          ) : (\n            <AISelectorCommands onSelect={(value, option) => complete(value, { body: { option } })} />\n          )}\n        </>\n      )}\n    </Command>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/tailwind/generative/generative-menu-switch.tsx",
    "content": "import { EditorBubble, removeAIHighlight, useEditor } from \"novel\";\nimport { Fragment, type ReactNode, useEffect } from \"react\";\nimport { Button } from \"../ui/button\";\nimport Magic from \"../ui/icons/magic\";\nimport { AISelector } from \"./ai-selector\";\n\ninterface GenerativeMenuSwitchProps {\n  children: ReactNode;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\nconst GenerativeMenuSwitch = ({ children, open, onOpenChange }: GenerativeMenuSwitchProps) => {\n  const { editor } = useEditor();\n\n  useEffect(() => {\n    if (!open) removeAIHighlight(editor);\n  }, [open]);\n  return (\n    <EditorBubble\n      tippyOptions={{\n        placement: open ? \"bottom-start\" : \"top\",\n        onHidden: () => {\n          onOpenChange(false);\n          editor.chain().unsetHighlight().run();\n        },\n      }}\n      className=\"flex w-fit max-w-[90vw] overflow-hidden rounded-md border border-muted bg-background shadow-xl\"\n    >\n      {open && <AISelector open={open} onOpenChange={onOpenChange} />}\n      {!open && (\n        <Fragment>\n          <Button\n            className=\"gap-1 rounded-none text-purple-500\"\n            variant=\"ghost\"\n            onClick={() => onOpenChange(true)}\n            size=\"sm\"\n          >\n            <Magic className=\"h-5 w-5\" />\n            Ask AI\n          </Button>\n          {children}\n        </Fragment>\n      )}\n    </EditorBubble>\n  );\n};\n\nexport default GenerativeMenuSwitch;\n"
  },
  {
    "path": "apps/web/components/tailwind/image-upload.ts",
    "content": "import { createImageUpload } from \"novel\";\nimport { toast } from \"sonner\";\n\nconst onUpload = (file: File) => {\n  const promise = fetch(\"/api/upload\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": file?.type || \"application/octet-stream\",\n      \"x-vercel-filename\": file?.name || \"image.png\",\n    },\n    body: file,\n  });\n\n  return new Promise((resolve, reject) => {\n    toast.promise(\n      promise.then(async (res) => {\n        // Successfully uploaded image\n        if (res.status === 200) {\n          const { url } = (await res.json()) as { url: string };\n          // preload the image\n          const image = new Image();\n          image.src = url;\n          image.onload = () => {\n            resolve(url);\n          };\n          // No blob store configured\n        } else if (res.status === 401) {\n          resolve(file);\n          throw new Error(\"`BLOB_READ_WRITE_TOKEN` environment variable not found, reading image locally instead.\");\n          // Unknown error\n        } else {\n          throw new Error(\"Error uploading image. Please try again.\");\n        }\n      }),\n      {\n        loading: \"Uploading image...\",\n        success: \"Image uploaded successfully.\",\n        error: (e) => {\n          reject(e);\n          return e.message;\n        },\n      },\n    );\n  });\n};\n\nexport const uploadFn = createImageUpload({\n  onUpload,\n  validateFn: (file) => {\n    if (!file.type.includes(\"image/\")) {\n      toast.error(\"File type not supported.\");\n      return false;\n    }\n    if (file.size / 1024 / 1024 > 20) {\n      toast.error(\"File size too big (max 20MB).\");\n      return false;\n    }\n    return true;\n  },\n});\n"
  },
  {
    "path": "apps/web/components/tailwind/selectors/color-selector.tsx",
    "content": "import { Check, ChevronDown } from \"lucide-react\";\nimport { EditorBubbleItem, useEditor } from \"novel\";\n\nimport { Button } from \"@/components/tailwind/ui/button\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/tailwind/ui/popover\";\nexport interface BubbleColorMenuItem {\n  name: string;\n  color: string;\n}\n\nconst TEXT_COLORS: BubbleColorMenuItem[] = [\n  {\n    name: \"Default\",\n    color: \"var(--novel-black)\",\n  },\n  {\n    name: \"Purple\",\n    color: \"#9333EA\",\n  },\n  {\n    name: \"Red\",\n    color: \"#E00000\",\n  },\n  {\n    name: \"Yellow\",\n    color: \"#EAB308\",\n  },\n  {\n    name: \"Blue\",\n    color: \"#2563EB\",\n  },\n  {\n    name: \"Green\",\n    color: \"#008A00\",\n  },\n  {\n    name: \"Orange\",\n    color: \"#FFA500\",\n  },\n  {\n    name: \"Pink\",\n    color: \"#BA4081\",\n  },\n  {\n    name: \"Gray\",\n    color: \"#A8A29E\",\n  },\n];\n\nconst HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [\n  {\n    name: \"Default\",\n    color: \"var(--novel-highlight-default)\",\n  },\n  {\n    name: \"Purple\",\n    color: \"var(--novel-highlight-purple)\",\n  },\n  {\n    name: \"Red\",\n    color: \"var(--novel-highlight-red)\",\n  },\n  {\n    name: \"Yellow\",\n    color: \"var(--novel-highlight-yellow)\",\n  },\n  {\n    name: \"Blue\",\n    color: \"var(--novel-highlight-blue)\",\n  },\n  {\n    name: \"Green\",\n    color: \"var(--novel-highlight-green)\",\n  },\n  {\n    name: \"Orange\",\n    color: \"var(--novel-highlight-orange)\",\n  },\n  {\n    name: \"Pink\",\n    color: \"var(--novel-highlight-pink)\",\n  },\n  {\n    name: \"Gray\",\n    color: \"var(--novel-highlight-gray)\",\n  },\n];\n\ninterface ColorSelectorProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\nexport const ColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => {\n  const { editor } = useEditor();\n\n  if (!editor) return null;\n  const activeColorItem = TEXT_COLORS.find(({ color }) => editor.isActive(\"textStyle\", { color }));\n\n  const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) => editor.isActive(\"highlight\", { color }));\n\n  return (\n    <Popover modal={true} open={open} onOpenChange={onOpenChange}>\n      <PopoverTrigger asChild>\n        <Button size=\"sm\" className=\"gap-2 rounded-none\" variant=\"ghost\">\n          <span\n            className=\"rounded-sm px-1\"\n            style={{\n              color: activeColorItem?.color,\n              backgroundColor: activeHighlightItem?.color,\n            }}\n          >\n            A\n          </span>\n          <ChevronDown className=\"h-4 w-4\" />\n        </Button>\n      </PopoverTrigger>\n\n      <PopoverContent\n        sideOffset={5}\n        className=\"my-1 flex max-h-80 w-48 flex-col overflow-hidden overflow-y-auto rounded border p-1 shadow-xl \"\n        align=\"start\"\n      >\n        <div className=\"flex flex-col\">\n          <div className=\"my-1 px-2 text-sm font-semibold text-muted-foreground\">Color</div>\n          {TEXT_COLORS.map(({ name, color }) => (\n            <EditorBubbleItem\n              key={name}\n              onSelect={() => {\n                editor.commands.unsetColor();\n                name !== \"Default\" &&\n                  editor\n                    .chain()\n                    .focus()\n                    .setColor(color || \"\")\n                    .run();\n                onOpenChange(false);\n              }}\n              className=\"flex cursor-pointer items-center justify-between px-2 py-1 text-sm hover:bg-accent\"\n            >\n              <div className=\"flex items-center gap-2\">\n                <div className=\"rounded-sm border px-2 py-px font-medium\" style={{ color }}>\n                  A\n                </div>\n                <span>{name}</span>\n              </div>\n            </EditorBubbleItem>\n          ))}\n        </div>\n        <div>\n          <div className=\"my-1 px-2 text-sm font-semibold text-muted-foreground\">Background</div>\n          {HIGHLIGHT_COLORS.map(({ name, color }) => (\n            <EditorBubbleItem\n              key={name}\n              onSelect={() => {\n                editor.commands.unsetHighlight();\n                name !== \"Default\" && editor.chain().focus().setHighlight({ color }).run();\n                onOpenChange(false);\n              }}\n              className=\"flex cursor-pointer items-center justify-between px-2 py-1 text-sm hover:bg-accent\"\n            >\n              <div className=\"flex items-center gap-2\">\n                <div className=\"rounded-sm border px-2 py-px font-medium\" style={{ backgroundColor: color }}>\n                  A\n                </div>\n                <span>{name}</span>\n              </div>\n              {editor.isActive(\"highlight\", { color }) && <Check className=\"h-4 w-4\" />}\n            </EditorBubbleItem>\n          ))}\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/web/components/tailwind/selectors/link-selector.tsx",
    "content": "import { Button } from \"@/components/tailwind/ui/button\";\nimport { PopoverContent } from \"@/components/tailwind/ui/popover\";\nimport { cn } from \"@/lib/utils\";\nimport { Popover, PopoverTrigger } from \"@radix-ui/react-popover\";\nimport { Check, Trash } from \"lucide-react\";\nimport { useEditor } from \"novel\";\nimport { useEffect, useRef } from \"react\";\n\nexport function isValidUrl(url: string) {\n  try {\n    new URL(url);\n    return true;\n  } catch (_e) {\n    return false;\n  }\n}\nexport function getUrlFromString(str: string) {\n  if (isValidUrl(str)) return str;\n  try {\n    if (str.includes(\".\") && !str.includes(\" \")) {\n      return new URL(`https://${str}`).toString();\n    }\n  } catch (_e) {\n    return null;\n  }\n}\ninterface LinkSelectorProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\nexport const LinkSelector = ({ open, onOpenChange }: LinkSelectorProps) => {\n  const inputRef = useRef<HTMLInputElement>(null);\n  const { editor } = useEditor();\n\n  // Autofocus on input by default\n  useEffect(() => {\n    inputRef.current?.focus();\n  });\n  if (!editor) return null;\n\n  return (\n    <Popover modal={true} open={open} onOpenChange={onOpenChange}>\n      <PopoverTrigger asChild>\n        <Button size=\"sm\" variant=\"ghost\" className=\"gap-2 rounded-none border-none\">\n          <p className=\"text-base\">↗</p>\n          <p\n            className={cn(\"underline decoration-stone-400 underline-offset-4\", {\n              \"text-blue-500\": editor.isActive(\"link\"),\n            })}\n          >\n            Link\n          </p>\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent align=\"start\" className=\"w-60 p-0\" sideOffset={10}>\n        <form\n          onSubmit={(e) => {\n            const target = e.currentTarget as HTMLFormElement;\n            e.preventDefault();\n            const input = target[0] as HTMLInputElement;\n            const url = getUrlFromString(input.value);\n            if (url) {\n              editor.chain().focus().setLink({ href: url }).run();\n              onOpenChange(false);\n            }\n          }}\n          className=\"flex  p-1 \"\n        >\n          <input\n            ref={inputRef}\n            type=\"text\"\n            placeholder=\"Paste a link\"\n            className=\"flex-1 bg-background p-1 text-sm outline-none\"\n            defaultValue={editor.getAttributes(\"link\").href || \"\"}\n          />\n          {editor.getAttributes(\"link\").href ? (\n            <Button\n              size=\"icon\"\n              variant=\"outline\"\n              type=\"button\"\n              className=\"flex h-8 items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800\"\n              onClick={() => {\n                editor.chain().focus().unsetLink().run();\n                inputRef.current.value = \"\";\n                onOpenChange(false);\n              }}\n            >\n              <Trash className=\"h-4 w-4\" />\n            </Button>\n          ) : (\n            <Button size=\"icon\" className=\"h-8\">\n              <Check className=\"h-4 w-4\" />\n            </Button>\n          )}\n        </form>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/web/components/tailwind/selectors/math-selector.tsx",
    "content": "import { Button } from \"@/components/tailwind/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { SigmaIcon } from \"lucide-react\";\nimport { useEditor } from \"novel\";\n\nexport const MathSelector = () => {\n  const { editor } = useEditor();\n\n  if (!editor) return null;\n\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"sm\"\n      className=\"rounded-none w-12\"\n      onClick={(evt) => {\n        if (editor.isActive(\"math\")) {\n          editor.chain().focus().unsetLatex().run();\n        } else {\n          const { from, to } = editor.state.selection;\n          const latex = editor.state.doc.textBetween(from, to);\n\n          if (!latex) return;\n\n          editor.chain().focus().setLatex({ latex }).run();\n        }\n      }}\n    >\n      <SigmaIcon\n        className={cn(\"size-4\", { \"text-blue-500\": editor.isActive(\"math\") })}\n        strokeWidth={2.3}\n      />\n    </Button>\n  );\n};\n"
  },
  {
    "path": "apps/web/components/tailwind/selectors/node-selector.tsx",
    "content": "import {\n  Check,\n  CheckSquare,\n  ChevronDown,\n  Code,\n  Heading1,\n  Heading2,\n  Heading3,\n  ListOrdered,\n  type LucideIcon,\n  TextIcon,\n  TextQuote,\n} from \"lucide-react\";\nimport { EditorBubbleItem, useEditor } from \"novel\";\n\nimport { Button } from \"@/components/tailwind/ui/button\";\nimport { PopoverContent, PopoverTrigger } from \"@/components/tailwind/ui/popover\";\nimport { Popover } from \"@radix-ui/react-popover\";\n\nexport type SelectorItem = {\n  name: string;\n  icon: LucideIcon;\n  command: (editor: ReturnType<typeof useEditor>[\"editor\"]) => void;\n  isActive: (editor: ReturnType<typeof useEditor>[\"editor\"]) => boolean;\n};\n\nconst items: SelectorItem[] = [\n  {\n    name: \"Text\",\n    icon: TextIcon,\n    command: (editor) => editor.chain().focus().clearNodes().run(),\n    // I feel like there has to be a more efficient way to do this – feel free to PR if you know how!\n    isActive: (editor) =>\n      editor.isActive(\"paragraph\") && !editor.isActive(\"bulletList\") && !editor.isActive(\"orderedList\"),\n  },\n  {\n    name: \"Heading 1\",\n    icon: Heading1,\n    command: (editor) => editor.chain().focus().clearNodes().toggleHeading({ level: 1 }).run(),\n    isActive: (editor) => editor.isActive(\"heading\", { level: 1 }),\n  },\n  {\n    name: \"Heading 2\",\n    icon: Heading2,\n    command: (editor) => editor.chain().focus().clearNodes().toggleHeading({ level: 2 }).run(),\n    isActive: (editor) => editor.isActive(\"heading\", { level: 2 }),\n  },\n  {\n    name: \"Heading 3\",\n    icon: Heading3,\n    command: (editor) => editor.chain().focus().clearNodes().toggleHeading({ level: 3 }).run(),\n    isActive: (editor) => editor.isActive(\"heading\", { level: 3 }),\n  },\n  {\n    name: \"To-do List\",\n    icon: CheckSquare,\n    command: (editor) => editor.chain().focus().clearNodes().toggleTaskList().run(),\n    isActive: (editor) => editor.isActive(\"taskItem\"),\n  },\n  {\n    name: \"Bullet List\",\n    icon: ListOrdered,\n    command: (editor) => editor.chain().focus().clearNodes().toggleBulletList().run(),\n    isActive: (editor) => editor.isActive(\"bulletList\"),\n  },\n  {\n    name: \"Numbered List\",\n    icon: ListOrdered,\n    command: (editor) => editor.chain().focus().clearNodes().toggleOrderedList().run(),\n    isActive: (editor) => editor.isActive(\"orderedList\"),\n  },\n  {\n    name: \"Quote\",\n    icon: TextQuote,\n    command: (editor) => editor.chain().focus().clearNodes().toggleBlockquote().run(),\n    isActive: (editor) => editor.isActive(\"blockquote\"),\n  },\n  {\n    name: \"Code\",\n    icon: Code,\n    command: (editor) => editor.chain().focus().clearNodes().toggleCodeBlock().run(),\n    isActive: (editor) => editor.isActive(\"codeBlock\"),\n  },\n];\ninterface NodeSelectorProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\nexport const NodeSelector = ({ open, onOpenChange }: NodeSelectorProps) => {\n  const { editor } = useEditor();\n  if (!editor) return null;\n  const activeItem = items.filter((item) => item.isActive(editor)).pop() ?? {\n    name: \"Multiple\",\n  };\n\n  return (\n    <Popover modal={true} open={open} onOpenChange={onOpenChange}>\n      <PopoverTrigger asChild className=\"gap-2 rounded-none border-none hover:bg-accent focus:ring-0\">\n        <Button size=\"sm\" variant=\"ghost\" className=\"gap-2\">\n          <span className=\"whitespace-nowrap text-sm\">{activeItem.name}</span>\n          <ChevronDown className=\"h-4 w-4\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent sideOffset={5} align=\"start\" className=\"w-48 p-1\">\n        {items.map((item) => (\n          <EditorBubbleItem\n            key={item.name}\n            onSelect={(editor) => {\n              item.command(editor);\n              onOpenChange(false);\n            }}\n            className=\"flex cursor-pointer items-center justify-between rounded-sm px-2 py-1 text-sm hover:bg-accent\"\n          >\n            <div className=\"flex items-center space-x-2\">\n              <div className=\"rounded-sm border p-1\">\n                <item.icon className=\"h-3 w-3\" />\n              </div>\n              <span>{item.name}</span>\n            </div>\n            {activeItem.name === item.name && <Check className=\"h-4 w-4\" />}\n          </EditorBubbleItem>\n        ))}\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/web/components/tailwind/selectors/text-buttons.tsx",
    "content": "import { Button } from \"@/components/tailwind/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { BoldIcon, CodeIcon, ItalicIcon, StrikethroughIcon, UnderlineIcon } from \"lucide-react\";\nimport { EditorBubbleItem, useEditor } from \"novel\";\nimport type { SelectorItem } from \"./node-selector\";\n\nexport const TextButtons = () => {\n  const { editor } = useEditor();\n  if (!editor) return null;\n  const items: SelectorItem[] = [\n    {\n      name: \"bold\",\n      isActive: (editor) => editor.isActive(\"bold\"),\n      command: (editor) => editor.chain().focus().toggleBold().run(),\n      icon: BoldIcon,\n    },\n    {\n      name: \"italic\",\n      isActive: (editor) => editor.isActive(\"italic\"),\n      command: (editor) => editor.chain().focus().toggleItalic().run(),\n      icon: ItalicIcon,\n    },\n    {\n      name: \"underline\",\n      isActive: (editor) => editor.isActive(\"underline\"),\n      command: (editor) => editor.chain().focus().toggleUnderline().run(),\n      icon: UnderlineIcon,\n    },\n    {\n      name: \"strike\",\n      isActive: (editor) => editor.isActive(\"strike\"),\n      command: (editor) => editor.chain().focus().toggleStrike().run(),\n      icon: StrikethroughIcon,\n    },\n    {\n      name: \"code\",\n      isActive: (editor) => editor.isActive(\"code\"),\n      command: (editor) => editor.chain().focus().toggleCode().run(),\n      icon: CodeIcon,\n    },\n  ];\n  return (\n    <div className=\"flex\">\n      {items.map((item) => (\n        <EditorBubbleItem\n          key={item.name}\n          onSelect={(editor) => {\n            item.command(editor);\n          }}\n        >\n          <Button size=\"sm\" className=\"rounded-none\" variant=\"ghost\" type=\"button\">\n            <item.icon\n              className={cn(\"h-4 w-4\", {\n                \"text-blue-500\": item.isActive(editor),\n              })}\n            />\n          </Button>\n        </EditorBubbleItem>\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/web/components/tailwind/slash-command.tsx",
    "content": "import {\n  CheckSquare,\n  Code,\n  Heading1,\n  Heading2,\n  Heading3,\n  ImageIcon,\n  List,\n  ListOrdered,\n  MessageSquarePlus,\n  Text,\n  TextQuote,\n  Twitter,\n  Youtube,\n} from \"lucide-react\";\nimport { Command, createSuggestionItems, renderItems } from \"novel\";\nimport { uploadFn } from \"./image-upload\";\n\nexport const suggestionItems = createSuggestionItems([\n  {\n    title: \"Send Feedback\",\n    description: \"Let us know how we can improve.\",\n    icon: <MessageSquarePlus size={18} />,\n    command: ({ editor, range }) => {\n      editor.chain().focus().deleteRange(range).run();\n      window.open(\"/feedback\", \"_blank\");\n    },\n  },\n  {\n    title: \"Text\",\n    description: \"Just start typing with plain text.\",\n    searchTerms: [\"p\", \"paragraph\"],\n    icon: <Text size={18} />,\n    command: ({ editor, range }) => {\n      editor.chain().focus().deleteRange(range).toggleNode(\"paragraph\", \"paragraph\").run();\n    },\n  },\n  {\n    title: \"To-do List\",\n    description: \"Track tasks with a to-do list.\",\n    searchTerms: [\"todo\", \"task\", \"list\", \"check\", \"checkbox\"],\n    icon: <CheckSquare size={18} />,\n    command: ({ editor, range }) => {\n      editor.chain().focus().deleteRange(range).toggleTaskList().run();\n    },\n  },\n  {\n    title: \"Heading 1\",\n    description: \"Big section heading.\",\n    searchTerms: [\"title\", \"big\", \"large\"],\n    icon: <Heading1 size={18} />,\n    command: ({ editor, range }) => {\n      editor.chain().focus().deleteRange(range).setNode(\"heading\", { level: 1 }).run();\n    },\n  },\n  {\n    title: \"Heading 2\",\n    description: \"Medium section heading.\",\n    searchTerms: [\"subtitle\", \"medium\"],\n    icon: <Heading2 size={18} />,\n    command: ({ editor, range }) => {\n      editor.chain().focus().deleteRange(range).setNode(\"heading\", { level: 2 }).run();\n    },\n  },\n  {\n    title: \"Heading 3\",\n    description: \"Small section heading.\",\n    searchTerms: [\"subtitle\", \"small\"],\n    icon: <Heading3 size={18} />,\n    command: ({ editor, range }) => {\n      editor.chain().focus().deleteRange(range).setNode(\"heading\", { level: 3 }).run();\n    },\n  },\n  {\n    title: \"Bullet List\",\n    description: \"Create a simple bullet list.\",\n    searchTerms: [\"unordered\", \"point\"],\n    icon: <List size={18} />,\n    command: ({ editor, range }) => {\n      editor.chain().focus().deleteRange(range).toggleBulletList().run();\n    },\n  },\n  {\n    title: \"Numbered List\",\n    description: \"Create a list with numbering.\",\n    searchTerms: [\"ordered\"],\n    icon: <ListOrdered size={18} />,\n    command: ({ editor, range }) => {\n      editor.chain().focus().deleteRange(range).toggleOrderedList().run();\n    },\n  },\n  {\n    title: \"Quote\",\n    description: \"Capture a quote.\",\n    searchTerms: [\"blockquote\"],\n    icon: <TextQuote size={18} />,\n    command: ({ editor, range }) =>\n      editor.chain().focus().deleteRange(range).toggleNode(\"paragraph\", \"paragraph\").toggleBlockquote().run(),\n  },\n  {\n    title: \"Code\",\n    description: \"Capture a code snippet.\",\n    searchTerms: [\"codeblock\"],\n    icon: <Code size={18} />,\n    command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),\n  },\n  {\n    title: \"Image\",\n    description: \"Upload an image from your computer.\",\n    searchTerms: [\"photo\", \"picture\", \"media\"],\n    icon: <ImageIcon size={18} />,\n    command: ({ editor, range }) => {\n      editor.chain().focus().deleteRange(range).run();\n      // upload image\n      const input = document.createElement(\"input\");\n      input.type = \"file\";\n      input.accept = \"image/*\";\n      input.onchange = async () => {\n        if (input.files?.length) {\n          const file = input.files[0];\n          const pos = editor.view.state.selection.from;\n          uploadFn(file, editor.view, pos);\n        }\n      };\n      input.click();\n    },\n  },\n  {\n    title: \"Youtube\",\n    description: \"Embed a Youtube video.\",\n    searchTerms: [\"video\", \"youtube\", \"embed\"],\n    icon: <Youtube size={18} />,\n    command: ({ editor, range }) => {\n      const videoLink = prompt(\"Please enter Youtube Video Link\");\n      //From https://regexr.com/3dj5t\n      const ytregex = new RegExp(\n        /^((?:https?:)?\\/\\/)?((?:www|m)\\.)?((?:youtube\\.com|youtu.be))(\\/(?:[\\w\\-]+\\?v=|embed\\/|v\\/)?)([\\w\\-]+)(\\S+)?$/,\n      );\n\n      if (ytregex.test(videoLink)) {\n        editor\n          .chain()\n          .focus()\n          .deleteRange(range)\n          .setYoutubeVideo({\n            src: videoLink,\n          })\n          .run();\n      } else {\n        if (videoLink !== null) {\n          alert(\"Please enter a correct Youtube Video Link\");\n        }\n      }\n    },\n  },\n  {\n    title: \"Twitter\",\n    description: \"Embed a Tweet.\",\n    searchTerms: [\"twitter\", \"embed\"],\n    icon: <Twitter size={18} />,\n    command: ({ editor, range }) => {\n      const tweetLink = prompt(\"Please enter Twitter Link\");\n      const tweetRegex = new RegExp(/^https?:\\/\\/(www\\.)?x\\.com\\/([a-zA-Z0-9_]{1,15})(\\/status\\/(\\d+))?(\\/\\S*)?$/);\n\n      if (tweetRegex.test(tweetLink)) {\n        editor\n          .chain()\n          .focus()\n          .deleteRange(range)\n          .setTweet({\n            src: tweetLink,\n          })\n          .run();\n      } else {\n        if (tweetLink !== null) {\n          alert(\"Please enter a correct Twitter Link\");\n        }\n      }\n    },\n  },\n]);\n\nexport const slashCommand = Command.configure({\n  suggestion: {\n    items: () => suggestionItems,\n    render: renderItems,\n  },\n});\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive: \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        outline: \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;\n  },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/command.tsx",
    "content": "\"use client\";\n\nimport type { DialogProps } from \"@radix-ui/react-dialog\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport * as React from \"react\";\n\nimport { Dialog, DialogContent } from \"@/components/tailwind/ui/dialog\";\nimport Magic from \"@/components/tailwind/ui/icons/magic\";\nimport { cn } from \"@/lib/utils\";\n\nconst Command = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nCommand.displayName = CommandPrimitive.displayName;\n\ninterface CommandDialogProps extends DialogProps {}\n\nconst CommandDialog = ({ children, ...props }: CommandDialogProps) => {\n  return (\n    <Dialog {...props}>\n      <DialogContent className=\"overflow-hidden p-0 shadow-lg\">\n        <Command className=\"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n          {children}\n        </Command>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst CommandInput = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Input>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n  <div className=\"flex items-center border-b px-4\" cmdk-input-wrapper=\"\">\n    <Magic className=\"mr-2 h-4 w-4 shrink-0 text-purple-500 \" />\n    <CommandPrimitive.Input\n      ref={ref}\n      className={cn(\n        \"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  </div>\n));\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.List\n    ref={ref}\n    className={cn(\"max-h-[300px] overflow-y-auto overflow-x-hidden\", className)}\n    {...props}\n  />\n));\n\nCommandList.displayName = CommandPrimitive.List.displayName;\n\nconst CommandEmpty = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Empty>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>((props, ref) => <CommandPrimitive.Empty ref={ref} className=\"py-6 text-center text-sm\" {...props} />);\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Group>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Group\n    ref={ref}\n    className={cn(\n      \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Separator ref={ref} className={cn(\"-mx-1 h-px bg-border\", className)} {...props} />\n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\nconst CommandItem = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {\n  return <span className={cn(\"ml-auto text-xs tracking-widest text-muted-foreground\", className)} {...props} />;\n};\nCommandShortcut.displayName = \"CommandShortcut\";\n\nexport {\n  Command,\n  CommandDialog,\n  CommandInput,\n  CommandList,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandShortcut,\n  CommandSeparator,\n};\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col space-y-1.5 text-center sm:text-left\", className)} {...props} />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\", className)} {...props} />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\"text-lg font-semibold leading-none tracking-tight\", className)}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description ref={ref} className={cn(\"text-sm text-muted-foreground\", className)} {...props} />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/icons/crazy-spinner.tsx",
    "content": "const CrazySpinner = () => {\n  return (\n    <div className=\"flex items-center justify-center gap-0.5\">\n      <div className=\"h-1.5 w-1.5 animate-bounce rounded-full bg-purple-500 [animation-delay:-0.3s]\" />\n      <div className=\"h-1.5 w-1.5 animate-bounce rounded-full bg-purple-500 [animation-delay:-0.15s]\" />\n      <div className=\"h-1.5 w-1.5 animate-bounce rounded-full bg-purple-500\" />\n    </div>\n  );\n};\n\nexport default CrazySpinner;\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/icons/font-default.tsx",
    "content": "export default function FontDefault({ className }: { className?: string }) {\n  return (\n    <svg width=\"27\" height=\"17\" viewBox=\"0 0 27 17\" xmlns=\"http://www.w3.org/2000/svg\" className={className}>\n      <title>Font Default Icon</title>\n      <path\n        d=\"M2.67735 16.7224H0L6.01587 0H8.92994L14.9458 16.7224H12.2685L7.54229 3.03746H7.41169L2.67735 16.7224ZM3.12629 10.1739H11.8114V12.2968H3.12629V10.1739Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M20.8127 17C20.0182 17 19.2999 16.853 18.6578 16.5591C18.0156 16.2597 17.5068 15.8269 17.1314 15.2608C16.7613 14.6947 16.5763 14.0006 16.5763 13.1787C16.5763 12.471 16.7123 11.8886 16.9844 11.4313C17.2565 10.9741 17.6238 10.6121 18.0864 10.3453C18.5489 10.0786 19.0659 9.8772 19.6373 9.74111C20.2087 9.60503 20.7909 9.5016 21.3841 9.43084C22.1351 9.34374 22.7445 9.27298 23.2125 9.21854C23.6805 9.15866 24.0206 9.0634 24.2329 8.93276C24.4451 8.80211 24.5512 8.58982 24.5512 8.29587V8.23871C24.5512 7.52562 24.3499 6.9731 23.9472 6.58117C23.5499 6.18924 22.9568 5.99328 22.1677 5.99328C21.346 5.99328 20.6984 6.17563 20.225 6.54035C19.757 6.89962 19.4332 7.29971 19.2536 7.74063L16.9599 7.21806C17.232 6.45597 17.6293 5.84086 18.1517 5.37272C18.6795 4.89914 19.2863 4.5562 19.972 4.3439C20.6576 4.12616 21.3787 4.01729 22.1351 4.01729C22.6357 4.01729 23.1663 4.07717 23.7268 4.19693C24.2927 4.31124 24.8206 4.52354 25.3103 4.83381C25.8055 5.14409 26.2109 5.58774 26.5266 6.16475C26.8422 6.73631 27 7.47935 27 8.39385V16.7224H24.6165V15.0077H24.5186C24.3607 15.3234 24.124 15.6337 23.8084 15.9385C23.4928 16.2434 23.0874 16.4965 22.5922 16.6979C22.097 16.8993 21.5038 17 20.8127 17ZM21.3433 15.0403C22.0181 15.0403 22.5949 14.907 23.0738 14.6403C23.5581 14.3735 23.9254 14.0251 24.1757 13.5951C24.4315 13.1596 24.5594 12.6942 24.5594 12.1988V10.5821C24.4723 10.6692 24.3036 10.7509 24.0533 10.8271C23.8084 10.8979 23.5282 10.9605 23.2125 11.0149C22.8969 11.0639 22.5894 11.1102 22.2902 11.1537C21.9909 11.1918 21.7405 11.2245 21.5392 11.2517C21.0658 11.3116 20.6331 11.4123 20.2413 11.5538C19.855 11.6953 19.5448 11.8995 19.3108 12.1662C19.0822 12.4275 18.968 12.7759 18.968 13.2113C18.968 13.8156 19.1911 14.2728 19.6373 14.5831C20.0835 14.8879 20.6522 15.0403 21.3433 15.0403Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/icons/font-mono.tsx",
    "content": "export default function FontMono({ className }: { className?: string }) {\n  return (\n    <svg\n      width=\"28\"\n      height=\"19\"\n      viewBox=\"0 0 28 19\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={className}\n    >\n      <title>Font Mono Icon</title>\n      <path\n        d=\"M10.4513 13.9906H3.35401L3.80321 11.8861H9.94225L10.4513 13.9906ZM7.06738 5.72075L2.90481 18.6739H0L6.4984 0H7.66631L14.1647 18.6739H11.1701L7.06738 5.72075Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M20.6032 19C19.066 19 17.8681 18.6542 17.0096 17.9626C16.1711 17.2512 15.7519 16.3323 15.7519 15.2059C15.7519 14.455 15.9216 13.7832 16.261 13.1903C16.6203 12.5777 17.1194 12.0541 17.7583 11.6193C18.4171 11.1846 19.2057 10.8487 20.1241 10.6115C21.0424 10.3547 22.0806 10.2262 23.2385 10.2262C23.4781 10.2262 23.7276 10.2361 23.9872 10.2559C24.2667 10.2559 24.5562 10.2657 24.8556 10.2855C25.175 10.3053 25.4945 10.325 25.8139 10.3448L25.9037 12.4493C25.6242 12.4098 25.3248 12.3801 25.0053 12.3604C24.7059 12.3406 24.4064 12.3307 24.107 12.3307C23.8075 12.3307 23.528 12.3307 23.2685 12.3307C22.4898 12.3307 21.8111 12.39 21.2321 12.5086C20.6731 12.6074 20.2039 12.7655 19.8246 12.9828C19.4652 13.2002 19.1957 13.467 19.016 13.7832C18.8364 14.0993 18.7465 14.4649 18.7465 14.8799C18.7465 15.2158 18.8064 15.5122 18.9262 15.7691C19.046 16.0062 19.2057 16.194 19.4053 16.3323C19.625 16.4706 19.8745 16.5793 20.154 16.6583C20.4535 16.7176 20.7729 16.7473 21.1123 16.7473C21.7711 16.7473 22.3501 16.6386 22.8492 16.4212C23.3483 16.2038 23.7676 15.8877 24.107 15.4727C24.4663 15.038 24.7358 14.5044 24.9155 13.8721C25.0952 13.22 25.185 12.4592 25.185 11.5897C25.185 10.3843 25.0453 9.44566 24.7658 8.77379C24.5062 8.10192 24.1169 7.63755 23.5979 7.38066C23.0788 7.12376 22.4299 6.99532 21.6513 6.99532C21.0125 6.99532 20.3936 7.104 19.7947 7.32137C19.2157 7.51898 18.6467 7.89444 18.0877 8.44774L16.5005 6.66927C17.2592 5.95788 18.0877 5.4441 18.9861 5.12793C19.9045 4.81175 20.8428 4.65367 21.8011 4.65367C22.6795 4.65367 23.4881 4.76235 24.2267 4.97972C24.9854 5.17733 25.6442 5.51326 26.2032 5.98752C26.7822 6.46178 27.2214 7.104 27.5209 7.9142C27.8403 8.72439 28 9.75195 28 10.9969V18.6739H24.8856V16.9844C24.626 17.3796 24.3365 17.7155 24.0171 17.9922C23.6977 18.2491 23.3483 18.4566 22.969 18.6147C22.6096 18.753 22.2303 18.8518 21.831 18.9111C21.4317 18.9704 21.0225 19 20.6032 19Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/icons/font-serif.tsx",
    "content": "export default function FontSerif({ className }: { className?: string }) {\n  return (\n    <svg\n      width=\"31\"\n      height=\"18\"\n      viewBox=\"0 0 31 18\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={className}\n    >\n      <title>Font Serif Icon</title>\n\n      <path\n        d=\"M0.108486 17.8929C0.0361621 17.8214 0 17.6875 0 17.4911C0 17.1696 0.0180811 17.0089 0.0542432 17.0089C0.198892 17.0089 0.406824 16.9732 0.67804 16.9018C0.967337 16.8304 1.23855 16.7411 1.49169 16.6339C1.7629 16.5089 1.93467 16.3839 2.007 16.2589C2.18781 15.9911 2.35958 15.6875 2.52231 15.3482C2.68504 14.9911 2.85681 14.5982 3.03762 14.1696L8.08224 2.11607C8.51619 1.02679 8.85069 0.383929 9.08574 0.1875C9.21231 0.0625 9.366 0 9.54681 0C9.67337 0 9.78186 0.0446429 9.87227 0.133929L15.5678 14.1696C15.7125 14.5089 15.8481 14.8482 15.9746 15.1875C16.1193 15.5089 16.2549 15.8214 16.3815 16.125C16.4538 16.3036 16.6165 16.4643 16.8696 16.6071C17.1228 16.7321 17.385 16.8304 17.6562 16.9018C17.9274 16.9732 18.1353 17.0089 18.28 17.0089C18.3523 17.0089 18.3885 17.1696 18.3885 17.4911C18.3885 17.6875 18.3704 17.8214 18.3342 17.8929L14.9169 17.7321L11.3911 17.8929C11.3188 17.8214 11.2826 17.6518 11.2826 17.3839C11.2826 17.1339 11.3188 17.0089 11.3911 17.0089C11.608 17.0089 11.9064 16.9554 12.2861 16.8482C12.6658 16.7411 12.8918 16.6161 12.9641 16.4732C13.0184 16.3661 13.0455 16.25 13.0455 16.125C13.0455 15.9464 13.0003 15.7054 12.9099 15.4018C12.8195 15.0982 12.6929 14.7232 12.5302 14.2768L11.5809 11.7857C11.5086 11.7143 11.4453 11.6786 11.3911 11.6786C11.1018 11.6607 10.7582 11.6518 10.3605 11.6518C9.98075 11.6339 9.54681 11.625 9.05862 11.625C8.08224 11.625 7.05162 11.6518 5.96675 11.7054C5.85827 11.7054 5.80402 11.7411 5.80402 11.8125L4.7734 14.3036C4.59259 14.75 4.45698 15.125 4.36658 15.4286C4.29425 15.7143 4.25809 15.9196 4.25809 16.0446C4.25809 16.2589 4.35754 16.4375 4.55643 16.5804C4.66492 16.6518 4.82765 16.7232 5.04462 16.7946C5.27967 16.8482 5.51473 16.9018 5.74978 16.9554C5.98484 16.9911 6.14756 17.0089 6.23797 17.0089C6.29221 17.0268 6.31933 17.1518 6.31933 17.3839C6.31933 17.5089 6.31029 17.6161 6.29221 17.7054C6.29221 17.7768 6.28317 17.8393 6.26509 17.8929C5.59609 17.8571 5.04462 17.8214 4.61067 17.7857C4.19481 17.75 3.80607 17.7321 3.44444 17.7321C3.08282 17.7321 2.64888 17.7411 2.14261 17.7589C1.63634 17.7768 0.958297 17.8214 0.108486 17.8929ZM8.57043 10.1786C9.22135 10.1786 9.96267 10.1518 10.7944 10.0982L10.8486 9.99107L8.7874 4.63393L6.56343 9.99107C6.56343 10.0625 6.58151 10.0982 6.61767 10.0982C7.25051 10.1518 7.90143 10.1786 8.57043 10.1786Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M23.0534 18C22.1493 18 21.408 17.7589 20.8294 17.2768C20.2689 16.7768 19.9886 16.0982 19.9886 15.2411C19.9886 14.6518 20.1785 14.1161 20.5582 13.6339C20.956 13.1518 21.5074 12.7411 22.2126 12.4018C22.4657 12.2768 22.6917 12.1786 22.8906 12.1071C23.1076 12.0179 23.3065 11.9375 23.4873 11.8661C24.1202 11.6696 24.6445 11.5089 25.0604 11.3839C25.4943 11.2589 25.8017 11.1696 25.9825 11.1161C26.1452 11.0268 26.2266 10.8839 26.2266 10.6875V9.21429C26.2266 8.58929 26.0819 8.09821 25.7927 7.74107C25.5034 7.38393 25.1146 7.20536 24.6264 7.20536C24.084 7.20536 23.6591 7.375 23.3517 7.71429C23.0443 8.03571 22.8906 8.5 22.8906 9.10714C22.8906 9.46429 22.755 9.73214 22.4838 9.91071C22.2126 10.0714 21.9052 10.1518 21.5617 10.1518C20.8927 10.1518 20.4226 9.9375 20.1514 9.50893C20.1514 9.08036 20.3141 8.66071 20.6395 8.25C20.965 7.82143 21.3899 7.4375 21.9143 7.09821C22.4567 6.75893 23.0534 6.49107 23.7043 6.29464C24.3552 6.08036 25.0061 5.97321 25.657 5.97321C26.7781 5.97321 27.6188 6.26786 28.1794 6.85714C28.7399 7.42857 29.0201 8.41964 29.0201 9.83036V15.1071C29.0201 15.8393 29.3185 16.2054 29.9151 16.2054C30.1864 16.2054 30.4937 16.0893 30.8373 15.8571C30.9458 15.875 31 15.9732 31 16.1518C31 16.4911 30.9458 16.7589 30.8373 16.9554C30.5661 17.1696 30.2044 17.3929 29.7524 17.625C29.3004 17.8571 28.8664 17.9732 28.4506 17.9732C28.0347 17.9732 27.6279 17.8214 27.2301 17.5179C26.8323 17.2143 26.5701 16.8839 26.4436 16.5268C26.2989 16.6518 26.1904 16.7411 26.1181 16.7946C26.0458 16.8482 25.9283 16.9375 25.7655 17.0625C25.3858 17.3125 24.97 17.5357 24.5179 17.7321C24.0659 17.9107 23.5777 18 23.0534 18ZM24.4366 16.2589C24.9248 16.2589 25.3406 16.1339 25.6842 15.8839C26.0458 15.6339 26.2266 15.3482 26.2266 15.0268V12.2946L24.4908 12.8839C24.0749 13.0625 23.7043 13.2768 23.3788 13.5268C23.0534 13.7768 22.8906 14.1339 22.8906 14.5982C22.8906 15.1696 23.0353 15.5893 23.3246 15.8571C23.6139 16.125 23.9845 16.2589 24.4366 16.2589Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/icons/index.tsx",
    "content": "export { default as FontDefault } from \"./font-default\";\nexport { default as FontSerif } from \"./font-serif\";\nexport { default as FontMono } from \"./font-mono\";\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/icons/loading-circle.tsx",
    "content": "export default function LoadingCircle({ dimensions }: { dimensions?: string }) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className={`${dimensions || \"h-4 w-4\"} animate-spin fill-stone-600 text-stone-200`}\n      viewBox=\"0 0 100 101\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z\"\n        fill=\"currentFill\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/icons/magic.tsx",
    "content": "export default function Magic({ className }: { className: string }) {\n  return (\n    <svg\n      width=\"469\"\n      height=\"469\"\n      viewBox=\"0 0 469 469\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      shapeRendering=\"geometricPrecision\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      strokeWidth=\"1.5\"\n      className={className}\n    >\n      <title>Magic AI icon</title>\n\n      <path\n        d=\"M237.092 62.3004L266.754 71.4198C267.156 71.5285 267.51 71.765 267.765 72.0934C268.02 72.4218 268.161 72.8243 268.166 73.2399C268.172 73.6555 268.042 74.0616 267.796 74.3967C267.55 74.7318 267.201 74.9777 266.803 75.097L237.141 84.3145C236.84 84.4058 236.566 84.5699 236.344 84.7922C236.121 85.0146 235.957 85.2883 235.866 85.5893L226.747 115.252C226.638 115.653 226.401 116.008 226.073 116.263C225.745 116.517 225.342 116.658 224.926 116.664C224.511 116.669 224.105 116.539 223.77 116.293C223.435 116.047 223.189 115.699 223.069 115.301L213.852 85.6383C213.761 85.3374 213.597 85.0636 213.374 84.8412C213.152 84.6189 212.878 84.4548 212.577 84.3635L182.914 75.2441C182.513 75.1354 182.158 74.8989 181.904 74.5705C181.649 74.2421 181.508 73.8396 181.503 73.424C181.497 73.0084 181.627 72.6023 181.873 72.2672C182.119 71.9321 182.467 71.6863 182.865 71.5669L212.528 62.3494C212.829 62.2582 213.103 62.0941 213.325 61.8717C213.547 61.6494 213.712 61.3756 213.803 61.0747L222.922 31.4121C223.031 31.0109 223.267 30.656 223.596 30.4013C223.924 30.1465 224.327 30.0057 224.742 30.0002C225.158 29.9946 225.564 30.1247 225.899 30.3706C226.234 30.6165 226.48 30.9649 226.599 31.363L235.817 61.0257C235.908 61.3266 236.072 61.6003 236.295 61.8227C236.517 62.0451 236.791 62.2091 237.092 62.3004Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M155.948 155.848L202.771 168.939C203.449 169.131 204.045 169.539 204.47 170.101C204.895 170.663 205.125 171.348 205.125 172.052C205.125 172.757 204.895 173.442 204.47 174.004C204.045 174.566 203.449 174.974 202.771 175.166L155.899 188.06C155.361 188.209 154.87 188.496 154.475 188.891C154.079 189.286 153.793 189.777 153.644 190.316L140.553 237.138C140.361 237.816 139.953 238.413 139.391 238.838C138.829 239.262 138.144 239.492 137.44 239.492C136.735 239.492 136.05 239.262 135.488 238.838C134.927 238.413 134.519 237.816 134.327 237.138L121.432 190.267C121.283 189.728 120.997 189.237 120.601 188.842C120.206 188.446 119.715 188.16 119.177 188.011L72.3537 174.92C71.676 174.728 71.0795 174.32 70.6547 173.759C70.2299 173.197 70 172.512 70 171.807C70 171.103 70.2299 170.418 70.6547 169.856C71.0795 169.294 71.676 168.886 72.3537 168.694L119.226 155.799C119.764 155.65 120.255 155.364 120.65 154.969C121.046 154.573 121.332 154.082 121.481 153.544L134.572 106.721C134.764 106.043 135.172 105.447 135.734 105.022C136.295 104.597 136.981 104.367 137.685 104.367C138.389 104.367 139.075 104.597 139.637 105.022C140.198 105.447 140.606 106.043 140.798 106.721L153.693 153.593C153.842 154.131 154.128 154.622 154.524 155.018C154.919 155.413 155.41 155.699 155.948 155.848Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M386.827 289.992C404.33 292.149 403.84 305.828 386.876 307.299C346.623 310.829 298.869 316.271 282.199 360.005C274.844 379.192 269.942 403.2 267.49 432.029C267.427 432.846 267.211 433.626 266.856 434.319C266.501 435.012 266.015 435.602 265.431 436.05C254.988 444.041 251.212 434.186 250.183 425.606C239.2 332.353 214.588 316.909 124.668 306.122C123.892 306.031 123.151 305.767 122.504 305.35C121.857 304.933 121.322 304.375 120.942 303.72C116.399 295.679 119.324 291.038 129.718 289.796C224.688 278.47 236.062 262.83 250.183 169.331C252.177 156.355 257.259 154.083 265.431 162.516C266.51 163.593 267.202 165.099 267.392 166.782C279.257 258.564 293.328 278.617 386.827 289.992Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/menu.tsx",
    "content": "\"use client\";\n\nimport { Check, Menu as MenuIcon, Monitor, Moon, SunDim } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { Button } from \"./button\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"./popover\";\n\n// TODO implement multiple fonts editor\n// const fonts = [\n//   {\n//     font: \"Default\",\n//     icon: <FontDefault className=\"h-4 w-4\" />,\n//   },\n//   {\n//     font: \"Serif\",\n//     icon: <FontSerif className=\"h-4 w-4\" />,\n//   },\n//   {\n//     font: \"Mono\",\n//     icon: <FontMono className=\"h-4 w-4\" />,\n//   },\n// ];\nconst appearances = [\n  {\n    theme: \"System\",\n    icon: <Monitor className=\"h-4 w-4\" />,\n  },\n  {\n    theme: \"Light\",\n    icon: <SunDim className=\"h-4 w-4\" />,\n  },\n  {\n    theme: \"Dark\",\n    icon: <Moon className=\"h-4 w-4\" />,\n  },\n];\nexport default function Menu() {\n  // const { font: currentFont, setFont } = useContext(AppContext);\n  const { theme: currentTheme, setTheme } = useTheme();\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon\">\n          <MenuIcon width={16} />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-52 p-2\" align=\"end\">\n        {/* <div className=\"p-2\">\n          <p className=\"p-2 text-xs font-medium text-stone-500\">Font</p>\n          {fonts.map(({ font, icon }) => (\n            <button\n              key={font}\n              className=\"flex w-full items-center justify-between rounded px-2 py-1 text-sm text-stone-600 hover:bg-stone-100\"\n              onClick={() => {\n                setFont(font);\n              }}\n            >\n              <div className=\"flex items-center space-x-2\">\n                <div className=\"rounded-sm border border-stone-200 p-1\">\n                  {icon}\n                </div>\n                <span>{font}</span>\n              </div>\n              {currentFont === font && <Check className=\"h-4 w-4\" />}\n            </button>\n          ))}\n        </div> */}\n        <p className=\"p-2 text-xs font-medium text-muted-foreground\">Appearance</p>\n        {appearances.map(({ theme, icon }) => (\n          <Button\n            variant=\"ghost\"\n            key={theme}\n            className=\"flex w-full items-center justify-between rounded px-2 py-1.5 text-sm\"\n            onClick={() => {\n              setTheme(theme.toLowerCase());\n            }}\n          >\n            <div className=\"flex items-center space-x-2\">\n              <div className=\"rounded-sm border  p-1\">{icon}</div>\n              <span>{theme}</span>\n            </div>\n            {currentTheme === theme.toLowerCase() && <Check className=\"h-4 w-4\" />}\n          </Button>\n        ))}\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/popover.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n));\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { Popover, PopoverTrigger, PopoverContent };\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/scroll-area.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <ScrollAreaPrimitive.Root ref={ref} className={cn(\"relative overflow-hidden\", className)} {...props}>\n    <ScrollAreaPrimitive.Viewport className=\"h-full w-full rounded-[inherit]\">{children}</ScrollAreaPrimitive.Viewport>\n    <ScrollBar />\n    <ScrollAreaPrimitive.Corner />\n  </ScrollAreaPrimitive.Root>\n));\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation}\n    className={cn(\n      \"flex touch-none select-none transition-colors\",\n      orientation === \"vertical\" && \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n      orientation === \"horizontal\" && \"h-2.5 flex-col border-t border-t-transparent p-[1px]\",\n      className,\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-border\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n));\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;\n\nexport { ScrollArea, ScrollBar };\n"
  },
  {
    "path": "apps/web/components/tailwind/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(({ className, orientation = \"horizontal\", decorative = true, ...props }, ref) => (\n  <SeparatorPrimitive.Root\n    ref={ref}\n    decorative={decorative}\n    orientation={orientation}\n    className={cn(\"shrink-0 bg-border\", orientation === \"horizontal\" ? \"h-[1px] w-full\" : \" w-[1px]\", className)}\n    {...props}\n  />\n));\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n"
  },
  {
    "path": "apps/web/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"styles/globals.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components/tailwind\",\n    \"utils\": \"@/lib/utils\"\n  }\n}\n"
  },
  {
    "path": "apps/web/hooks/use-local-storage.ts",
    "content": "import { useEffect, useState } from \"react\";\n\nconst useLocalStorage = <T>(\n  key: string,\n  initialValue: T,\n  // eslint-disable-next-line no-unused-vars\n): [T, (value: T) => void] => {\n  const [storedValue, setStoredValue] = useState(initialValue);\n\n  useEffect(() => {\n    // Retrieve from localStorage\n    const item = window.localStorage.getItem(key);\n    if (item) {\n      setStoredValue(JSON.parse(item));\n    }\n  }, [key]);\n\n  const setValue = (value: T) => {\n    // Save state\n    setStoredValue(value);\n    // Save to localStorage\n    window.localStorage.setItem(key, JSON.stringify(value));\n  };\n  return [storedValue, setValue];\n};\n\nexport default useLocalStorage;\n"
  },
  {
    "path": "apps/web/lib/content.ts",
    "content": "export const defaultEditorContent = {\n  type: \"doc\",\n  content: [\n    {\n      type: \"heading\",\n      attrs: { level: 2 },\n      content: [{ type: \"text\", text: \"Introducing Novel\" }],\n    },\n    {\n      type: \"paragraph\",\n      content: [\n        {\n          type: \"text\",\n          marks: [\n            {\n              type: \"link\",\n              attrs: {\n                href: \"https://github.com/steven-tey/novel\",\n                target: \"_blank\",\n              },\n            },\n          ],\n          text: \"Novel\",\n        },\n        {\n          type: \"text\",\n          text: \" is a Notion-style WYSIWYG editor with AI-powered autocompletion. Built with \",\n        },\n        {\n          type: \"text\",\n          marks: [\n            {\n              type: \"link\",\n              attrs: {\n                href: \"https://tiptap.dev/\",\n                target: \"_blank\",\n              },\n            },\n          ],\n          text: \"Tiptap\",\n        },\n        { type: \"text\", text: \" + \" },\n        {\n          type: \"text\",\n          marks: [\n            {\n              type: \"link\",\n              attrs: {\n                href: \"https://sdk.vercel.ai/docs\",\n                target: \"_blank\",\n              },\n            },\n          ],\n          text: \"Vercel AI SDK\",\n        },\n        { type: \"text\", text: \".\" },\n      ],\n    },\n    {\n      type: \"heading\",\n      attrs: { level: 3 },\n      content: [{ type: \"text\", text: \"Installation\" }],\n    },\n    {\n      type: \"codeBlock\",\n      attrs: { language: null },\n      content: [{ type: \"text\", text: \"npm i novel\" }],\n    },\n    {\n      type: \"heading\",\n      attrs: { level: 3 },\n      content: [{ type: \"text\", text: \"Usage\" }],\n    },\n    {\n      type: \"codeBlock\",\n      attrs: { language: null },\n      content: [\n        {\n          type: \"text\",\n          text: 'import { Editor } from \"novel\";\\n\\nexport default function App() {\\n  return (\\n     <Editor />\\n  )\\n}',\n        },\n      ],\n    },\n    {\n      type: \"heading\",\n      attrs: { level: 3 },\n      content: [{ type: \"text\", text: \"Features\" }],\n    },\n    {\n      type: \"orderedList\",\n      attrs: { tight: true, start: 1 },\n      content: [\n        {\n          type: \"listItem\",\n          content: [\n            {\n              type: \"paragraph\",\n              content: [{ type: \"text\", text: \"Slash menu & bubble menu\" }],\n            },\n          ],\n        },\n        {\n          type: \"listItem\",\n          content: [\n            {\n              type: \"paragraph\",\n              content: [\n                { type: \"text\", text: \"AI autocomplete (type \" },\n                { type: \"text\", marks: [{ type: \"code\" }], text: \"++\" },\n                {\n                  type: \"text\",\n                  text: \" to activate, or select from slash menu)\",\n                },\n              ],\n            },\n          ],\n        },\n        {\n          type: \"listItem\",\n          content: [\n            {\n              type: \"paragraph\",\n              content: [\n                {\n                  type: \"text\",\n                  text: \"Image uploads (drag & drop / copy & paste, or select from slash menu) \",\n                },\n              ],\n            },\n          ],\n        },\n        {\n          type: \"listItem\",\n          content: [\n            {\n              type: \"paragraph\",\n              content: [\n                {\n                  type: \"text\",\n                  text: \"Add tweets from the command slash menu:\",\n                },\n              ],\n            },\n            {\n              type: \"twitter\",\n              attrs: {\n                src: \"https://x.com/elonmusk/status/1800759252224729577\",\n              },\n            },\n          ],\n        },\n        {\n          type: \"listItem\",\n          content: [\n            {\n              type: \"paragraph\",\n              content: [\n                {\n                  type: \"text\",\n                  text: \"Mathematical symbols with LaTeX expression:\",\n                },\n              ],\n            },\n            {\n              type: \"orderedList\",\n              attrs: {\n                tight: true,\n                start: 1,\n              },\n              content: [\n                {\n                  type: \"listItem\",\n                  content: [\n                    {\n                      type: \"paragraph\",\n                      content: [\n                        {\n                          type: \"math\",\n                          attrs: {\n                            latex: \"E = mc^2\",\n                          },\n                        },\n                      ],\n                    },\n                  ],\n                },\n                {\n                  type: \"listItem\",\n                  content: [\n                    {\n                      type: \"paragraph\",\n                      content: [\n                        {\n                          type: \"math\",\n                          attrs: {\n                            latex: \"a^2 = \\\\sqrt{b^2 + c^2}\",\n                          },\n                        },\n                      ],\n                    },\n                  ],\n                },\n                {\n                  type: \"listItem\",\n                  content: [\n                    {\n                      type: \"paragraph\",\n                      content: [\n                        {\n                          type: \"math\",\n                          attrs: {\n                            latex:\n                              \"\\\\hat{f} (\\\\xi)=\\\\int_{-\\\\infty}^{\\\\infty}f(x)e^{-2\\\\pi ix\\\\xi}dx\",\n                          },\n                        },\n                      ],\n                    },\n                  ],\n                },\n                {\n                  type: \"listItem\",\n                  content: [\n                    {\n                      type: \"paragraph\",\n                      content: [\n                        {\n                          type: \"math\",\n                          attrs: {\n                            latex:\n                              \"A=\\\\begin{bmatrix}a&b\\\\\\\\c&d \\\\end{bmatrix}\",\n                          },\n                        },\n                      ],\n                    },\n                  ],\n                },\n                {\n                  type: \"listItem\",\n                  content: [\n                    {\n                      type: \"paragraph\",\n                      content: [\n                        {\n                          type: \"math\",\n                          attrs: {\n                            latex: \"\\\\sum_{i=0}^n x_i\",\n                          },\n                        },\n                      ],\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n        },\n      ],\n    },\n    {\n      type: \"image\",\n      attrs: {\n        src: \"https://public.blob.vercel-storage.com/pJrjXbdONOnAeZAZ/banner-2wQk82qTwyVgvlhTW21GIkWgqPGD2C.png\",\n        alt: \"banner.png\",\n        title: \"banner.png\",\n        width: null,\n        height: null,\n      },\n    },\n    { type: \"horizontalRule\" },\n    {\n      type: \"heading\",\n      attrs: { level: 3 },\n      content: [{ type: \"text\", text: \"Learn more\" }],\n    },\n    {\n      type: \"taskList\",\n      content: [\n        {\n          type: \"taskItem\",\n          attrs: { checked: false },\n          content: [\n            {\n              type: \"paragraph\",\n              content: [\n                { type: \"text\", text: \"Star us on \" },\n                {\n                  type: \"text\",\n                  marks: [\n                    {\n                      type: \"link\",\n                      attrs: {\n                        href: \"https://github.com/steven-tey/novel\",\n                        target: \"_blank\",\n                      },\n                    },\n                  ],\n                  text: \"GitHub\",\n                },\n              ],\n            },\n          ],\n        },\n        {\n          type: \"taskItem\",\n          attrs: { checked: false },\n          content: [\n            {\n              type: \"paragraph\",\n              content: [\n                { type: \"text\", text: \"Install the \" },\n                {\n                  type: \"text\",\n                  marks: [\n                    {\n                      type: \"link\",\n                      attrs: {\n                        href: \"https://www.npmjs.com/package/novel\",\n                        target: \"_blank\",\n                      },\n                    },\n                  ],\n                  text: \"NPM package\",\n                },\n              ],\n            },\n          ],\n        },\n        {\n          type: \"taskItem\",\n          attrs: { checked: false },\n          content: [\n            {\n              type: \"paragraph\",\n              content: [\n                {\n                  type: \"text\",\n                  marks: [\n                    {\n                      type: \"link\",\n                      attrs: {\n                        href: \"https://vercel.com/templates/next.js/novel\",\n                        target: \"_blank\",\n                      },\n                    },\n                  ],\n                  text: \"Deploy your own\",\n                },\n                { type: \"text\", text: \" to Vercel\" },\n              ],\n            },\n          ],\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/web/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "apps/web/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  redirects: async () => {\n    return [\n      {\n        source: \"/github\",\n        destination: \"https://github.com/steven-tey/novel\",\n        permanent: true,\n      },\n      {\n        source: \"/sdk\",\n        destination: \"https://www.npmjs.com/package/novel\",\n        permanent: true,\n      },\n      {\n        source: \"/npm\",\n        destination: \"https://www.npmjs.com/package/novel\",\n        permanent: true,\n      },\n      {\n        source: \"/svelte\",\n        destination: \"https://github.com/tglide/novel-svelte\",\n        permanent: false,\n      },\n      {\n        source: \"/vue\",\n        destination: \"https://github.com/naveennaidu/novel-vue\",\n        permanent: false,\n      },\n      {\n        source: \"/vscode\",\n        destination:\n          \"https://marketplace.visualstudio.com/items?itemName=bennykok.novel-vscode\",\n        permanent: false,\n      },\n      {\n        source: \"/feedback\",\n        destination: \"https://github.com/steven-tey/novel/issues\",\n        permanent: true,\n      },\n      {\n        source: \"/deploy\",\n        destination: \"https://vercel.com/templates/next.js/novel\",\n        permanent: true,\n      },\n    ];\n  },\n  productionBrowserSourceMaps: true,\n};\n\nmodule.exports = nextConfig;\n"
  },
  {
    "path": "apps/web/package.json",
    "content": "{\n  \"name\": \"novel-next-app\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"biome lint .\",\n    \"format\": \"biome format . \",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/openai\": \"^1.1.0\",\n    \"@radix-ui/react-dialog\": \"^1.0.5\",\n    \"@radix-ui/react-popover\": \"^1.0.7\",\n    \"@radix-ui/react-scroll-area\": \"^1.0.5\",\n    \"@radix-ui/react-select\": \"^2.0.0\",\n    \"@radix-ui/react-separator\": \"^1.0.3\",\n    \"@radix-ui/react-slot\": \"^1.0.2\",\n    \"@tailwindcss/typography\": \"^0.5.10\",\n    \"@upstash/ratelimit\": \"^1.0.1\",\n    \"@vercel/analytics\": \"^1.2.2\",\n    \"@vercel/blob\": \"^0.22.1\",\n    \"@vercel/kv\": \"^1.0.1\",\n    \"ai\": \"^3.0.12\",\n    \"autoprefixer\": \"^10.4.17\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.0\",\n    \"cmdk\": \"^1.0.4\",\n    \"eventsource-parser\": \"^1.1.2\",\n    \"highlight.js\": \"^11.9.0\",\n    \"lowlight\": \"^3.1.0\",\n    \"lucide-react\": \"^0.358.0\",\n    \"next\": \"15.1.4\",\n    \"next-themes\": \"^0.2.1\",\n    \"novel\": \"workspace:^\",\n    \"openai\": \"^4.28.4\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\",\n    \"react-markdown\": \"^9.0.1\",\n    \"sonner\": \"^1.4.3\",\n    \"tailwind-merge\": \"^2.2.1\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"tippy.js\": \"^6.3.7\",\n    \"ts-pattern\": \"^5.0.8\",\n    \"typescript\": \"^5.4.2\",\n    \"use-debounce\": \"^10.0.0\"\n  },\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"^1.7.2\",\n    \"@types/node\": \"20.11.24\",\n    \"@types/react\": \"^18.2.61\",\n    \"@types/react-dom\": \"18.2.19\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"tsconfig\": \"workspace:*\"\n  }\n}"
  },
  {
    "path": "apps/web/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    \"postcss-import\": {},\n    \"tailwindcss/nesting\": {},\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "apps/web/styles/fonts.ts",
    "content": "import localFont from \"next/font/local\";\nimport { Crimson_Text, Inconsolata, Inter } from \"next/font/google\";\n\nexport const cal = localFont({\n  src: \"./CalSans-SemiBold.otf\",\n  variable: \"--font-title\",\n});\n\nexport const crimsonBold = Crimson_Text({\n  weight: \"700\",\n  variable: \"--font-title\",\n  subsets: [\"latin\"],\n});\n\nexport const inter = Inter({\n  variable: \"--font-default\",\n  subsets: [\"latin\"],\n});\n\nexport const inconsolataBold = Inconsolata({\n  weight: \"700\",\n  variable: \"--font-title\",\n  subsets: [\"latin\"],\n});\n\nexport const crimson = Crimson_Text({\n  weight: \"400\",\n  variable: \"--font-default\",\n  subsets: [\"latin\"],\n});\n\nexport const inconsolata = Inconsolata({\n  variable: \"--font-default\",\n  subsets: [\"latin\"],\n});\n\nexport const titleFontMapper = {\n  Default: cal.variable,\n  Serif: crimsonBold.variable,\n  Mono: inconsolataBold.variable,\n};\n\nexport const defaultFontMapper = {\n  Default: inter.variable,\n  Serif: crimson.variable,\n  Mono: inconsolata.variable,\n};\n"
  },
  {
    "path": "apps/web/styles/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 84% 4.9%;\n\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 84% 4.9%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 84% 4.9%;\n\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n\n    --secondary: 210 40% 96.1%;\n    --secondary-foreground: 222.2 47.4% 11.2%;\n\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n\n    --accent: 210 40% 96.1%;\n    --accent-foreground: 222.2 47.4% 11.2%;\n\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --ring: 222.2 84% 4.9%;\n\n    --radius: 0.5rem;\n\n    --novel-highlight-default: #ffffff;\n    --novel-highlight-purple: #f6f3f8;\n    --novel-highlight-red: #fdebeb;\n    --novel-highlight-yellow: #fbf4a2;\n    --novel-highlight-blue: #c1ecf9;\n    --novel-highlight-green: #acf79f;\n    --novel-highlight-orange: #faebdd;\n    --novel-highlight-pink: #faf1f5;\n    --novel-highlight-gray: #f1f1ef;\n  }\n\n  .dark {\n    --background: 222.2 84% 4.9%;\n    --foreground: 210 40% 98%;\n\n    --card: 222.2 84% 4.9%;\n    --card-foreground: 210 40% 98%;\n\n    --popover: 222.2 84% 4.9%;\n    --popover-foreground: 210 40% 98%;\n\n    --primary: 210 40% 98%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n\n    --secondary: 217.2 32.6% 17.5%;\n    --secondary-foreground: 210 40% 98%;\n\n    --muted: 217.2 32.6% 17.5%;\n    --muted-foreground: 215 20.2% 65.1%;\n\n    --accent: 217.2 32.6% 17.5%;\n    --accent-foreground: 210 40% 98%;\n\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 217.2 32.6% 17.5%;\n    --input: 217.2 32.6% 17.5%;\n    --ring: 212.7 26.8% 83.9%;\n\n    --novel-highlight-default: #000000;\n    --novel-highlight-purple: #3f2c4b;\n    --novel-highlight-red: #5c1a1a;\n    --novel-highlight-yellow: #5c4b1a;\n    --novel-highlight-blue: #1a3d5c;\n    --novel-highlight-green: #1a5c20;\n    --novel-highlight-orange: #5c3a1a;\n    --novel-highlight-pink: #5c1a3a;\n    --novel-highlight-gray: #3a3a3a;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n\npre {\n  background: #0d0d0d;\n  border-radius: 0.5rem;\n  color: #fff;\n  font-family: \"JetBrainsMono\", monospace;\n  padding: 0.75rem 1rem;\n\n  code {\n    background: none;\n    color: inherit;\n    font-size: 0.8rem;\n    padding: 0;\n  }\n\n  .hljs-comment,\n  .hljs-quote {\n    color: #616161;\n  }\n\n  .hljs-variable,\n  .hljs-template-variable,\n  .hljs-attribute,\n  .hljs-tag,\n  .hljs-name,\n  .hljs-regexp,\n  .hljs-link,\n  .hljs-name,\n  .hljs-selector-id,\n  .hljs-selector-class {\n    color: #f98181;\n  }\n\n  .hljs-number,\n  .hljs-meta,\n  .hljs-built_in,\n  .hljs-builtin-name,\n  .hljs-literal,\n  .hljs-type,\n  .hljs-params {\n    color: #fbbc88;\n  }\n\n  .hljs-string,\n  .hljs-symbol,\n  .hljs-bullet {\n    color: #b9f18d;\n  }\n\n  .hljs-title,\n  .hljs-section {\n    color: #faf594;\n  }\n\n  .hljs-keyword,\n  .hljs-selector-tag {\n    color: #70cff8;\n  }\n\n  .hljs-emphasis {\n    font-style: italic;\n  }\n\n  .hljs-strong {\n    font-weight: 700;\n  }\n}\n"
  },
  {
    "path": "apps/web/styles/prosemirror.css",
    "content": ".ProseMirror {\n  @apply p-12 px-8 sm:px-12;\n}\n\n.ProseMirror .is-editor-empty:first-child::before {\n  content: attr(data-placeholder);\n  float: left;\n  color: hsl(var(--muted-foreground));\n  pointer-events: none;\n  height: 0;\n}\n.ProseMirror .is-empty::before {\n  content: attr(data-placeholder);\n  float: left;\n  color: hsl(var(--muted-foreground));\n  pointer-events: none;\n  height: 0;\n}\n\n/* Custom image styles */\n\n.ProseMirror img {\n  transition: filter 0.1s ease-in-out;\n\n  &:hover {\n    cursor: pointer;\n    filter: brightness(90%);\n  }\n\n  &.ProseMirror-selectednode {\n    outline: 3px solid #5abbf7;\n    filter: brightness(90%);\n  }\n}\n\n.img-placeholder {\n  position: relative;\n\n  &:before {\n    content: \"\";\n    box-sizing: border-box;\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    width: 36px;\n    height: 36px;\n    border-radius: 50%;\n    border: 3px solid var(--novel-stone-200);\n    border-top-color: var(--novel-stone-800);\n    animation: spinning 0.6s linear infinite;\n  }\n}\n\n@keyframes spinning {\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */\n\nul[data-type=\"taskList\"] li > label {\n  margin-right: 0.2rem;\n  user-select: none;\n}\n\n@media screen and (max-width: 768px) {\n  ul[data-type=\"taskList\"] li > label {\n    margin-right: 0.5rem;\n  }\n}\n\nul[data-type=\"taskList\"] li > label input[type=\"checkbox\"] {\n  -webkit-appearance: none;\n  appearance: none;\n  background-color: hsl(var(--background));\n  margin: 0;\n  cursor: pointer;\n  width: 1.2em;\n  height: 1.2em;\n  position: relative;\n  top: 5px;\n  border: 2px solid hsl(var(--border));\n  margin-right: 0.3rem;\n  display: grid;\n  place-content: center;\n\n  &:hover {\n    background-color: hsl(var(--accent));\n  }\n\n  &:active {\n    background-color: hsl(var(--accent));\n  }\n\n  &::before {\n    content: \"\";\n    width: 0.65em;\n    height: 0.65em;\n    transform: scale(0);\n    transition: 120ms transform ease-in-out;\n    box-shadow: inset 1em 1em;\n    transform-origin: center;\n    clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);\n  }\n\n  &:checked::before {\n    transform: scale(1);\n  }\n}\n\nul[data-type=\"taskList\"] li[data-checked=\"true\"] > div > p {\n  color: var(--muted-foreground);\n  text-decoration: line-through;\n  text-decoration-thickness: 2px;\n}\n\n/* Overwrite tippy-box original max-width */\n\n.tippy-box {\n  max-width: 400px !important;\n}\n\n.ProseMirror:not(.dragging) .ProseMirror-selectednode {\n  outline: none !important;\n  background-color: var(--novel-highlight-blue);\n  transition: background-color 0.2s;\n  box-shadow: none;\n}\n\n.drag-handle {\n  position: fixed;\n  opacity: 1;\n  transition: opacity ease-in 0.2s;\n  border-radius: 0.25rem;\n\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(0, 0, 0, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E\");\n  background-size: calc(0.5em + 0.375rem) calc(0.5em + 0.375rem);\n  background-repeat: no-repeat;\n  background-position: center;\n  width: 1.2rem;\n  height: 1.5rem;\n  z-index: 50;\n  cursor: grab;\n\n  &:hover {\n    background-color: var(--novel-stone-100);\n    transition: background-color 0.2s;\n  }\n\n  &:active {\n    background-color: var(--novel-stone-200);\n    transition: background-color 0.2s;\n    cursor: grabbing;\n  }\n\n  &.hide {\n    opacity: 0;\n    pointer-events: none;\n  }\n\n  @media screen and (max-width: 600px) {\n    display: none;\n    pointer-events: none;\n  }\n}\n\n.dark .drag-handle {\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(255, 255, 255, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E\");\n}\n\n/* Custom Youtube Video CSS */\niframe {\n  border: 8px solid #ffd00027;\n  border-radius: 4px;\n  min-width: 200px;\n  min-height: 200px;\n  display: block;\n  outline: 0px solid transparent;\n}\n\ndiv[data-youtube-video] > iframe {\n  cursor: move;\n  aspect-ratio: 16 / 9;\n  width: 100%;\n}\n\n.ProseMirror-selectednode iframe {\n  transition: outline 0.15s;\n  outline: 6px solid #fbbf24;\n}\n\n@media only screen and (max-width: 480px) {\n  div[data-youtube-video] > iframe {\n    max-height: 50px;\n  }\n}\n\n@media only screen and (max-width: 720px) {\n  div[data-youtube-video] > iframe {\n    max-height: 100px;\n  }\n}\n\n/* CSS for bold coloring and highlighting issue*/\nspan[style] > strong {\n  color: inherit;\n}\n\nmark[style] > strong {\n  color: inherit;\n}"
  },
  {
    "path": "apps/web/tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nconst config = {\n  darkMode: [\"class\"],\n  content: [\n    \"./pages/**/*.{ts,tsx}\",\n    \"./components/**/*.{ts,tsx}\",\n    \"./app/**/*.{ts,tsx}\",\n    \"./src/**/*.{ts,tsx}\",\n    \"./lib/**/*.{ts,tsx}\",\n  ],\n  prefix: \"\",\n  theme: {\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1400px\",\n      },\n    },\n    extend: {\n      colors: {\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        background: \"hsl(var(--background))\",\n        foreground: \"hsl(var(--foreground))\",\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n      },\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n      keyframes: {\n        \"accordion-down\": {\n          from: { height: \"0\" },\n          to: { height: \"var(--radix-accordion-content-height)\" },\n        },\n        \"accordion-up\": {\n          from: { height: \"var(--radix-accordion-content-height)\" },\n          to: { height: \"0\" },\n        },\n      },\n      animation: {\n        \"accordion-down\": \"accordion-down 0.2s ease-out\",\n        \"accordion-up\": \"accordion-up 0.2s ease-out\",\n      },\n    },\n  },\n  plugins: [require(\"tailwindcss-animate\"), require(\"@tailwindcss/typography\")],\n} satisfies Config;\n\nexport default config;\n"
  },
  {
    "path": "apps/web/tsconfig.json",
    "content": "{\n  \"extends\": \"tsconfig/next.json\",\n\n  \"compilerOptions\": {\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "apps/web/vercel.json",
    "content": "{\n  \"rewrites\": [\n    {\n      \"source\": \"/docs\",\n      \"destination\": \"https://novel.mintlify.dev/docs\"\n    },\n    {\n      \"source\": \"/docs/:match*\",\n      \"destination\": \"https://novel.mintlify.dev/docs/:match*\"\n    }\n  ]\n}\n"
  },
  {
    "path": "biome.json",
    "content": "{\n  \"$schema\": \"https://biomejs.dev/schemas/1.6.4/schema.json\",\n  \"files\": {\n    \"ignoreUnknown\": true,\n    \"ignore\": [\n      \"node_modules/*\",\n      \"*.config.*\",\n      \"*.json\",\n      \"tsconfig.json\",\n      \".turbo\",\n      \"**/dist\",\n      \"**/out\",\n      \".next\"\n    ]\n  },\n  \"organizeImports\": {\n    \"enabled\": true\n  },\n  \"linter\": {\n    \"enabled\": true,\n    \"rules\": {\n      \"recommended\": true,\n      \"complexity\": {\n        \"noForEach\": \"off\",\n        \"noUselessFragments\": \"off\"\n      },\n      \"correctness\": {\n        \"useExhaustiveDependencies\": \"off\",\n        \"noUnusedImports\": \"warn\",\n        \"noUnusedVariables\": \"warn\"\n      },\n      \"style\": {\n        \"noParameterAssign\": \"off\"\n      }\n    }\n  },\n  \"formatter\": {\n    \"enabled\": true,\n    \"formatWithErrors\": false,\n    \"indentStyle\": \"space\",\n    \"lineEnding\": \"lf\",\n    \"lineWidth\": 120\n  }\n}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"novel\",\n  \"private\": true,\n  \"scripts\": {\n    \"changeset\": \"changeset\",\n    \"publish:packages\": \"changeset publish\",\n    \"version:packages\": \"turbo build && changeset version\",\n    \"build\": \"turbo build\",\n    \"dev\": \"turbo dev\",\n    \"format\": \"turbo format --continue --\",\n    \"format:fix\": \"turbo format --continue -- --write\",\n    \"lint\": \"turbo lint --continue --\",\n    \"lint:fix\": \"turbo lint --continue -- --apply\",\n    \"clean\": \"turbo clean\",\n    \"release\": \"turbo run release\",\n    \"prepare\": \"husky install\",\n    \"typecheck\": \"turbo typecheck\"\n  },\n  \"dependencies\": {\n    \"@changesets/changelog-github\": \"^0.5.0\",\n    \"@changesets/cli\": \"^2.27.11\",\n    \"turbo\": \"^2.3.3\"\n  },\n  \"packageManager\": \"pnpm@9.5.0\",\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"^1.9.4\",\n    \"@commitlint/cli\": \"^19.6.1\",\n    \"@commitlint/config-conventional\": \"^19.6.0\",\n    \"husky\": \"^9.1.7\",\n    \"postcss\": \"^8.5.1\"\n  },\n  \"commitlint\": {\n    \"extends\": [\n      \"@commitlint/config-conventional\"\n    ],\n    \"rules\": {\n      \"type-enum\": [\n        2,\n        \"always\",\n        [\n          \"build\",\n          \"chore\",\n          \"ci\",\n          \"clean\",\n          \"doc\",\n          \"feat\",\n          \"fix\",\n          \"perf\",\n          \"ref\",\n          \"revert\",\n          \"style\",\n          \"test\"\n        ]\n      ],\n      \"subject-case\": [\n        0,\n        \"always\",\n        \"sentence-case\"\n      ],\n      \"body-leading-blank\": [\n        2,\n        \"always\",\n        true\n      ],\n      \"body-max-line-length\": [\n        0,\n        \"always\",\n        100\n      ]\n    }\n  }\n}"
  },
  {
    "path": "packages/headless/CHANGELOG.md",
    "content": "# [0.2.0](https://github.com/steven-tey/novel/compare/v0.1.0...v0.2.0) (2025-01-17)\n\n## 1.0.0\n\n### Major Changes\n\n- cleanup novel\n\n### Bug Fixes\n\n- add correct config ([7c2a9a1](https://github.com/steven-tey/novel/commit/7c2a9a1eb79c774e5b11ca1f137f7f28ee4aaadf))\n- add correct workflow ([db2781c](https://github.com/steven-tey/novel/commit/db2781c48e1e25517d8c473209b09a06ea2edda4))\n- add crazy spinner ([256ab4a](https://github.com/steven-tey/novel/commit/256ab4a03b168ee170e90b91a90ad7b8df0e3ec6))\n- add default prose ([cb93667](https://github.com/steven-tey/novel/commit/cb9366704180f9c7b5ccd793806af35d6c8f7d97))\n- add docs button ([1cc5140](https://github.com/steven-tey/novel/commit/1cc514089c8ba0b1e19e26e0d72b45c4f68ea360))\n- add missing dependecy ([75da619](https://github.com/steven-tey/novel/commit/75da61969b43a6f1b567c624f1a68e119578a589))\n- add shorter longer options ([abdd1b7](https://github.com/steven-tey/novel/commit/abdd1b795acf1fe7211e7983171c5c73670b473a))\n- build fix & prettier top level ([a8d30fc](https://github.com/steven-tey/novel/commit/a8d30fc28550fa3d1238bca0e4ec0e7a83d5ebdd))\n- bump version ([74ebbc0](https://github.com/steven-tey/novel/commit/74ebbc0852deeca76668b3319e490582756c97ed))\n- bump version ([2abe146](https://github.com/steven-tey/novel/commit/2abe1460207d78651593cd75990ea39195b8d5b8))\n- bump version ([41d7e6a](https://github.com/steven-tey/novel/commit/41d7e6afdf787d82095bef6b819ed7572e3559f8))\n- bump version ([02409d1](https://github.com/steven-tey/novel/commit/02409d14918166be52f0976a4a7f5b2df8cd4e81))\n- bump version ([4294547](https://github.com/steven-tey/novel/commit/4294547e97bbd5788a4db58d8c75540cb19fc155))\n- bump version ([75ed43a](https://github.com/steven-tey/novel/commit/75ed43afe643671b7249874e29e4e62053c98f40))\n- bump version ([4c4b28f](https://github.com/steven-tey/novel/commit/4c4b28f981073b79a3068a8d5d1979b55cfdba20))\n- cannot set text color when the text is bold ([6db8197](https://github.com/steven-tey/novel/commit/6db81970ebc2773ea723dfe60b644766b0a238dd))\n- checkbox fix ([4d49c2f](https://github.com/steven-tey/novel/commit/4d49c2f92ded59c73e5e79ccbc95eed0e75f361a))\n- chore bump version ([68243f6](https://github.com/steven-tey/novel/commit/68243f69613a94e414fb401992a613228f40ac98))\n- chore bump version ([7051d54](https://github.com/steven-tey/novel/commit/7051d542a78320565d8ce2fe9086967603934ace))\n- codeblock-lowlight doesn't render in html output ([de5c2e5](https://github.com/steven-tey/novel/commit/de5c2e55e1b7cb553ee55437d363fc0a041552c9))\n- colection to collection ([50248de](https://github.com/steven-tey/novel/commit/50248dea0bb3463850f7cb1332fbc03377c17594))\n- default value and css styles ([ddf0e01](https://github.com/steven-tey/novel/commit/ddf0e0145d2e41dc2ab694bf29fcc9a93d57c04e))\n- **docs:** tailwind extensions guide ([7274419](https://github.com/steven-tey/novel/commit/7274419fa74a21b27f8de3db93249e90608ca6ff))\n- **docs:** tailwind extensions guide ([a1457c4](https://github.com/steven-tey/novel/commit/a1457c4dfc2f869bb59e8950c49e05a50938b652))\n- dont trigger slash-command on codeBlock nodes ([8a3570b](https://github.com/steven-tey/novel/commit/8a3570bd72f32076f9edb42b0492e5eea8d5a014))\n- error deploy ([ca06a20](https://github.com/steven-tey/novel/commit/ca06a205fa9ffdc9a8f5b96051cc80244239989d))\n- expose editor command list ([8456652](https://github.com/steven-tey/novel/commit/84566528fc3a4c3420c94f68a54a600810e9942c))\n- expose utils ([33252ac](https://github.com/steven-tey/novel/commit/33252ac5148437cd4be7be736b7b8f872b07888d))\n- format lint ([d4484f9](https://github.com/steven-tey/novel/commit/d4484f96b764f17ac9bcb152db7caa5279ca7666))\n- image is deleted if an error occurs ([13e74b3](https://github.com/steven-tey/novel/commit/13e74b3a4b9b77356c895892f0a142d3a872403e))\n- keep drag handle ([8363ea6](https://github.com/steven-tey/novel/commit/8363ea60c6bc7bd64c5e8409b38a8696d1d7240f))\n- not show bubble menu if editor cannot be editable ([2aeee1b](https://github.com/steven-tey/novel/commit/2aeee1b1f402cd2006a9f86b0450efb8db003e73))\n- pass ref to div ([a3cd338](https://github.com/steven-tey/novel/commit/a3cd33888ab47a86a80f000a35b8abd6c663e431))\n- remove AI autocomplete default placeholder ([5ca260e](https://github.com/steven-tey/novel/commit/5ca260e4496ec9e4da114cb41ee874090a519c13))\n- remove auto joiner ([dbef03c](https://github.com/steven-tey/novel/commit/dbef03c5cafb691c3dc4f8c14e6944707ca70a98))\n- remove default extensions and make them standalone exports ([54bfd40](https://github.com/steven-tey/novel/commit/54bfd404b013a094449a2ac16ec47a561ddd15a5))\n- remove drag-handle on drop ([1ec5518](https://github.com/steven-tey/novel/commit/1ec551819e2490afddd1e042e0eb70d626c3325d))\n- remove katex styling import inside mathematics extension ([91264c7](https://github.com/steven-tey/novel/commit/91264c7c764f8a1b0d3859c3179fc8ccf18f330f))\n- rename to workflows ([16d3ff5](https://github.com/steven-tey/novel/commit/16d3ff56a6b1cb291b79c72dcf71ed5735645dcc))\n- rename updated image type ([d1b21d6](https://github.com/steven-tey/novel/commit/d1b21d695ba105e3e7f26d607ee8969b92289ff5))\n- safari related fix ([40a6fd8](https://github.com/steven-tey/novel/commit/40a6fd8ef3d881a89792e2c078c4f56bde4327ec))\n- The tailwind example link on setup page redirects to correct file ([55e4e69](https://github.com/steven-tey/novel/commit/55e4e69da6059023716e08bbb40c87cf829c9588))\n- update docs & rename type to EditorInstance ([ff9cf90](https://github.com/steven-tey/novel/commit/ff9cf902581cc2a65167f2b5493c9c183a489817))\n- update packge docs ([40b860f](https://github.com/steven-tey/novel/commit/40b860f654483934846fe66991c13906400c0fb1))\n- update tiptap-markdown ([d6358fe](https://github.com/steven-tey/novel/commit/d6358fe2c036e2d4c8101abb0912c8faabc9cf60))\n- update types ([8d168f5](https://github.com/steven-tey/novel/commit/8d168f58ca8e6cc9a859f232a570a2ded6532367))\n- use per-editor instance of tunnel to render slash command popover ([d05c03f](https://github.com/steven-tey/novel/commit/d05c03ff5f668d8b65a1729a9adc2d79315f9591))\n- use verbatim import ([1c4df17](https://github.com/steven-tey/novel/commit/1c4df17f252773ca472150541468a01c18d37588))\n\n### Features\n\n- add ai features example ([2a5e18c](https://github.com/steven-tey/novel/commit/2a5e18c8950f2e26659775d812b1be1d56baf169))\n- add custom highlight extension ([45efd37](https://github.com/steven-tey/novel/commit/45efd37d157e5f0654fb8ec83e5175df7b2aa918))\n- add custom upload config ([a202e6e](https://github.com/steven-tey/novel/commit/a202e6eb14488fe640e082d9fa665ce32ff02f65))\n- add dialog usage ([9152e46](https://github.com/steven-tey/novel/commit/9152e461a5ba8fb6480d423b4688a15407365c47))\n- add docs step to include editor props ([17dcc6d](https://github.com/steven-tey/novel/commit/17dcc6d94a9d213f86a704c043985b97138912c1))\n- add issue template ([2241fd1](https://github.com/steven-tey/novel/commit/2241fd1c5275456f8cd81cffb19dbe9055444bab))\n- add mathematics extension ([15b4428](https://github.com/steven-tey/novel/commit/15b44284d60a7ee88da3e1f1ee3462acdc1f8af8))\n- add twitter extension ([e019f34](https://github.com/steven-tey/novel/commit/e019f34575b0a9d8a1e1fbc04c72c100780ed035))\n- add utils functions for text generation ([7e99b72](https://github.com/steven-tey/novel/commit/7e99b722e393cde51bd206b0d012c130837c7137))\n- ai prev markdown ([122b3ee](https://github.com/steven-tey/novel/commit/122b3eed4e748e0824b6a7d670bf26c93bdada5a))\n- clear nodes on node selector ([596d811](https://github.com/steven-tey/novel/commit/596d81176030b29dfa1b41ee99797e855e9cafbe))\n- configure changeset for release ([c09dd55](https://github.com/steven-tey/novel/commit/c09dd55f0cc271b8d272a03a14a8b6108f611ee5))\n- fix biome linting ([081ab3b](https://github.com/steven-tey/novel/commit/081ab3bd6367d6e2e5660da1f2615ce322def85c))\n- forward ref components ([957e5dc](https://github.com/steven-tey/novel/commit/957e5dc279c804bab4c4af8a94f9275967fbbc65))\n- remove old docs & example add typecheck ([e0b2c99](https://github.com/steven-tey/novel/commit/e0b2c99b913fb283d5b55d94a0837b986936e160))\n- support for custom OpenAI base url ([7ac5895](https://github.com/steven-tey/novel/commit/7ac5895b7aece309a1e671bf5fa4d5042db296ea))\n- update docs ([9534c6e](https://github.com/steven-tey/novel/commit/9534c6ed78fc5850e46673499117fc144c770058))\n- update docs with demo code link ([4569347](https://github.com/steven-tey/novel/commit/4569347e8306517747aa0b5be40c399a286b1b9a))\n- use biome for linting & formatting ([e2601a0](https://github.com/steven-tey/novel/commit/e2601a059332e7db580d517f3081d7db555a1fb3))\n- use semantic release library ([4854d8a](https://github.com/steven-tey/novel/commit/4854d8a4a1d315dfbd3d96ca9e9a91e4f08afbfe))\n\n# [0.2.0](https://github.com/steven-tey/novel/compare/v0.1.0...v0.2.0) (2025-01-17)\n\n### Bug Fixes\n\n- add correct config ([7c2a9a1](https://github.com/steven-tey/novel/commit/7c2a9a1eb79c774e5b11ca1f137f7f28ee4aaadf))\n- add correct workflow ([db2781c](https://github.com/steven-tey/novel/commit/db2781c48e1e25517d8c473209b09a06ea2edda4))\n- add crazy spinner ([256ab4a](https://github.com/steven-tey/novel/commit/256ab4a03b168ee170e90b91a90ad7b8df0e3ec6))\n- add default prose ([cb93667](https://github.com/steven-tey/novel/commit/cb9366704180f9c7b5ccd793806af35d6c8f7d97))\n- add docs button ([1cc5140](https://github.com/steven-tey/novel/commit/1cc514089c8ba0b1e19e26e0d72b45c4f68ea360))\n- add missing dependecy ([75da619](https://github.com/steven-tey/novel/commit/75da61969b43a6f1b567c624f1a68e119578a589))\n- add shorter longer options ([abdd1b7](https://github.com/steven-tey/novel/commit/abdd1b795acf1fe7211e7983171c5c73670b473a))\n- build fix & prettier top level ([a8d30fc](https://github.com/steven-tey/novel/commit/a8d30fc28550fa3d1238bca0e4ec0e7a83d5ebdd))\n- bump version ([74ebbc0](https://github.com/steven-tey/novel/commit/74ebbc0852deeca76668b3319e490582756c97ed))\n- bump version ([2abe146](https://github.com/steven-tey/novel/commit/2abe1460207d78651593cd75990ea39195b8d5b8))\n- bump version ([41d7e6a](https://github.com/steven-tey/novel/commit/41d7e6afdf787d82095bef6b819ed7572e3559f8))\n- bump version ([02409d1](https://github.com/steven-tey/novel/commit/02409d14918166be52f0976a4a7f5b2df8cd4e81))\n- bump version ([4294547](https://github.com/steven-tey/novel/commit/4294547e97bbd5788a4db58d8c75540cb19fc155))\n- bump version ([75ed43a](https://github.com/steven-tey/novel/commit/75ed43afe643671b7249874e29e4e62053c98f40))\n- bump version ([4c4b28f](https://github.com/steven-tey/novel/commit/4c4b28f981073b79a3068a8d5d1979b55cfdba20))\n- cannot set text color when the text is bold ([6db8197](https://github.com/steven-tey/novel/commit/6db81970ebc2773ea723dfe60b644766b0a238dd))\n- checkbox fix ([4d49c2f](https://github.com/steven-tey/novel/commit/4d49c2f92ded59c73e5e79ccbc95eed0e75f361a))\n- chore bump version ([68243f6](https://github.com/steven-tey/novel/commit/68243f69613a94e414fb401992a613228f40ac98))\n- chore bump version ([7051d54](https://github.com/steven-tey/novel/commit/7051d542a78320565d8ce2fe9086967603934ace))\n- codeblock-lowlight doesn't render in html output ([de5c2e5](https://github.com/steven-tey/novel/commit/de5c2e55e1b7cb553ee55437d363fc0a041552c9))\n- colection to collection ([50248de](https://github.com/steven-tey/novel/commit/50248dea0bb3463850f7cb1332fbc03377c17594))\n- default value and css styles ([ddf0e01](https://github.com/steven-tey/novel/commit/ddf0e0145d2e41dc2ab694bf29fcc9a93d57c04e))\n- **docs:** tailwind extensions guide ([7274419](https://github.com/steven-tey/novel/commit/7274419fa74a21b27f8de3db93249e90608ca6ff))\n- **docs:** tailwind extensions guide ([a1457c4](https://github.com/steven-tey/novel/commit/a1457c4dfc2f869bb59e8950c49e05a50938b652))\n- dont trigger slash-command on codeBlock nodes ([8a3570b](https://github.com/steven-tey/novel/commit/8a3570bd72f32076f9edb42b0492e5eea8d5a014))\n- error deploy ([ca06a20](https://github.com/steven-tey/novel/commit/ca06a205fa9ffdc9a8f5b96051cc80244239989d))\n- expose editor command list ([8456652](https://github.com/steven-tey/novel/commit/84566528fc3a4c3420c94f68a54a600810e9942c))\n- expose utils ([33252ac](https://github.com/steven-tey/novel/commit/33252ac5148437cd4be7be736b7b8f872b07888d))\n- format lint ([d4484f9](https://github.com/steven-tey/novel/commit/d4484f96b764f17ac9bcb152db7caa5279ca7666))\n- image is deleted if an error occurs ([13e74b3](https://github.com/steven-tey/novel/commit/13e74b3a4b9b77356c895892f0a142d3a872403e))\n- keep drag handle ([8363ea6](https://github.com/steven-tey/novel/commit/8363ea60c6bc7bd64c5e8409b38a8696d1d7240f))\n- not show bubble menu if editor cannot be editable ([2aeee1b](https://github.com/steven-tey/novel/commit/2aeee1b1f402cd2006a9f86b0450efb8db003e73))\n- pass ref to div ([a3cd338](https://github.com/steven-tey/novel/commit/a3cd33888ab47a86a80f000a35b8abd6c663e431))\n- remove AI autocomplete default placeholder ([5ca260e](https://github.com/steven-tey/novel/commit/5ca260e4496ec9e4da114cb41ee874090a519c13))\n- remove auto joiner ([dbef03c](https://github.com/steven-tey/novel/commit/dbef03c5cafb691c3dc4f8c14e6944707ca70a98))\n- remove default extensions and make them standalone exports ([54bfd40](https://github.com/steven-tey/novel/commit/54bfd404b013a094449a2ac16ec47a561ddd15a5))\n- remove drag-handle on drop ([1ec5518](https://github.com/steven-tey/novel/commit/1ec551819e2490afddd1e042e0eb70d626c3325d))\n- remove katex styling import inside mathematics extension ([91264c7](https://github.com/steven-tey/novel/commit/91264c7c764f8a1b0d3859c3179fc8ccf18f330f))\n- rename to workflows ([16d3ff5](https://github.com/steven-tey/novel/commit/16d3ff56a6b1cb291b79c72dcf71ed5735645dcc))\n- rename updated image type ([d1b21d6](https://github.com/steven-tey/novel/commit/d1b21d695ba105e3e7f26d607ee8969b92289ff5))\n- safari related fix ([40a6fd8](https://github.com/steven-tey/novel/commit/40a6fd8ef3d881a89792e2c078c4f56bde4327ec))\n- The tailwind example link on setup page redirects to correct file ([55e4e69](https://github.com/steven-tey/novel/commit/55e4e69da6059023716e08bbb40c87cf829c9588))\n- update docs & rename type to EditorInstance ([ff9cf90](https://github.com/steven-tey/novel/commit/ff9cf902581cc2a65167f2b5493c9c183a489817))\n- update packge docs ([40b860f](https://github.com/steven-tey/novel/commit/40b860f654483934846fe66991c13906400c0fb1))\n- update tiptap-markdown ([d6358fe](https://github.com/steven-tey/novel/commit/d6358fe2c036e2d4c8101abb0912c8faabc9cf60))\n- update types ([8d168f5](https://github.com/steven-tey/novel/commit/8d168f58ca8e6cc9a859f232a570a2ded6532367))\n- use per-editor instance of tunnel to render slash command popover ([d05c03f](https://github.com/steven-tey/novel/commit/d05c03ff5f668d8b65a1729a9adc2d79315f9591))\n- use verbatim import ([1c4df17](https://github.com/steven-tey/novel/commit/1c4df17f252773ca472150541468a01c18d37588))\n\n### Features\n\n- add ai features example ([2a5e18c](https://github.com/steven-tey/novel/commit/2a5e18c8950f2e26659775d812b1be1d56baf169))\n- add custom highlight extension ([45efd37](https://github.com/steven-tey/novel/commit/45efd37d157e5f0654fb8ec83e5175df7b2aa918))\n- add custom upload config ([a202e6e](https://github.com/steven-tey/novel/commit/a202e6eb14488fe640e082d9fa665ce32ff02f65))\n- add dialog usage ([9152e46](https://github.com/steven-tey/novel/commit/9152e461a5ba8fb6480d423b4688a15407365c47))\n- add docs step to include editor props ([17dcc6d](https://github.com/steven-tey/novel/commit/17dcc6d94a9d213f86a704c043985b97138912c1))\n- add issue template ([2241fd1](https://github.com/steven-tey/novel/commit/2241fd1c5275456f8cd81cffb19dbe9055444bab))\n- add mathematics extension ([15b4428](https://github.com/steven-tey/novel/commit/15b44284d60a7ee88da3e1f1ee3462acdc1f8af8))\n- add twitter extension ([e019f34](https://github.com/steven-tey/novel/commit/e019f34575b0a9d8a1e1fbc04c72c100780ed035))\n- add utils functions for text generation ([7e99b72](https://github.com/steven-tey/novel/commit/7e99b722e393cde51bd206b0d012c130837c7137))\n- ai prev markdown ([122b3ee](https://github.com/steven-tey/novel/commit/122b3eed4e748e0824b6a7d670bf26c93bdada5a))\n- clear nodes on node selector ([596d811](https://github.com/steven-tey/novel/commit/596d81176030b29dfa1b41ee99797e855e9cafbe))\n- configure changeset for release ([c09dd55](https://github.com/steven-tey/novel/commit/c09dd55f0cc271b8d272a03a14a8b6108f611ee5))\n- fix biome linting ([081ab3b](https://github.com/steven-tey/novel/commit/081ab3bd6367d6e2e5660da1f2615ce322def85c))\n- forward ref components ([957e5dc](https://github.com/steven-tey/novel/commit/957e5dc279c804bab4c4af8a94f9275967fbbc65))\n- remove old docs & example add typecheck ([e0b2c99](https://github.com/steven-tey/novel/commit/e0b2c99b913fb283d5b55d94a0837b986936e160))\n- support for custom OpenAI base url ([7ac5895](https://github.com/steven-tey/novel/commit/7ac5895b7aece309a1e671bf5fa4d5042db296ea))\n- update docs ([9534c6e](https://github.com/steven-tey/novel/commit/9534c6ed78fc5850e46673499117fc144c770058))\n- update docs with demo code link ([4569347](https://github.com/steven-tey/novel/commit/4569347e8306517747aa0b5be40c399a286b1b9a))\n- use biome for linting & formatting ([e2601a0](https://github.com/steven-tey/novel/commit/e2601a059332e7db580d517f3081d7db555a1fb3))\n- use semantic release library ([4854d8a](https://github.com/steven-tey/novel/commit/4854d8a4a1d315dfbd3d96ca9e9a91e4f08afbfe))\n\n# [0.2.0](https://github.com/steven-tey/novel/compare/v0.1.0...v0.2.0) (2025-01-17)\n\n### Bug Fixes\n\n- add correct config ([7c2a9a1](https://github.com/steven-tey/novel/commit/7c2a9a1eb79c774e5b11ca1f137f7f28ee4aaadf))\n- add correct workflow ([db2781c](https://github.com/steven-tey/novel/commit/db2781c48e1e25517d8c473209b09a06ea2edda4))\n- add crazy spinner ([256ab4a](https://github.com/steven-tey/novel/commit/256ab4a03b168ee170e90b91a90ad7b8df0e3ec6))\n- add default prose ([cb93667](https://github.com/steven-tey/novel/commit/cb9366704180f9c7b5ccd793806af35d6c8f7d97))\n- add docs button ([1cc5140](https://github.com/steven-tey/novel/commit/1cc514089c8ba0b1e19e26e0d72b45c4f68ea360))\n- add missing dependecy ([75da619](https://github.com/steven-tey/novel/commit/75da61969b43a6f1b567c624f1a68e119578a589))\n- add shorter longer options ([abdd1b7](https://github.com/steven-tey/novel/commit/abdd1b795acf1fe7211e7983171c5c73670b473a))\n- build fix & prettier top level ([a8d30fc](https://github.com/steven-tey/novel/commit/a8d30fc28550fa3d1238bca0e4ec0e7a83d5ebdd))\n- bump version ([74ebbc0](https://github.com/steven-tey/novel/commit/74ebbc0852deeca76668b3319e490582756c97ed))\n- bump version ([2abe146](https://github.com/steven-tey/novel/commit/2abe1460207d78651593cd75990ea39195b8d5b8))\n- bump version ([41d7e6a](https://github.com/steven-tey/novel/commit/41d7e6afdf787d82095bef6b819ed7572e3559f8))\n- bump version ([02409d1](https://github.com/steven-tey/novel/commit/02409d14918166be52f0976a4a7f5b2df8cd4e81))\n- bump version ([4294547](https://github.com/steven-tey/novel/commit/4294547e97bbd5788a4db58d8c75540cb19fc155))\n- bump version ([75ed43a](https://github.com/steven-tey/novel/commit/75ed43afe643671b7249874e29e4e62053c98f40))\n- bump version ([4c4b28f](https://github.com/steven-tey/novel/commit/4c4b28f981073b79a3068a8d5d1979b55cfdba20))\n- cannot set text color when the text is bold ([6db8197](https://github.com/steven-tey/novel/commit/6db81970ebc2773ea723dfe60b644766b0a238dd))\n- checkbox fix ([4d49c2f](https://github.com/steven-tey/novel/commit/4d49c2f92ded59c73e5e79ccbc95eed0e75f361a))\n- chore bump version ([68243f6](https://github.com/steven-tey/novel/commit/68243f69613a94e414fb401992a613228f40ac98))\n- chore bump version ([7051d54](https://github.com/steven-tey/novel/commit/7051d542a78320565d8ce2fe9086967603934ace))\n- codeblock-lowlight doesn't render in html output ([de5c2e5](https://github.com/steven-tey/novel/commit/de5c2e55e1b7cb553ee55437d363fc0a041552c9))\n- colection to collection ([50248de](https://github.com/steven-tey/novel/commit/50248dea0bb3463850f7cb1332fbc03377c17594))\n- default value and css styles ([ddf0e01](https://github.com/steven-tey/novel/commit/ddf0e0145d2e41dc2ab694bf29fcc9a93d57c04e))\n- **docs:** tailwind extensions guide ([7274419](https://github.com/steven-tey/novel/commit/7274419fa74a21b27f8de3db93249e90608ca6ff))\n- **docs:** tailwind extensions guide ([a1457c4](https://github.com/steven-tey/novel/commit/a1457c4dfc2f869bb59e8950c49e05a50938b652))\n- dont trigger slash-command on codeBlock nodes ([8a3570b](https://github.com/steven-tey/novel/commit/8a3570bd72f32076f9edb42b0492e5eea8d5a014))\n- error deploy ([ca06a20](https://github.com/steven-tey/novel/commit/ca06a205fa9ffdc9a8f5b96051cc80244239989d))\n- expose editor command list ([8456652](https://github.com/steven-tey/novel/commit/84566528fc3a4c3420c94f68a54a600810e9942c))\n- expose utils ([33252ac](https://github.com/steven-tey/novel/commit/33252ac5148437cd4be7be736b7b8f872b07888d))\n- format lint ([d4484f9](https://github.com/steven-tey/novel/commit/d4484f96b764f17ac9bcb152db7caa5279ca7666))\n- image is deleted if an error occurs ([13e74b3](https://github.com/steven-tey/novel/commit/13e74b3a4b9b77356c895892f0a142d3a872403e))\n- keep drag handle ([8363ea6](https://github.com/steven-tey/novel/commit/8363ea60c6bc7bd64c5e8409b38a8696d1d7240f))\n- not show bubble menu if editor cannot be editable ([2aeee1b](https://github.com/steven-tey/novel/commit/2aeee1b1f402cd2006a9f86b0450efb8db003e73))\n- pass ref to div ([a3cd338](https://github.com/steven-tey/novel/commit/a3cd33888ab47a86a80f000a35b8abd6c663e431))\n- remove AI autocomplete default placeholder ([5ca260e](https://github.com/steven-tey/novel/commit/5ca260e4496ec9e4da114cb41ee874090a519c13))\n- remove auto joiner ([dbef03c](https://github.com/steven-tey/novel/commit/dbef03c5cafb691c3dc4f8c14e6944707ca70a98))\n- remove default extensions and make them standalone exports ([54bfd40](https://github.com/steven-tey/novel/commit/54bfd404b013a094449a2ac16ec47a561ddd15a5))\n- remove drag-handle on drop ([1ec5518](https://github.com/steven-tey/novel/commit/1ec551819e2490afddd1e042e0eb70d626c3325d))\n- remove katex styling import inside mathematics extension ([91264c7](https://github.com/steven-tey/novel/commit/91264c7c764f8a1b0d3859c3179fc8ccf18f330f))\n- rename to workflows ([16d3ff5](https://github.com/steven-tey/novel/commit/16d3ff56a6b1cb291b79c72dcf71ed5735645dcc))\n- rename updated image type ([d1b21d6](https://github.com/steven-tey/novel/commit/d1b21d695ba105e3e7f26d607ee8969b92289ff5))\n- safari related fix ([40a6fd8](https://github.com/steven-tey/novel/commit/40a6fd8ef3d881a89792e2c078c4f56bde4327ec))\n- The tailwind example link on setup page redirects to correct file ([55e4e69](https://github.com/steven-tey/novel/commit/55e4e69da6059023716e08bbb40c87cf829c9588))\n- update docs & rename type to EditorInstance ([ff9cf90](https://github.com/steven-tey/novel/commit/ff9cf902581cc2a65167f2b5493c9c183a489817))\n- update packge docs ([40b860f](https://github.com/steven-tey/novel/commit/40b860f654483934846fe66991c13906400c0fb1))\n- update tiptap-markdown ([d6358fe](https://github.com/steven-tey/novel/commit/d6358fe2c036e2d4c8101abb0912c8faabc9cf60))\n- update types ([8d168f5](https://github.com/steven-tey/novel/commit/8d168f58ca8e6cc9a859f232a570a2ded6532367))\n- use per-editor instance of tunnel to render slash command popover ([d05c03f](https://github.com/steven-tey/novel/commit/d05c03ff5f668d8b65a1729a9adc2d79315f9591))\n- use verbatim import ([1c4df17](https://github.com/steven-tey/novel/commit/1c4df17f252773ca472150541468a01c18d37588))\n\n### Features\n\n- add ai features example ([2a5e18c](https://github.com/steven-tey/novel/commit/2a5e18c8950f2e26659775d812b1be1d56baf169))\n- add custom highlight extension ([45efd37](https://github.com/steven-tey/novel/commit/45efd37d157e5f0654fb8ec83e5175df7b2aa918))\n- add custom upload config ([a202e6e](https://github.com/steven-tey/novel/commit/a202e6eb14488fe640e082d9fa665ce32ff02f65))\n- add dialog usage ([9152e46](https://github.com/steven-tey/novel/commit/9152e461a5ba8fb6480d423b4688a15407365c47))\n- add docs step to include editor props ([17dcc6d](https://github.com/steven-tey/novel/commit/17dcc6d94a9d213f86a704c043985b97138912c1))\n- add issue template ([2241fd1](https://github.com/steven-tey/novel/commit/2241fd1c5275456f8cd81cffb19dbe9055444bab))\n- add mathematics extension ([15b4428](https://github.com/steven-tey/novel/commit/15b44284d60a7ee88da3e1f1ee3462acdc1f8af8))\n- add twitter extension ([e019f34](https://github.com/steven-tey/novel/commit/e019f34575b0a9d8a1e1fbc04c72c100780ed035))\n- add utils functions for text generation ([7e99b72](https://github.com/steven-tey/novel/commit/7e99b722e393cde51bd206b0d012c130837c7137))\n- ai prev markdown ([122b3ee](https://github.com/steven-tey/novel/commit/122b3eed4e748e0824b6a7d670bf26c93bdada5a))\n- clear nodes on node selector ([596d811](https://github.com/steven-tey/novel/commit/596d81176030b29dfa1b41ee99797e855e9cafbe))\n- configure changeset for release ([c09dd55](https://github.com/steven-tey/novel/commit/c09dd55f0cc271b8d272a03a14a8b6108f611ee5))\n- fix biome linting ([081ab3b](https://github.com/steven-tey/novel/commit/081ab3bd6367d6e2e5660da1f2615ce322def85c))\n- forward ref components ([957e5dc](https://github.com/steven-tey/novel/commit/957e5dc279c804bab4c4af8a94f9275967fbbc65))\n- remove old docs & example add typecheck ([e0b2c99](https://github.com/steven-tey/novel/commit/e0b2c99b913fb283d5b55d94a0837b986936e160))\n- support for custom OpenAI base url ([7ac5895](https://github.com/steven-tey/novel/commit/7ac5895b7aece309a1e671bf5fa4d5042db296ea))\n- update docs ([9534c6e](https://github.com/steven-tey/novel/commit/9534c6ed78fc5850e46673499117fc144c770058))\n- update docs with demo code link ([4569347](https://github.com/steven-tey/novel/commit/4569347e8306517747aa0b5be40c399a286b1b9a))\n- use biome for linting & formatting ([e2601a0](https://github.com/steven-tey/novel/commit/e2601a059332e7db580d517f3081d7db555a1fb3))\n- use semantic release library ([4854d8a](https://github.com/steven-tey/novel/commit/4854d8a4a1d315dfbd3d96ca9e9a91e4f08afbfe))\n\n# novel\n\n## 0.5.0\n\n### Minor Changes\n\n- update extensions export\n\n## 0.4.3\n\n### Patch Changes\n\n- add twitter extension\n\n## 0.4.2\n\n### Patch Changes\n\n- bump version\n\n## 0.4.1\n\n### Patch Changes\n\n- expose utils\n\n## 0.4.0\n\n### Minor Changes\n\n- expose utils fix bugs\n\n## 0.3.1\n\n### Patch Changes\n\n- regression fix\n\n## 0.3.0\n\n### Minor Changes\n\n- update drag handle\n\n## 0.2.13\n\n### Patch Changes\n\n- small fixes\n\n## 0.2.12\n\n### Patch Changes\n\n- Expose command list editor\n\n## 0.2.11\n\n### Patch Changes\n\n- Ai utils & generative example\n\n## 0.2.10\n\n### Patch Changes\n\n- Fix types\n\n## 0.2.9\n\n### Patch Changes\n\n- Custom upload config\n\n## 0.2.8\n\n### Patch Changes\n\n- Code quality and extensions fixing\n\n## 0.2.7\n\n### Patch Changes\n\n- [#311](https://github.com/steven-tey/novel/pull/311) [`c09dd55`](https://github.com/steven-tey/novel/commit/c09dd55f0cc271b8d272a03a14a8b6108f611ee5) Thanks [@andrewdoro](https://github.com/andrewdoro)! - Rename type from Editor to EditorInstance\n"
  },
  {
    "path": "packages/headless/biome.json",
    "content": "{\n  \"extends\": [\"../../biome.json\"]\n}\n"
  },
  {
    "path": "packages/headless/package.json",
    "content": "{\n  \"name\": \"novel\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Notion-style WYSIWYG editor with AI-powered autocompletions\",\n  \"license\": \"Apache-2.0\",\n  \"type\": \"module\",\n  \"main\": \"dist/index.cjs\",\n  \"module\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"build\": \"tsup\",\n    \"lint\": \"biome lint ./src\",\n    \"format\": \"biome format ./src \"\n  },\n  \"sideEffects\": false,\n  \"peerDependencies\": {\n    \"react\": \">=18\"\n  },\n  \"dependencies\": {\n    \"@radix-ui/react-slot\": \"^1.1.1\",\n    \"@tiptap/core\": \"^2.11.2\",\n    \"@tiptap/extension-character-count\": \"^2.11.2\",\n    \"@tiptap/extension-code-block-lowlight\": \"^2.11.2\",\n    \"@tiptap/extension-color\": \"^2.11.2\",\n    \"@tiptap/extension-highlight\": \"^2.11.2\",\n    \"@tiptap/extension-horizontal-rule\": \"^2.11.2\",\n    \"@tiptap/extension-image\": \"^2.11.2\",\n    \"@tiptap/extension-link\": \"^2.11.2\",\n    \"@tiptap/extension-placeholder\": \"^2.11.2\",\n    \"@tiptap/extension-task-item\": \"^2.11.2\",\n    \"@tiptap/extension-task-list\": \"^2.11.2\",\n    \"@tiptap/extension-text-style\": \"^2.11.2\",\n    \"@tiptap/extension-underline\": \"^2.11.2\",\n    \"@tiptap/extension-youtube\": \"^2.11.2\",\n    \"@tiptap/pm\": \"^2.11.2\",\n    \"@tiptap/react\": \"^2.11.2\",\n    \"@tiptap/starter-kit\": \"^2.11.2\",\n    \"@tiptap/suggestion\": \"^2.11.2\",\n    \"@types/node\": \"^22.10.6\",\n    \"cmdk\": \"^1.0.4\",\n    \"jotai\": \"^2.11.0\",\n    \"react-markdown\": \"^9.0.3\",\n    \"react-moveable\": \"^0.56.0\",\n    \"react-tweet\": \"^3.2.1\",\n    \"katex\": \"^0.16.20\",\n    \"tippy.js\": \"^6.3.7\",\n    \"tiptap-extension-global-drag-handle\": \"^0.1.16\",\n    \"tiptap-markdown\": \"^0.8.10\",\n    \"tunnel-rat\": \"^0.1.2\"\n  },\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"^1.9.4\",\n    \"@types/katex\": \"^0.16.7\",\n    \"@types/react\": \"^18.2.55\",\n    \"@types/react-dom\": \"18.2.19\",\n    \"tsconfig\": \"workspace:*\",\n    \"tsup\": \"^8.3.5\",\n    \"typescript\": \"^5.7.3\"\n  },\n  \"author\": \"Steven Tey <stevensteel97@gmail.com>\",\n  \"homepage\": \"https://novel.sh\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/steven-tey/novel.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/steven-tey/novel/issues\"\n  },\n  \"keywords\": [\n    \"ai\",\n    \"novel\",\n    \"editor\",\n    \"markdown\",\n    \"nextjs\",\n    \"react\"\n  ]\n}"
  },
  {
    "path": "packages/headless/src/components/editor-bubble-item.tsx",
    "content": "import { forwardRef } from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { useCurrentEditor } from \"@tiptap/react\";\nimport type { Editor } from \"@tiptap/react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\n\ninterface EditorBubbleItemProps {\n  readonly children: ReactNode;\n  readonly asChild?: boolean;\n  readonly onSelect?: (editor: Editor) => void;\n}\n\nexport const EditorBubbleItem = forwardRef<\n  HTMLDivElement,\n  EditorBubbleItemProps & Omit<ComponentPropsWithoutRef<\"div\">, \"onSelect\">\n>(({ children, asChild, onSelect, ...rest }, ref) => {\n  const { editor } = useCurrentEditor();\n  const Comp = asChild ? Slot : \"div\";\n\n  if (!editor) return null;\n\n  return (\n    <Comp ref={ref} {...rest} onClick={() => onSelect?.(editor)}>\n      {children}\n    </Comp>\n  );\n});\n\nEditorBubbleItem.displayName = \"EditorBubbleItem\";\n\nexport default EditorBubbleItem;\n"
  },
  {
    "path": "packages/headless/src/components/editor-bubble.tsx",
    "content": "import { BubbleMenu, isNodeSelection, useCurrentEditor } from \"@tiptap/react\";\nimport type { BubbleMenuProps } from \"@tiptap/react\";\nimport { forwardRef, useEffect, useMemo, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport type { Instance, Props } from \"tippy.js\";\n\nexport interface EditorBubbleProps extends Omit<BubbleMenuProps, \"editor\"> {\n  readonly children: ReactNode;\n}\n\nexport const EditorBubble = forwardRef<HTMLDivElement, EditorBubbleProps>(\n  ({ children, tippyOptions, ...rest }, ref) => {\n    const { editor: currentEditor } = useCurrentEditor();\n    const instanceRef = useRef<Instance<Props> | null>(null);\n\n    useEffect(() => {\n      if (!instanceRef.current || !tippyOptions?.placement) return;\n\n      instanceRef.current.setProps({ placement: tippyOptions.placement });\n      instanceRef.current.popperInstance?.update();\n    }, [tippyOptions?.placement]);\n\n    const bubbleMenuProps: Omit<BubbleMenuProps, \"children\"> = useMemo(() => {\n      const shouldShow: BubbleMenuProps[\"shouldShow\"] = ({ editor, state }) => {\n        const { selection } = state;\n        const { empty } = selection;\n\n        // don't show bubble menu if:\n        // - the editor is not editable\n        // - the selected node is an image\n        // - the selection is empty\n        // - the selection is a node selection (for drag handles)\n        if (!editor.isEditable || editor.isActive(\"image\") || empty || isNodeSelection(selection)) {\n          return false;\n        }\n        return true;\n      };\n\n      return {\n        shouldShow,\n        tippyOptions: {\n          onCreate: (val) => {\n            instanceRef.current = val;\n\n            instanceRef.current.popper.firstChild?.addEventListener(\"blur\", (event) => {\n              event.preventDefault();\n              event.stopImmediatePropagation();\n            });\n          },\n          moveTransition: \"transform 0.15s ease-out\",\n          ...tippyOptions,\n        },\n        editor: currentEditor,\n        ...rest,\n      };\n    }, [rest, tippyOptions]);\n\n    if (!currentEditor) return null;\n\n    return (\n      // We need to add this because of https://github.com/ueberdosis/tiptap/issues/2658\n      <div ref={ref}>\n        <BubbleMenu {...bubbleMenuProps}>{children}</BubbleMenu>\n      </div>\n    );\n  },\n);\n\nEditorBubble.displayName = \"EditorBubble\";\n\nexport default EditorBubble;\n"
  },
  {
    "path": "packages/headless/src/components/editor-command-item.tsx",
    "content": "import { forwardRef } from \"react\";\nimport { CommandEmpty, CommandItem } from \"cmdk\";\nimport { useCurrentEditor } from \"@tiptap/react\";\nimport { useAtomValue } from \"jotai\";\nimport { rangeAtom } from \"../utils/atoms\";\nimport type { ComponentPropsWithoutRef } from \"react\";\nimport type { Editor, Range } from \"@tiptap/core\";\n\ninterface EditorCommandItemProps {\n  readonly onCommand: ({\n    editor,\n    range,\n  }: {\n    editor: Editor;\n    range: Range;\n  }) => void;\n}\n\nexport const EditorCommandItem = forwardRef<\n  HTMLDivElement,\n  EditorCommandItemProps & ComponentPropsWithoutRef<typeof CommandItem>\n>(({ children, onCommand, ...rest }, ref) => {\n  const { editor } = useCurrentEditor();\n  const range = useAtomValue(rangeAtom);\n\n  if (!editor || !range) return null;\n\n  return (\n    <CommandItem ref={ref} {...rest} onSelect={() => onCommand({ editor, range })}>\n      {children}\n    </CommandItem>\n  );\n});\n\nEditorCommandItem.displayName = \"EditorCommandItem\";\n\nexport const EditorCommandEmpty = CommandEmpty;\n\nexport default EditorCommandItem;\n"
  },
  {
    "path": "packages/headless/src/components/editor-command.tsx",
    "content": "import { useAtom, useSetAtom } from \"jotai\";\nimport { useEffect, forwardRef, createContext } from \"react\";\nimport { Command } from \"cmdk\";\nimport { queryAtom, rangeAtom } from \"../utils/atoms\";\nimport { novelStore } from \"../utils/store\";\nimport type tunnel from \"tunnel-rat\";\nimport type { ComponentPropsWithoutRef, FC } from \"react\";\nimport type { Range } from \"@tiptap/core\";\n\nexport const EditorCommandTunnelContext = createContext({} as ReturnType<typeof tunnel>);\n\ninterface EditorCommandOutProps {\n  readonly query: string;\n  readonly range: Range;\n}\n\nexport const EditorCommandOut: FC<EditorCommandOutProps> = ({ query, range }) => {\n  const setQuery = useSetAtom(queryAtom, { store: novelStore });\n  const setRange = useSetAtom(rangeAtom, { store: novelStore });\n\n  useEffect(() => {\n    setQuery(query);\n  }, [query, setQuery]);\n\n  useEffect(() => {\n    setRange(range);\n  }, [range, setRange]);\n\n  useEffect(() => {\n    const navigationKeys = [\"ArrowUp\", \"ArrowDown\", \"Enter\"];\n    const onKeyDown = (e: KeyboardEvent) => {\n      if (navigationKeys.includes(e.key)) {\n        e.preventDefault();\n        const commandRef = document.querySelector(\"#slash-command\");\n\n        if (commandRef)\n          commandRef.dispatchEvent(\n            new KeyboardEvent(\"keydown\", {\n              key: e.key,\n              cancelable: true,\n              bubbles: true,\n            }),\n          );\n\n        return false;\n      }\n    };\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => {\n      document.removeEventListener(\"keydown\", onKeyDown);\n    };\n  }, []);\n\n  return (\n    <EditorCommandTunnelContext.Consumer>\n      {(tunnelInstance) => <tunnelInstance.Out />}\n    </EditorCommandTunnelContext.Consumer>\n  );\n};\n\nexport const EditorCommand = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<typeof Command>>(\n  ({ children, className, ...rest }, ref) => {\n    const [query, setQuery] = useAtom(queryAtom);\n\n    return (\n      <EditorCommandTunnelContext.Consumer>\n        {(tunnelInstance) => (\n          <tunnelInstance.In>\n            <Command\n              ref={ref}\n              onKeyDown={(e) => {\n                e.stopPropagation();\n              }}\n              id=\"slash-command\"\n              className={className}\n              {...rest}\n            >\n              <Command.Input value={query} onValueChange={setQuery} style={{ display: \"none\" }} />\n              {children}\n            </Command>\n          </tunnelInstance.In>\n        )}\n      </EditorCommandTunnelContext.Consumer>\n    );\n  },\n);\nexport const EditorCommandList = Command.List;\n\nEditorCommand.displayName = \"EditorCommand\";\n"
  },
  {
    "path": "packages/headless/src/components/editor.tsx",
    "content": "import { EditorProvider } from \"@tiptap/react\";\nimport type { EditorProviderProps, JSONContent } from \"@tiptap/react\";\nimport { Provider } from \"jotai\";\nimport { forwardRef, useRef } from \"react\";\nimport type { FC, ReactNode } from \"react\";\nimport tunnel from \"tunnel-rat\";\nimport { novelStore } from \"../utils/store\";\nimport { EditorCommandTunnelContext } from \"./editor-command\";\n\nexport interface EditorProps {\n  readonly children: ReactNode;\n  readonly className?: string;\n}\n\ninterface EditorRootProps {\n  readonly children: ReactNode;\n}\n\nexport const EditorRoot: FC<EditorRootProps> = ({ children }) => {\n  const tunnelInstance = useRef(tunnel()).current;\n\n  return (\n    <Provider store={novelStore}>\n      <EditorCommandTunnelContext.Provider value={tunnelInstance}>{children}</EditorCommandTunnelContext.Provider>\n    </Provider>\n  );\n};\n\nexport type EditorContentProps = Omit<EditorProviderProps, \"content\"> & {\n  readonly children?: ReactNode;\n  readonly className?: string;\n  readonly initialContent?: JSONContent;\n};\n\nexport const EditorContent = forwardRef<HTMLDivElement, EditorContentProps>(\n  ({ className, children, initialContent, ...rest }, ref) => (\n    <div ref={ref} className={className}>\n      <EditorProvider {...rest} content={initialContent}>\n        {children}\n      </EditorProvider>\n    </div>\n  ),\n);\n\nEditorContent.displayName = \"EditorContent\";\n"
  },
  {
    "path": "packages/headless/src/components/index.ts",
    "content": "export { useCurrentEditor as useEditor } from \"@tiptap/react\";\nexport { type Editor as EditorInstance } from \"@tiptap/core\";\nexport type { JSONContent } from \"@tiptap/react\";\n\nexport { EditorRoot, EditorContent, type EditorContentProps } from \"./editor\";\nexport { EditorBubble } from \"./editor-bubble\";\nexport { EditorBubbleItem } from \"./editor-bubble-item\";\nexport { EditorCommand, EditorCommandList } from \"./editor-command\";\nexport { EditorCommandItem, EditorCommandEmpty } from \"./editor-command-item\";\n"
  },
  {
    "path": "packages/headless/src/extensions/ai-highlight.ts",
    "content": "import { type Editor, Mark, markInputRule, markPasteRule, mergeAttributes } from \"@tiptap/core\";\n\nexport interface AIHighlightOptions {\n  HTMLAttributes: Record<string, string>;\n}\n\ndeclare module \"@tiptap/core\" {\n  interface Commands<ReturnType> {\n    AIHighlight: {\n      /**\n       * Set a AIHighlight mark\n       */\n      setAIHighlight: (attributes?: { color: string }) => ReturnType;\n      /**\n       * Toggle a AIHighlight mark\n       */\n      toggleAIHighlight: (attributes?: { color: string }) => ReturnType;\n      /**\n       * Unset a AIHighlight mark\n       */\n      unsetAIHighlight: () => ReturnType;\n    };\n  }\n}\n\nexport const inputRegex = /(?:^|\\s)((?:==)((?:[^~=]+))(?:==))$/;\nexport const pasteRegex = /(?:^|\\s)((?:==)((?:[^~=]+))(?:==))/g;\n\nexport const AIHighlight = Mark.create<AIHighlightOptions>({\n  name: \"ai-highlight\",\n\n  addOptions() {\n    return {\n      HTMLAttributes: {},\n    };\n  },\n\n  addAttributes() {\n    return {\n      color: {\n        default: null,\n        parseHTML: (element) => element.getAttribute(\"data-color\") || element.style.backgroundColor,\n        renderHTML: (attributes) => {\n          if (!attributes.color) {\n            return {};\n          }\n\n          return {\n            \"data-color\": attributes.color,\n            style: `background-color: ${attributes.color}; color: inherit`,\n          };\n        },\n      },\n    };\n  },\n\n  parseHTML() {\n    return [\n      {\n        tag: \"mark\",\n      },\n    ];\n  },\n\n  renderHTML({ HTMLAttributes }) {\n    return [\"mark\", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];\n  },\n\n  addCommands() {\n    return {\n      setAIHighlight:\n        (attributes) =>\n        ({ commands }) => {\n          return commands.setMark(this.name, attributes);\n        },\n      toggleAIHighlight:\n        (attributes) =>\n        ({ commands }) => {\n          return commands.toggleMark(this.name, attributes);\n        },\n      unsetAIHighlight:\n        () =>\n        ({ commands }) => {\n          return commands.unsetMark(this.name);\n        },\n    };\n  },\n\n  addKeyboardShortcuts() {\n    return {\n      \"Mod-Shift-h\": () => this.editor.commands.toggleAIHighlight(),\n    };\n  },\n\n  addInputRules() {\n    return [\n      markInputRule({\n        find: inputRegex,\n        type: this.type,\n      }),\n    ];\n  },\n\n  addPasteRules() {\n    return [\n      markPasteRule({\n        find: pasteRegex,\n        type: this.type,\n      }),\n    ];\n  },\n});\n\nexport const removeAIHighlight = (editor: Editor) => {\n  const tr = editor.state.tr;\n  tr.removeMark(0, editor.state.doc.nodeSize - 2, editor.state.schema.marks[\"ai-highlight\"]);\n  editor.view.dispatch(tr);\n};\nexport const addAIHighlight = (editor: Editor, color?: string) => {\n  editor\n    .chain()\n    .setAIHighlight({ color: color ?? \"#c1ecf970\" })\n    .run();\n};\n"
  },
  {
    "path": "packages/headless/src/extensions/custom-keymap.ts",
    "content": "import { Extension } from \"@tiptap/core\";\n\ndeclare module \"@tiptap/core\" {\n  // eslint-disable-next-line no-unused-vars\n  interface Commands<ReturnType> {\n    customkeymap: {\n      /**\n       * Select text between node boundaries\n       */\n      selectTextWithinNodeBoundaries: () => ReturnType;\n    };\n  }\n}\n\nconst CustomKeymap = Extension.create({\n  name: \"CustomKeymap\",\n\n  addCommands() {\n    return {\n      selectTextWithinNodeBoundaries:\n        () =>\n        ({ editor, commands }) => {\n          const { state } = editor;\n          const { tr } = state;\n          const startNodePos = tr.selection.$from.start();\n          const endNodePos = tr.selection.$to.end();\n          return commands.setTextSelection({\n            from: startNodePos,\n            to: endNodePos,\n          });\n        },\n    };\n  },\n\n  addKeyboardShortcuts() {\n    return {\n      \"Mod-a\": ({ editor }) => {\n        const { state } = editor;\n        const { tr } = state;\n        const startSelectionPos = tr.selection.from;\n        const endSelectionPos = tr.selection.to;\n        const startNodePos = tr.selection.$from.start();\n        const endNodePos = tr.selection.$to.end();\n        const isCurrentTextSelectionNotExtendedToNodeBoundaries =\n          startSelectionPos > startNodePos || endSelectionPos < endNodePos;\n        if (isCurrentTextSelectionNotExtendedToNodeBoundaries) {\n          editor.chain().selectTextWithinNodeBoundaries().run();\n          return true;\n        }\n        return false;\n      },\n    };\n  },\n});\n\nexport default CustomKeymap;\n"
  },
  {
    "path": "packages/headless/src/extensions/image-resizer.tsx",
    "content": "import { useCurrentEditor } from \"@tiptap/react\";\nimport type { FC } from \"react\";\nimport Moveable from \"react-moveable\";\n\nexport const ImageResizer: FC = () => {\n  const { editor } = useCurrentEditor();\n\n  if (!editor?.isActive(\"image\")) return null;\n\n  const updateMediaSize = () => {\n    const imageInfo = document.querySelector(\".ProseMirror-selectednode\") as HTMLImageElement;\n    if (imageInfo) {\n      const selection = editor.state.selection;\n      const setImage = editor.commands.setImage as (options: {\n        src: string;\n        width: number;\n        height: number;\n      }) => boolean;\n\n      setImage({\n        src: imageInfo.src,\n        width: Number(imageInfo.style.width.replace(\"px\", \"\")),\n        height: Number(imageInfo.style.height.replace(\"px\", \"\")),\n      });\n      editor.commands.setNodeSelection(selection.from);\n    }\n  };\n\n  return (\n    <Moveable\n      target={document.querySelector(\".ProseMirror-selectednode\") as HTMLDivElement}\n      container={null}\n      origin={false}\n      /* Resize event edges */\n      edge={false}\n      throttleDrag={0}\n      /* When resize or scale, keeps a ratio of the width, height. */\n      keepRatio={true}\n      /* resizable*/\n      /* Only one of resizable, scalable, warpable can be used. */\n      resizable={true}\n      throttleResize={0}\n      onResize={({\n        target,\n        width,\n        height,\n        // dist,\n        delta,\n      }) => {\n        if (delta[0]) target.style.width = `${width}px`;\n        if (delta[1]) target.style.height = `${height}px`;\n      }}\n      // { target, isDrag, clientX, clientY }: any\n      onResizeEnd={() => {\n        updateMediaSize();\n      }}\n      /* scalable */\n      /* Only one of resizable, scalable, warpable can be used. */\n      scalable={true}\n      throttleScale={0}\n      /* Set the direction of resizable */\n      renderDirections={[\"w\", \"e\"]}\n      onScale={({\n        target,\n        // scale,\n        // dist,\n        // delta,\n        transform,\n      }) => {\n        target.style.transform = transform;\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "packages/headless/src/extensions/index.ts",
    "content": "import { InputRule } from \"@tiptap/core\";\nimport { Color } from \"@tiptap/extension-color\";\nimport Highlight from \"@tiptap/extension-highlight\";\nimport HorizontalRule from \"@tiptap/extension-horizontal-rule\";\nimport TiptapImage from \"@tiptap/extension-image\";\nimport TiptapLink from \"@tiptap/extension-link\";\nimport Placeholder from \"@tiptap/extension-placeholder\";\nimport { TaskItem } from \"@tiptap/extension-task-item\";\nimport { TaskList } from \"@tiptap/extension-task-list\";\nimport TextStyle from \"@tiptap/extension-text-style\";\nimport TiptapUnderline from \"@tiptap/extension-underline\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport { Markdown } from \"tiptap-markdown\";\nimport CustomKeymap from \"./custom-keymap\";\nimport { ImageResizer } from \"./image-resizer\";\nimport { Twitter } from \"./twitter\";\nimport { Mathematics } from \"./mathematics\";\nimport UpdatedImage from \"./updated-image\";\n\nimport CharacterCount from \"@tiptap/extension-character-count\";\nimport CodeBlockLowlight from \"@tiptap/extension-code-block-lowlight\";\nimport Youtube from \"@tiptap/extension-youtube\";\nimport GlobalDragHandle from \"tiptap-extension-global-drag-handle\";\n\nconst PlaceholderExtension = Placeholder.configure({\n  placeholder: ({ node }) => {\n    if (node.type.name === \"heading\") {\n      return `Heading ${node.attrs.level}`;\n    }\n    return \"Press '/' for commands\";\n  },\n  includeChildren: true,\n});\n\nconst HighlightExtension = Highlight.configure({\n  multicolor: true,\n});\n\nconst MarkdownExtension = Markdown.configure({\n  html: false,\n  transformCopiedText: true,\n});\n\nconst Horizontal = HorizontalRule.extend({\n  addInputRules() {\n    return [\n      new InputRule({\n        find: /^(?:---|—-|___\\s|\\*\\*\\*\\s)$/u,\n        handler: ({ state, range }) => {\n          const attributes = {};\n\n          const { tr } = state;\n          const start = range.from;\n          const end = range.to;\n\n          tr.insert(start - 1, this.type.create(attributes)).delete(tr.mapping.map(start), tr.mapping.map(end));\n        },\n      }),\n    ];\n  },\n});\n\nexport * from \"./ai-highlight\";\nexport * from \"./slash-command\";\nexport {\n  CodeBlockLowlight,\n  Horizontal as HorizontalRule,\n  ImageResizer,\n  InputRule,\n  PlaceholderExtension as Placeholder,\n  StarterKit,\n  TaskItem,\n  TaskList,\n  TiptapImage,\n  TiptapUnderline,\n  MarkdownExtension,\n  TextStyle,\n  Color,\n  HighlightExtension,\n  CustomKeymap,\n  TiptapLink,\n  UpdatedImage,\n  Youtube,\n  Twitter,\n  Mathematics,\n  CharacterCount,\n  GlobalDragHandle,\n};\n"
  },
  {
    "path": "packages/headless/src/extensions/mathematics.ts",
    "content": "import { Node, mergeAttributes } from \"@tiptap/core\";\nimport { EditorState } from \"@tiptap/pm/state\";\nimport katex, { type KatexOptions } from \"katex\";\n\nexport interface MathematicsOptions {\n  /**\n   * By default LaTeX decorations can render when mathematical expressions are not inside a code block.\n   * @param state - EditorState\n   * @param pos - number\n   * @returns boolean\n   */\n  shouldRender: (state: EditorState, pos: number) => boolean;\n\n  /**\n   * @see https://katex.org/docs/options.html\n   */\n  katexOptions?: KatexOptions;\n\n  HTMLAttributes: Record<string, any>;\n}\n\ndeclare module \"@tiptap/core\" {\n  interface Commands<ReturnType> {\n    LatexCommand: {\n\n      /**\n       * Set selection to a LaTex symbol\n       */\n      setLatex: ({ latex }: { latex: string }) => ReturnType;\n\n      /**\n       * Unset a LaTex symbol\n       */\n      unsetLatex: () => ReturnType;\n\n    };\n  }\n}\n\n/**\n * This extension adds support for mathematical symbols with LaTex expression.\n * \n * NOTE: Don't forget to import `katex/dist/katex.min.css` CSS for KaTex styling.\n * \n * @see https://katex.org/\n */\nexport const Mathematics = Node.create<MathematicsOptions>({\n  name: \"math\",\n  inline: true,\n  group: \"inline\",\n  atom: true,\n  selectable: true,\n  marks: \"\",\n\n  addAttributes() {\n    return {\n      latex: \"\",\n    };\n  },\n\n  addOptions() {\n    return {\n      shouldRender: (state, pos) => {\n        const $pos = state.doc.resolve(pos);\n\n        if (!$pos.parent.isTextblock) {\n          return false;\n        }\n        \n        return $pos.parent.type.name !== \"codeBlock\";\n      },\n      katexOptions: {\n        throwOnError: false,\n      },\n      HTMLAttributes: {},\n    };\n  },\n\n  addCommands() {\n    return {\n      setLatex:\n        ({ latex }) =>\n        ({ chain, state }) => {\n          if (!latex) {\n            return false;\n          }\n          const { from, to, $anchor } = state.selection;\n\n          if (!this.options.shouldRender(state, $anchor.pos)) {\n            return false;\n          }\n\n          return chain()\n            .insertContentAt(\n              { from: from, to: to },\n              {\n                type: \"math\",\n                attrs: {\n                  latex: latex,\n                },\n              }\n            )\n            .setTextSelection({ from: from, to: from + 1 })\n            .run();\n        },\n      unsetLatex:\n        () =>\n        ({ editor, state, chain }) => {\n          const latex = editor.getAttributes(this.name).latex;\n          if (typeof latex !== \"string\") {\n            return false;\n          }\n\n          const { from, to } = state.selection;\n\n          return chain()\n            .command(({ tr }) => {\n              tr.insertText(latex, from, to);\n              return true;\n            })\n            .setTextSelection({\n              from: from,\n              to: from + latex.length,\n            })\n            .run();\n        },\n    };\n  },\n\n  parseHTML() {\n    return [{ tag: `span[data-type=\"${this.name}\"]` }];\n  },\n\n  renderHTML({ node, HTMLAttributes }) {\n    const latex = node.attrs[\"latex\"] ?? \"\";\n    return [\n      \"span\",\n      mergeAttributes(HTMLAttributes, {\n        \"data-type\": this.name,\n      }),\n      latex,\n    ];\n  },\n\n  renderText({ node }) {\n    return node.attrs[\"latex\"] ?? \"\";\n  },\n\n  addNodeView() {\n    return ({ node, HTMLAttributes, getPos, editor }) => {\n      const dom = document.createElement(\"span\");\n      const latex: string = node.attrs[\"latex\"] ?? \"\";\n\n      Object.entries(this.options.HTMLAttributes).forEach(([key, value]) => {\n        dom.setAttribute(key, value);\n      });\n\n      Object.entries(HTMLAttributes).forEach(([key, value]) => {\n        dom.setAttribute(key, value);\n      });\n\n      dom.addEventListener(\"click\", (evt) => {\n        if (editor.isEditable && typeof getPos === \"function\") {\n          const pos = getPos();\n          const nodeSize = node.nodeSize;\n          editor.commands.setTextSelection({ from: pos, to: pos + nodeSize });\n        }\n      });\n\n      dom.contentEditable = \"false\";\n\n      dom.innerHTML = katex.renderToString(latex, this.options.katexOptions);\n\n      return {\n        dom: dom,\n      };\n    };\n  },\n});\n"
  },
  {
    "path": "packages/headless/src/extensions/slash-command.tsx",
    "content": "import { Extension } from \"@tiptap/core\";\nimport type { Editor, Range } from \"@tiptap/core\";\nimport { ReactRenderer } from \"@tiptap/react\";\nimport Suggestion, { type SuggestionOptions } from \"@tiptap/suggestion\";\nimport type { RefObject } from \"react\";\nimport type { ReactNode } from \"react\";\nimport tippy, { type GetReferenceClientRect, type Instance, type Props } from \"tippy.js\";\nimport { EditorCommandOut } from \"../components/editor-command\";\n\nconst Command = Extension.create({\n  name: \"slash-command\",\n  addOptions() {\n    return {\n      suggestion: {\n        char: \"/\",\n        command: ({ editor, range, props }) => {\n          props.command({ editor, range });\n        },\n      } as SuggestionOptions,\n    };\n  },\n  addProseMirrorPlugins() {\n    return [\n      Suggestion({\n        editor: this.editor,\n        ...this.options.suggestion,\n      }),\n    ];\n  },\n});\n\nconst renderItems = (elementRef?: RefObject<Element> | null) => {\n  let component: ReactRenderer | null = null;\n  let popup: Instance<Props>[] | null = null;\n\n  return {\n    onStart: (props: { editor: Editor; clientRect: DOMRect }) => {\n      component = new ReactRenderer(EditorCommandOut, {\n        props,\n        editor: props.editor,\n      });\n\n      const { selection } = props.editor.state;\n\n      const parentNode = selection.$from.node(selection.$from.depth);\n      const blockType = parentNode.type.name;\n\n      if (blockType === \"codeBlock\") {\n        return false;\n      }\n\n      // @ts-ignore\n      popup = tippy(\"body\", {\n        getReferenceClientRect: props.clientRect,\n        appendTo: () => (elementRef ? elementRef.current : document.body),\n        content: component.element,\n        showOnCreate: true,\n        interactive: true,\n        trigger: \"manual\",\n        placement: \"bottom-start\",\n      });\n    },\n    onUpdate: (props: { editor: Editor; clientRect: GetReferenceClientRect }) => {\n      component?.updateProps(props);\n\n      popup?.[0]?.setProps({\n        getReferenceClientRect: props.clientRect,\n      });\n    },\n\n    onKeyDown: (props: { event: KeyboardEvent }) => {\n      if (props.event.key === \"Escape\") {\n        popup?.[0]?.hide();\n\n        return true;\n      }\n\n      // @ts-ignore\n      return component?.ref?.onKeyDown(props);\n    },\n    onExit: () => {\n      popup?.[0]?.destroy();\n      component?.destroy();\n    },\n  };\n};\n\nexport interface SuggestionItem {\n  title: string;\n  description: string;\n  icon: ReactNode;\n  searchTerms?: string[];\n  command?: (props: { editor: Editor; range: Range }) => void;\n}\n\nexport const createSuggestionItems = (items: SuggestionItem[]) => items;\n\nexport const handleCommandNavigation = (event: KeyboardEvent) => {\n  if ([\"ArrowUp\", \"ArrowDown\", \"Enter\"].includes(event.key)) {\n    const slashCommand = document.querySelector(\"#slash-command\");\n    if (slashCommand) {\n      return true;\n    }\n  }\n};\n\nexport { Command, renderItems };\n"
  },
  {
    "path": "packages/headless/src/extensions/twitter.tsx",
    "content": "import { Node, mergeAttributes, nodePasteRule } from \"@tiptap/core\";\nimport { NodeViewWrapper, ReactNodeViewRenderer, type ReactNodeViewRendererOptions } from \"@tiptap/react\";\nimport { Tweet } from \"react-tweet\";\nexport const TWITTER_REGEX_GLOBAL = /(https?:\\/\\/)?(www\\.)?x\\.com\\/([a-zA-Z0-9_]{1,15})(\\/status\\/(\\d+))?(\\/\\S*)?/g;\nexport const TWITTER_REGEX = /^https?:\\/\\/(www\\.)?x\\.com\\/([a-zA-Z0-9_]{1,15})(\\/status\\/(\\d+))?(\\/\\S*)?$/;\n\nexport const isValidTwitterUrl = (url: string) => {\n  return url.match(TWITTER_REGEX);\n};\n\nconst TweetComponent = ({ node }: { node: Partial<ReactNodeViewRendererOptions> }) => {\n  const url = (node?.attrs as Record<string, string>)?.src;\n  const tweetId = url?.split(\"/\").pop();\n\n  if (!tweetId) {\n    return null;\n  }\n\n  return (\n    <NodeViewWrapper>\n      <div data-twitter=\"\">\n        <Tweet id={tweetId} />\n      </div>\n    </NodeViewWrapper>\n  );\n};\n\nexport interface TwitterOptions {\n  /**\n   * Controls if the paste handler for tweets should be added.\n   * @default true\n   * @example false\n   */\n  addPasteHandler: boolean;\n\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  HTMLAttributes: Record<string, any>;\n\n  /**\n   * Controls if the twitter node should be inline or not.\n   * @default false\n   * @example true\n   */\n  inline: boolean;\n\n  /**\n   * The origin of the tweet.\n   * @default ''\n   * @example 'https://tiptap.dev'\n   */\n  origin: string;\n}\n\n/**\n * The options for setting a tweet.\n */\ntype SetTweetOptions = { src: string };\n\ndeclare module \"@tiptap/core\" {\n  interface Commands<ReturnType> {\n    twitter: {\n      /**\n       * Insert a tweet\n       * @param options The tweet attributes\n       * @example editor.commands.setTweet({ src: 'https://x.com/seanpk/status/1800145949580517852' })\n       */\n      setTweet: (options: SetTweetOptions) => ReturnType;\n    };\n  }\n}\n\n/**\n * This extension adds support for tweets.\n */\nexport const Twitter = Node.create<TwitterOptions>({\n  name: \"twitter\",\n\n  addOptions() {\n    return {\n      addPasteHandler: true,\n      HTMLAttributes: {},\n      inline: false,\n      origin: \"\",\n    };\n  },\n\n  addNodeView() {\n    return ReactNodeViewRenderer(TweetComponent, { attrs: this.options.HTMLAttributes });\n  },\n\n  inline() {\n    return this.options.inline;\n  },\n\n  group() {\n    return this.options.inline ? \"inline\" : \"block\";\n  },\n\n  draggable: true,\n\n  addAttributes() {\n    return {\n      src: {\n        default: null,\n      },\n    };\n  },\n\n  parseHTML() {\n    return [\n      {\n        tag: \"div[data-twitter]\",\n      },\n    ];\n  },\n\n  addCommands() {\n    return {\n      setTweet:\n        (options: SetTweetOptions) =>\n        ({ commands }) => {\n          if (!isValidTwitterUrl(options.src)) {\n            return false;\n          }\n\n          return commands.insertContent({\n            type: this.name,\n            attrs: options,\n          });\n        },\n    };\n  },\n\n  addPasteRules() {\n    if (!this.options.addPasteHandler) {\n      return [];\n    }\n\n    return [\n      nodePasteRule({\n        find: TWITTER_REGEX_GLOBAL,\n        type: this.type,\n        getAttributes: (match) => {\n          return { src: match.input };\n        },\n      }),\n    ];\n  },\n\n  renderHTML({ HTMLAttributes }) {\n    return [\"div\", mergeAttributes({ \"data-twitter\": \"\" }, HTMLAttributes)];\n  },\n});\n"
  },
  {
    "path": "packages/headless/src/extensions/updated-image.ts",
    "content": "import Image from \"@tiptap/extension-image\";\n\nconst UpdatedImage = Image.extend({\n  name: \"image\",\n  addAttributes() {\n    return {\n      ...this.parent?.(),\n      width: {\n        default: null,\n      },\n      height: {\n        default: null,\n      },\n    };\n  },\n});\n\nexport default UpdatedImage;\n"
  },
  {
    "path": "packages/headless/src/index.ts",
    "content": "// Components\nexport {\n  EditorRoot,\n  EditorContent,\n  type EditorContentProps,\n  EditorBubble,\n  EditorBubbleItem,\n  EditorCommand,\n  EditorCommandList,\n  EditorCommandItem,\n  EditorCommandEmpty,\n  useEditor,\n  type EditorInstance,\n  type JSONContent,\n} from \"./components\";\n\n// Extensions\nexport {\n  AIHighlight,\n  removeAIHighlight,\n  addAIHighlight,\n  CodeBlockLowlight,\n  HorizontalRule,\n  ImageResizer,\n  InputRule,\n  Placeholder,\n  StarterKit,\n  TaskItem,\n  TaskList,\n  TiptapImage,\n  TiptapUnderline,\n  MarkdownExtension,\n  TextStyle,\n  Color,\n  HighlightExtension,\n  CustomKeymap,\n  TiptapLink,\n  UpdatedImage,\n  Youtube,\n  Twitter,\n  Mathematics,\n  CharacterCount,\n  GlobalDragHandle,\n  Command,\n  renderItems,\n  createSuggestionItems,\n  handleCommandNavigation,\n  type SuggestionItem,\n} from \"./extensions\";\n\n// Plugins\nexport {\n  UploadImagesPlugin,\n  type UploadFn,\n  type ImageUploadOptions,\n  createImageUpload,\n  handleImageDrop,\n  handleImagePaste,\n} from \"./plugins\";\n\n// Utils\nexport {\n  isValidUrl,\n  getUrlFromString,\n  getPrevText,\n  getAllContent,\n} from \"./utils\";\n\n// Store and Atoms\nexport { queryAtom, rangeAtom } from \"./utils/atoms\";\n"
  },
  {
    "path": "packages/headless/src/plugins/index.ts",
    "content": "export {\n  UploadImagesPlugin,\n  type UploadFn,\n  type ImageUploadOptions,\n  createImageUpload,\n  handleImageDrop,\n  handleImagePaste,\n} from \"./upload-images\";\n"
  },
  {
    "path": "packages/headless/src/plugins/upload-images.tsx",
    "content": "import { type EditorState, Plugin, PluginKey } from \"@tiptap/pm/state\";\nimport { Decoration, DecorationSet, type EditorView } from \"@tiptap/pm/view\";\n\nconst uploadKey = new PluginKey(\"upload-image\");\n\nexport const UploadImagesPlugin = ({ imageClass }: { imageClass: string }) =>\n  new Plugin({\n    key: uploadKey,\n    state: {\n      init() {\n        return DecorationSet.empty;\n      },\n      apply(tr, set) {\n        set = set.map(tr.mapping, tr.doc);\n        // See if the transaction adds or removes any placeholders\n        //@ts-expect-error - not yet sure what the type I need here\n        const action = tr.getMeta(this);\n        if (action?.add) {\n          const { id, pos, src } = action.add;\n\n          const placeholder = document.createElement(\"div\");\n          placeholder.setAttribute(\"class\", \"img-placeholder\");\n          const image = document.createElement(\"img\");\n          image.setAttribute(\"class\", imageClass);\n          image.src = src;\n          placeholder.appendChild(image);\n          const deco = Decoration.widget(pos + 1, placeholder, {\n            id,\n          });\n          set = set.add(tr.doc, [deco]);\n        } else if (action?.remove) {\n          // biome-ignore lint/suspicious/noDoubleEquals: <explanation>\n          set = set.remove(set.find(undefined, undefined, (spec) => spec.id == action.remove.id));\n        }\n        return set;\n      },\n    },\n    props: {\n      decorations(state) {\n        return this.getState(state);\n      },\n    },\n  });\n\n// biome-ignore lint/complexity/noBannedTypes: <explanation>\nfunction findPlaceholder(state: EditorState, id: {}) {\n  const decos = uploadKey.getState(state) as DecorationSet;\n  // biome-ignore lint/suspicious/noDoubleEquals: <explanation>\n  const found = decos.find(undefined, undefined, (spec) => spec.id == id);\n  return found.length ? found[0]?.from : null;\n}\n\nexport interface ImageUploadOptions {\n  validateFn?: (file: File) => void;\n  onUpload: (file: File) => Promise<unknown>;\n}\n\nexport const createImageUpload =\n  ({ validateFn, onUpload }: ImageUploadOptions): UploadFn =>\n  (file, view, pos) => {\n    // check if the file is an image\n    const validated = validateFn?.(file);\n    if (!validated) return;\n    // A fresh object to act as the ID for this upload\n    const id = {};\n\n    // Replace the selection with a placeholder\n    const tr = view.state.tr;\n    if (!tr.selection.empty) tr.deleteSelection();\n\n    const reader = new FileReader();\n    reader.readAsDataURL(file);\n    reader.onload = () => {\n      tr.setMeta(uploadKey, {\n        add: {\n          id,\n          pos,\n          src: reader.result,\n        },\n      });\n      view.dispatch(tr);\n    };\n\n    onUpload(file).then((src) => {\n      const { schema } = view.state;\n\n      const pos = findPlaceholder(view.state, id);\n\n      // If the content around the placeholder has been deleted, drop\n      // the image\n      if (pos == null) return;\n\n      // Otherwise, insert it at the placeholder's position, and remove\n      // the placeholder\n\n      // When BLOB_READ_WRITE_TOKEN is not valid or unavailable, read\n      // the image locally\n      const imageSrc = typeof src === \"object\" ? reader.result : src;\n\n      const node = schema.nodes.image?.create({ src: imageSrc });\n      if (!node) return;\n\n      const transaction = view.state.tr.replaceWith(pos, pos, node).setMeta(uploadKey, { remove: { id } });\n      view.dispatch(transaction);\n    }, () => {\n      // Deletes the image placeholder on error\n      const transaction = view.state.tr\n        .delete(pos, pos)\n        .setMeta(uploadKey, { remove: { id } });\n      view.dispatch(transaction);\n    });\n  };\n\nexport type UploadFn = (file: File, view: EditorView, pos: number) => void;\n\nexport const handleImagePaste = (view: EditorView, event: ClipboardEvent, uploadFn: UploadFn) => {\n  if (event.clipboardData?.files.length) {\n    event.preventDefault();\n    const [file] = Array.from(event.clipboardData.files);\n    const pos = view.state.selection.from;\n\n    if (file) uploadFn(file, view, pos);\n    return true;\n  }\n  return false;\n};\n\nexport const handleImageDrop = (view: EditorView, event: DragEvent, moved: boolean, uploadFn: UploadFn) => {\n  if (!moved && event.dataTransfer?.files.length) {\n    event.preventDefault();\n    const [file] = Array.from(event.dataTransfer.files);\n    const coordinates = view.posAtCoords({\n      left: event.clientX,\n      top: event.clientY,\n    });\n    // here we deduct 1 from the pos or else the image will create an extra node\n    if (file) uploadFn(file, view, coordinates?.pos ?? 0 - 1);\n    return true;\n  }\n  return false;\n};\n"
  },
  {
    "path": "packages/headless/src/utils/atoms.ts",
    "content": "import { atom } from \"jotai\";\nimport type { Range } from \"@tiptap/core\";\n\nexport const queryAtom = atom(\"\");\nexport const rangeAtom = atom<Range | null>(null);\n"
  },
  {
    "path": "packages/headless/src/utils/index.ts",
    "content": "import { Fragment, type Node } from \"@tiptap/pm/model\";\nimport type { EditorInstance } from \"../components\";\n\nexport function isValidUrl(url: string) {\n  try {\n    new URL(url);\n    return true;\n  } catch (_e) {\n    return false;\n  }\n}\n\nexport function getUrlFromString(str: string) {\n  if (isValidUrl(str)) return str;\n  try {\n    if (str.includes(\".\") && !str.includes(\" \")) {\n      return new URL(`https://${str}`).toString();\n    }\n  } catch (_e) {\n    return null;\n  }\n}\n\n// Get the text before a given position in markdown format\nexport const getPrevText = (editor: EditorInstance, position: number) => {\n  const nodes: Node[] = [];\n  editor.state.doc.forEach((node, pos) => {\n    if (pos >= position) return false;\n    nodes.push(node);\n    return true;\n  });\n  const fragment = Fragment.fromArray(nodes);\n  const doc = editor.state.doc.copy(fragment);\n\n  return editor.storage.markdown.serializer.serialize(doc) as string;\n};\n\n// Get all content from the editor in markdown format\nexport const getAllContent = (editor: EditorInstance) => {\n  const fragment = editor.state.doc.content;\n  const doc = editor.state.doc.copy(fragment);\n\n  return editor.storage.markdown.serializer.serialize(doc) as string;\n};\n"
  },
  {
    "path": "packages/headless/src/utils/store.ts",
    "content": "import { createStore } from \"jotai\";\n\n// biome-ignore lint/suspicious/noExplicitAny: <explanation>\nexport const novelStore: any = createStore();\nexport * from \"jotai\";\n"
  },
  {
    "path": "packages/headless/tsconfig.json",
    "content": "{\n  \"extends\": \"tsconfig/react.json\",\n  \"include\": [\"./src/**/*\"],\n  \"exclude\": [\"dist\", \"build\", \"node_modules\"],\n  \"compilerOptions\": {\n    \"declarationMap\": false,\n    \"outDir\": \"dist\"\n  }\n}\n"
  },
  {
    "path": "packages/headless/tsup.config.ts",
    "content": "import { defineConfig, Options } from \"tsup\";\n\nexport default defineConfig((options: Options) => ({\n  entry: [\"src/index.ts\"],\n  banner: {\n    js: \"'use client'\",\n  },\n  minify: true,\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  clean: true,\n  external: [\"react\", \"react-dom\"],\n  ...options,\n}));\n"
  },
  {
    "path": "packages/tsconfig/base.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"alwaysStrict\": false,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"noEmit\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n\n    \"downlevelIteration\": true,\n    \"allowJs\": true,\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"skipLibCheck\": true,\n    \"skipDefaultLibCheck\": true,\n    \"incremental\": true,\n    \"tsBuildInfoFile\": \".tsbuildinfo\"\n  },\n  \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\", \"src/tests\"]\n}\n"
  },
  {
    "path": "packages/tsconfig/next.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Next.js\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": false,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"incremental\": true,\n    \"esModuleInterop\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\"\n  },\n  \"include\": [\"src\", \"next-env.d.ts\"]\n}\n"
  },
  {
    "path": "packages/tsconfig/package.json",
    "content": "{\n  \"name\": \"tsconfig\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"license\": \"MIT\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "packages/tsconfig/react.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"React Library\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\"],\n    \"target\": \"ESNext\",\n    \"jsx\": \"react-jsx\"\n  }\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - \"apps/*\"\n  - \"packages/*\""
  },
  {
    "path": "prettier.config.js",
    "content": "module.exports = {\n  bracketSpacing: true,\n  semi: true,\n  trailingComma: \"all\",\n  printWidth: 80,\n  tabWidth: 2,\n};\n"
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"globalDependencies\": [\n    \"**/.env.*local\"\n  ],\n  \"tasks\": {\n    \"topo\": {\n      \"dependsOn\": [\n        \"^topo\"\n      ]\n    },\n    \"build\": {\n      \"dependsOn\": [\n        \"^build\",\n        \"typecheck\"\n      ],\n      \"outputs\": [\n        \"dist/**\",\n        \".next/**\",\n        \"!.next/cache/**\"\n      ]\n    },\n    \"typecheck\": {\n      \"dependsOn\": [\n        \"^topo\"\n      ],\n      \"outputs\": []\n    },\n    \"lint\": {\n      \"dependsOn\": [\n        \"^topo\"\n      ]\n    },\n    \"format\": {\n      \"dependsOn\": [\n        \"^topo\"\n      ]\n    },\n    \"lint:fix\": {\n      \"dependsOn\": [\n        \"^topo\"\n      ]\n    },\n    \"format:fix\": {\n      \"dependsOn\": [\n        \"^topo\"\n      ]\n    },\n    \"check-types\": {},\n    \"dev\": {\n      \"cache\": false,\n      \"persistent\": true\n    },\n    \"clean\": {\n      \"cache\": false\n    },\n    \"release\": {\n      \"cache\": false\n    }\n  }\n}"
  }
]