├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── npmpublish.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── apps ├── demo-react-router │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── app.css │ │ ├── root.tsx │ │ ├── routes.ts │ │ └── routes │ │ │ └── home.tsx │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── udm_se_ds.pdf │ ├── react-router.config.ts │ ├── tsconfig.json │ └── vite.config.ts ├── demo │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── udm_se_ds.pdf │ ├── src │ │ ├── App.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── docs │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── prettier.config.js │ ├── src │ ├── app │ │ ├── docs │ │ │ ├── basic-usage │ │ │ │ └── page.md │ │ │ ├── customization │ │ │ │ └── page.md │ │ │ ├── examples │ │ │ │ └── page.md │ │ │ └── installation │ │ │ │ └── page.md │ │ ├── favicon.ico │ │ ├── layout.tsx │ │ ├── not-found.tsx │ │ ├── page.md │ │ └── providers.tsx │ ├── components │ │ ├── Button.tsx │ │ ├── Callout.tsx │ │ ├── DocsHeader.tsx │ │ ├── DocsLayout.tsx │ │ ├── Fence.tsx │ │ ├── Hero.tsx │ │ ├── HeroBackground.tsx │ │ ├── Icon.tsx │ │ ├── Layout.tsx │ │ ├── Logo.tsx │ │ ├── MobileNavigation.tsx │ │ ├── Navigation.tsx │ │ ├── PrevNextLinks.tsx │ │ ├── Prose.tsx │ │ ├── QuickLinks.tsx │ │ ├── Search.tsx │ │ ├── TableOfContents.tsx │ │ ├── ThemeSelector.tsx │ │ └── icons │ │ │ ├── InstallationIcon.tsx │ │ │ ├── LightbulbIcon.tsx │ │ │ ├── PluginsIcon.tsx │ │ │ ├── PresetsIcon.tsx │ │ │ ├── ThemingIcon.tsx │ │ │ └── WarningIcon.tsx │ ├── fonts │ │ ├── lexend.txt │ │ └── lexend.woff2 │ ├── images │ │ ├── blur-cyan.png │ │ └── blur-indigo.png │ ├── lib │ │ ├── navigation.ts │ │ └── sections.ts │ ├── markdoc │ │ ├── nodes.js │ │ ├── search.mjs │ │ └── tags.js │ └── styles │ │ ├── prism.css │ │ └── tailwind.css │ ├── tsconfig.json │ └── types.d.ts ├── package.json ├── packages └── react-pdf-js │ ├── README.md │ ├── package.json │ ├── src │ └── index.tsx │ ├── test │ └── index.test.tsx │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mikecousins 4 | patreon: # Replace with a single Patreon username 5 | open_collective: react-pdf-js 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "12:00" 8 | open-pull-requests-limit: 10 9 | reviewers: 10 | - mikecousins 11 | assignees: 12 | - mikecousins 13 | ignore: 14 | - dependency-name: "@testing-library/react" 15 | versions: 16 | - 11.2.2 17 | - 11.2.3 18 | - dependency-name: typescript 19 | versions: 20 | - 4.1.3 21 | - dependency-name: "@types/jest" 22 | versions: 23 | - 26.0.19 24 | - dependency-name: "@types/react-dom" 25 | versions: 26 | - 17.0.0 27 | - dependency-name: "@types/react" 28 | versions: 29 | - 17.0.0 30 | - dependency-name: tslib 31 | versions: 32 | - 2.0.3 33 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: 12 19 | - run: yarn 20 | - run: yarn lint 21 | - run: yarn test:coverage 22 | - run: yarn build 23 | - name: Upload coverage to Codecov 24 | uses: codecov/codecov-action@v1.0.5 25 | with: 26 | token: ${{secrets.CODECOV_TOKEN}} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | node_modules 6 | /umd 7 | npm-debug.log* 8 | .rts2_cache_cjs 9 | .rts2_cache_es 10 | .rts2_cache_esm 11 | .rts2_cache_umd 12 | dist 13 | .cache 14 | 15 | .pnp.* 16 | .yarn/* 17 | !.yarn/patches 18 | !.yarn/plugins 19 | !.yarn/releases 20 | !.yarn/sdks 21 | !.yarn/versions 22 | 23 | .turbo 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at mike@mikecousins.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mike Cousins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-pdf-js 2 | 3 | `react-pdf-js` provides a component for rendering PDF documents using [PDF.js](http://mozilla.github.io/pdf.js/). 4 | 5 | --- 6 | 7 | [![NPM Version](https://img.shields.io/npm/v/@mikecousins/react-pdf.svg?style=flat-square)](https://www.npmjs.com/package/@mikecousins/react-pdf) 8 | [![NPM Downloads](https://img.shields.io/npm/dm/@mikecousins/react-pdf.svg?style=flat-square)](https://www.npmjs.com/package/@mikecousins/react-pdf) 9 | [![codecov](https://codecov.io/gh/mikecousins/react-pdf-js/branch/master/graph/badge.svg)](https://codecov.io/gh/mikecousins/react-pdf-js) 10 | 11 | # Demo 12 | 13 | [https://react-pdf.cousins.ai](https://react-pdf.cousins.ai/) 14 | 15 | # Usage 16 | 17 | Install with `yarn add @mikecousins/react-pdf pdfjs-dist` or `npm install @mikecousins/react-pdf pdfjs-dist` 18 | 19 | ## `usePdf` hook 20 | 21 | Use the hook in your app (showing some basic pagination as well): 22 | 23 | ```js 24 | import React, { useState, useRef } from 'react'; 25 | import { usePdf } from '@mikecousins/react-pdf'; 26 | 27 | const MyPdfViewer = () => { 28 | const [page, setPage] = useState(1); 29 | const canvasRef = useRef(null); 30 | 31 | const { pdfDocument, pdfPage } = usePdf({ 32 | file: 'test.pdf', 33 | page, 34 | canvasRef, 35 | }); 36 | 37 | return ( 38 |
39 | {!pdfDocument && Loading...} 40 | 41 | {Boolean(pdfDocument && pdfDocument.numPages) && ( 42 | 59 | )} 60 |
61 | ); 62 | }; 63 | ``` 64 | 65 | ## Props 66 | 67 | When you call usePdf you'll want to pass in a subset of these props, like this: 68 | 69 | > `const { pdfDocument, pdfPage } = usePdf({ canvasRef, file: 'https://example.com/test.pdf', page });` 70 | 71 | ### canvasRef 72 | 73 | A reference to the canvas element. Create with: 74 | 75 | > `const canvasRef = useRef(null);` 76 | 77 | and then render it like: 78 | 79 | > `` 80 | 81 | and then pass it into usePdf. 82 | 83 | ### file 84 | 85 | URL of the PDF file. 86 | 87 | ### onDocumentLoadSuccess 88 | 89 | Allows you to specify a callback that is called when the PDF document data will be fully loaded. 90 | Callback is called with [PDFDocumentProxy](https://github.com/mozilla/pdf.js/blob/master/src/display/api.js#L579) 91 | as an only argument. 92 | 93 | ### onDocumentLoadFail 94 | 95 | Allows you to specify a callback that is called after an error occurred during PDF document data loading. 96 | 97 | ### onPageLoadSuccess 98 | 99 | Allows you to specify a callback that is called when the PDF page data will be fully loaded. 100 | Callback is called with [PDFPageProxy](https://github.com/mozilla/pdf.js/blob/master/src/display/api.js#L897) 101 | as an only argument. 102 | 103 | ### onPageLoadFail 104 | 105 | Allows you to specify a callback that is called after an error occurred during PDF page data loading. 106 | 107 | ### onPageRenderSuccess 108 | 109 | Allows you to specify a callback that is called when the PDF page will be fully rendered into the DOM. 110 | Callback is called with [PDFPageProxy](https://github.com/mozilla/pdf.js/blob/master/src/display/api.js#L897) 111 | as an only argument. 112 | 113 | ### onPageRenderFail 114 | 115 | Allows you to specify a callback that is called after an error occurred during PDF page rendering. 116 | 117 | ### page 118 | 119 | Specify the page that you want to display. Default = 1, 120 | 121 | ### scale 122 | 123 | Allows you to scale the PDF. Default = 1. 124 | 125 | ### rotate 126 | 127 | Allows you to rotate the PDF. Number is in degrees. Default = 0. 128 | 129 | ### cMapUrl 130 | 131 | Allows you to specify a cmap url. Default = '../node_modules/pdfjs-dist/cmaps/'. 132 | 133 | ### cMapPacked 134 | 135 | Allows you to specify whether the cmaps are packed or not. Default = false. 136 | 137 | ### workerSrc 138 | 139 | Allows you to specify a custom pdf worker url. Default = '//cdnjs.cloudflare.com/ajax/libs/pdf.js/\${pdfjs.version}/pdf.worker.js'. 140 | 141 | ### withCredentials 142 | 143 | Allows you to add the withCredentials flag. Default = false. 144 | 145 | ## Returned values 146 | 147 | ### pdfDocument 148 | 149 | `pdfjs`'s `PDFDocumentProxy` [object](https://github.com/mozilla/pdf.js/blob/master/src/display/api.js#L579). 150 | This can be undefined if document has not been loaded yet. 151 | 152 | ### pdfPage 153 | 154 | `pdfjs`'s `PDFPageProxy` [object](https://github.com/mozilla/pdf.js/blob/master/src/display/api.js#L897) 155 | This can be undefined if page has not been loaded yet. 156 | 157 | # License 158 | 159 | MIT © [mikecousins](https://github.com/mikecousins) 160 | -------------------------------------------------------------------------------- /apps/demo-react-router/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | 4 | # React Router 5 | /.react-router/ 6 | /build/ 7 | -------------------------------------------------------------------------------- /apps/demo-react-router/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! 2 | 3 | A modern, production-ready template for building full-stack React applications using React Router. 4 | 5 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default) 6 | 7 | ## Features 8 | 9 | - 🚀 Server-side rendering 10 | - ⚡️ Hot Module Replacement (HMR) 11 | - 📦 Asset bundling and optimization 12 | - 🔄 Data loading and mutations 13 | - 🔒 TypeScript by default 14 | - 🎉 TailwindCSS for styling 15 | - 📖 [React Router docs](https://reactrouter.com/) 16 | 17 | ## Getting Started 18 | 19 | ### Installation 20 | 21 | Install the dependencies: 22 | 23 | ```bash 24 | npm install 25 | ``` 26 | 27 | ### Development 28 | 29 | Start the development server with HMR: 30 | 31 | ```bash 32 | npm run dev 33 | ``` 34 | 35 | Your application will be available at `http://localhost:5173`. 36 | 37 | ## Building for Production 38 | 39 | Create a production build: 40 | 41 | ```bash 42 | npm run build 43 | ``` 44 | 45 | ## Deployment 46 | 47 | ### Docker Deployment 48 | 49 | To build and run using Docker: 50 | 51 | ```bash 52 | docker build -t my-app . 53 | 54 | # Run the container 55 | docker run -p 3000:3000 my-app 56 | ``` 57 | 58 | The containerized application can be deployed to any platform that supports Docker, including: 59 | 60 | - AWS ECS 61 | - Google Cloud Run 62 | - Azure Container Apps 63 | - Digital Ocean App Platform 64 | - Fly.io 65 | - Railway 66 | 67 | ### DIY Deployment 68 | 69 | If you're familiar with deploying Node applications, the built-in app server is production-ready. 70 | 71 | Make sure to deploy the output of `npm run build` 72 | 73 | ``` 74 | ├── package.json 75 | ├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) 76 | ├── build/ 77 | │ ├── client/ # Static assets 78 | │ └── server/ # Server-side code 79 | ``` 80 | 81 | ## Styling 82 | 83 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. 84 | 85 | --- 86 | 87 | Built with ❤️ using React Router. 88 | -------------------------------------------------------------------------------- /apps/demo-react-router/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @theme { 4 | --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, 5 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 6 | } 7 | 8 | html, 9 | body { 10 | @apply bg-white dark:bg-gray-950; 11 | 12 | @media (prefers-color-scheme: dark) { 13 | color-scheme: dark; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/demo-react-router/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | isRouteErrorResponse, 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "react-router"; 9 | 10 | import type { Route } from "./+types/root"; 11 | import "./app.css"; 12 | 13 | export const links: Route.LinksFunction = () => [ 14 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 15 | { 16 | rel: "preconnect", 17 | href: "https://fonts.gstatic.com", 18 | crossOrigin: "anonymous", 19 | }, 20 | { 21 | rel: "stylesheet", 22 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 23 | }, 24 | ]; 25 | 26 | export function Layout({ children }: { children: React.ReactNode }) { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | export default function App() { 45 | return ; 46 | } 47 | 48 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { 49 | let message = "Oops!"; 50 | let details = "An unexpected error occurred."; 51 | let stack: string | undefined; 52 | 53 | if (isRouteErrorResponse(error)) { 54 | message = error.status === 404 ? "404" : "Error"; 55 | details = 56 | error.status === 404 57 | ? "The requested page could not be found." 58 | : error.statusText || details; 59 | } else if (import.meta.env.DEV && error && error instanceof Error) { 60 | details = error.message; 61 | stack = error.stack; 62 | } 63 | 64 | return ( 65 |
66 |

{message}

67 |

{details}

68 | {stack && ( 69 |
70 |           {stack}
71 |         
72 | )} 73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /apps/demo-react-router/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig, index } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.tsx")] satisfies RouteConfig; 4 | -------------------------------------------------------------------------------- /apps/demo-react-router/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import type { Route } from './+types/home'; 2 | import { useRef, useState } from 'react'; 3 | import { usePdf } from '@mikecousins/react-pdf'; 4 | import clsx from 'clsx'; 5 | import { 6 | ArrowLeftCircleIcon, 7 | ArrowRightCircleIcon, 8 | } from '@heroicons/react/24/solid'; 9 | 10 | export function meta({}: Route.MetaArgs) { 11 | return [ 12 | { title: '@mikecousins/react-pdf' }, 13 | { 14 | name: 'description', 15 | content: 16 | 'The simplest PDF rendering library for modern websites. @mikecousins/react-pdf is super lightweight and will embed a PDF in your website easily.', 17 | }, 18 | ]; 19 | } 20 | 21 | export default function Home() { 22 | const [page, setPage] = useState(1); 23 | const canvasRef = useRef(null); 24 | 25 | const { pdfDocument } = usePdf({ 26 | file: 'udm_se_ds.pdf', 27 | page, 28 | canvasRef, 29 | scale: 0.4, 30 | }); 31 | 32 | const previousDisabled = page === 1; 33 | const nextDisabled = Boolean(page === pdfDocument?.numPages); 34 | 35 | return ( 36 |
37 |
38 |
39 |
40 | @mikecousins/react-pdf 41 |
42 |
43 | The easiest way to render PDFs in React.{' '} 44 | 48 | Under 1kB in size. 49 | {' '} 50 | Modern React hook architecture. 51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | 67 |
68 |
69 |
70 | {!pdfDocument && Loading...} 71 | 72 |
73 |
74 |
75 | 84 |
85 |
86 |
87 |
88 | 118 |
119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /apps/demo-react-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-react-router", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "react-router build", 7 | "dev": "react-router dev", 8 | "start": "react-router-serve ./build/server/index.js", 9 | "typecheck": "react-router typegen && tsc" 10 | }, 11 | "dependencies": { 12 | "@heroicons/react": "^2.2.0", 13 | "@mikecousins/react-pdf": "workspace:*", 14 | "@react-router/node": "^7.4.1", 15 | "@react-router/serve": "^7.4.1", 16 | "clsx": "^2.1.1", 17 | "isbot": "^5.1.25", 18 | "pdfjs-dist": "^5.0.375", 19 | "react": "^19.1.0", 20 | "react-dom": "^19.1.0", 21 | "react-router": "^7.4.1" 22 | }, 23 | "devDependencies": { 24 | "@react-router/dev": "^7.4.1", 25 | "@tailwindcss/vite": "^4.0.17", 26 | "@types/node": "^22.13.17", 27 | "@types/react": "^19.0.12", 28 | "@types/react-dom": "^19.0.4", 29 | "react-router-devtools": "^1.1.8", 30 | "tailwindcss": "^4.0.17", 31 | "typescript": "^5.8.2", 32 | "vite": "^6.2.4", 33 | "vite-tsconfig-paths": "^5.1.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/demo-react-router/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikecousins/react-pdf-js/911db12532691cfafa62c7d95a4f5b95c6c9704f/apps/demo-react-router/public/favicon.ico -------------------------------------------------------------------------------- /apps/demo-react-router/public/udm_se_ds.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikecousins/react-pdf-js/911db12532691cfafa62c7d95a4f5b95c6c9704f/apps/demo-react-router/public/udm_se_ds.pdf -------------------------------------------------------------------------------- /apps/demo-react-router/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | // Config options... 5 | // Server-side render by default, to enable SPA mode set this to `false` 6 | ssr: true, 7 | } satisfies Config; 8 | -------------------------------------------------------------------------------- /apps/demo-react-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*", 4 | "**/.server/**/*", 5 | "**/.client/**/*", 6 | ".react-router/types/**/*" 7 | ], 8 | "compilerOptions": { 9 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 10 | "types": ["node", "vite/client"], 11 | "target": "ES2022", 12 | "module": "ES2022", 13 | "moduleResolution": "bundler", 14 | "jsx": "react-jsx", 15 | "rootDirs": [".", "./.react-router/types"], 16 | "baseUrl": ".", 17 | "paths": { 18 | "~/*": ["./app/*"] 19 | }, 20 | "esModuleInterop": true, 21 | "verbatimModuleSyntax": true, 22 | "noEmit": true, 23 | "resolveJsonModule": true, 24 | "skipLibCheck": true, 25 | "strict": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/demo-react-router/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig({ 7 | plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], 8 | }); 9 | -------------------------------------------------------------------------------- /apps/demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /apps/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | @mikecousins/react-pdf - React PDF Library 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@heroicons/react": "^2.2.0", 13 | "@mikecousins/react-pdf": "workspace:*", 14 | "clsx": "^2.1.1", 15 | "react": "^19.1.0", 16 | "react-dom": "^19.1.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^19.0.12", 20 | "@types/react-dom": "^19.0.4", 21 | "@vitejs/plugin-react": "^4.3.4", 22 | "autoprefixer": "^10.4.21", 23 | "postcss": "^8.5.3", 24 | "tailwindcss": "^3.4.17", 25 | "typescript": "^5.8.2", 26 | "vite": "^6.2.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/demo/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /apps/demo/public/udm_se_ds.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikecousins/react-pdf-js/911db12532691cfafa62c7d95a4f5b95c6c9704f/apps/demo/public/udm_se_ds.pdf -------------------------------------------------------------------------------- /apps/demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import { usePdf } from '@mikecousins/react-pdf'; 3 | import { 4 | ArrowLeftCircleIcon, 5 | ArrowRightCircleIcon, 6 | } from '@heroicons/react/24/outline'; 7 | import './index.css'; 8 | import clsx from 'clsx'; 9 | 10 | function App() { 11 | const [page, setPage] = useState(1); 12 | const canvasRef = useRef(null); 13 | 14 | const { pdfDocument } = usePdf({ 15 | file: 'udm_se_ds.pdf', 16 | page, 17 | canvasRef, 18 | scale: 0.4, 19 | }); 20 | 21 | const previousDisabled = page === 1; 22 | const nextDisabled = Boolean(page === pdfDocument?.numPages); 23 | 24 | return ( 25 |
26 |
27 |
28 |
29 | @mikecousins/react-pdf 30 |
31 |
32 | The easiest way to render PDFs in React.{' '} 33 | 37 | Under 1kB in size. 38 | {' '} 39 | Modern React hook architecture. 40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 54 |
55 |
56 |
57 | {!pdfDocument && Loading...} 58 | 59 |
60 |
61 |
62 | 69 |
70 |
71 |
72 |
73 | 103 |
104 | ); 105 | } 106 | 107 | export default App; 108 | -------------------------------------------------------------------------------- /apps/demo/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /apps/demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | ReactDOM.createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | 9 | ) 10 | -------------------------------------------------------------------------------- /apps/demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/demo/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | build: { 8 | target: "es2022" 9 | }, 10 | esbuild: { 11 | target: "es2022" 12 | }, 13 | optimizeDeps:{ 14 | esbuildOptions: { 15 | target: "es2022", 16 | } 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /apps/docs/.gitignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | -------------------------------------------------------------------------------- /apps/docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2025-03-22 4 | 5 | - Update template to Tailwind CSS v4.0.15 6 | 7 | ## 2025-03-18 8 | 9 | - Fix heading spacing in callout component ([#1677](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1677)) 10 | 11 | ## 2025-02-10 12 | 13 | - Update template to Tailwind CSS v4.0.6 14 | 15 | ## 2025-01-23 16 | 17 | - Update template to Tailwind CSS v4.0 18 | 19 | ## 2024-06-21 20 | 21 | - Bump Headless UI dependency to v2.1 22 | 23 | ## 2024-06-18 24 | 25 | - Update `prettier` and `prettier-plugin-tailwindcss` dependencies 26 | 27 | ## 2024-05-31 28 | 29 | - Fix `npm audit` warnings 30 | 31 | ## 2024-05-07 32 | 33 | - Bump Headless UI dependency to v2.0 34 | 35 | ## 2024-01-17 36 | 37 | - Fix `sharp` dependency issues ([#1549](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1549)) 38 | 39 | ## 2024-01-10 40 | 41 | - Update Tailwind CSS, Next.js, Prettier, TypeScript, ESLint, and other dependencies 42 | - Update Tailwind `darkMode` setting to new `selector` option 43 | 44 | ## 2023-10-23 45 | 46 | - Bump Markdoc dependencies 47 | - Remove unnecessary Markdoc configuration in `next.config.mjs` file 48 | 49 | ## 2023-09-07 50 | 51 | - Added TypeScript version of template 52 | 53 | ## 2023-09-05 54 | 55 | - Add scroll position buffer for table of contents ([#1499](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1499)) 56 | 57 | ## 2023-08-15 58 | 59 | - Bump Next.js dependency 60 | 61 | ## 2023-08-11 62 | 63 | - Port template to Next.js app router 64 | 65 | ## 2023-07-24 66 | 67 | - Fix search rendering bug in Safari ([#1470](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1470)) 68 | 69 | ## 2023-07-18 70 | 71 | - Add 404 page 72 | - Sort imports 73 | 74 | ## 2023-05-16 75 | 76 | - Bump Next.js dependency 77 | 78 | ## 2023-05-15 79 | 80 | - Replace Algolia DocSearch with basic built-in search ([#1395](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1395)) 81 | 82 | ## 2023-04-11 83 | 84 | - Bump Next.js dependency 85 | 86 | ## 2023-04-05 87 | 88 | - Fix listbox console error ([#1442](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1442)) 89 | 90 | ## 2023-03-29 91 | 92 | - Bump Tailwind CSS and Prettier dependencies 93 | - Sort classes 94 | 95 | ## 2023-03-22 96 | 97 | - Bump Headless UI dependency 98 | 99 | ## 2023-02-15 100 | 101 | - Remove `passive` option from `removeEventListener` 102 | 103 | ## 2023-02-02 104 | 105 | - Bump Headless UI dependency 106 | - Sort imports 107 | 108 | ## 2022-11-04 109 | 110 | - Bump Tailwind CSS and Next.js dependencies 111 | 112 | ## 2022-09-27 113 | 114 | - Update Headless UI, Next.js, Markdoc, and Autoprefixer dependencies 115 | - Fix nav sidebar overflow issue ([#1337](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1337)) 116 | 117 | ## 2022-09-19 118 | 119 | - Fix bug with theme switching ([#1325](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1325)) 120 | 121 | ## 2022-09-09 122 | 123 | - Update Next.js dependency 124 | 125 | ## 2022-09-07 126 | 127 | - Update Headless UI dependency 128 | 129 | ## 2022-09-01 130 | 131 | - Update Tailwind CSS, Next.js, Headless UI, ESLint, and other dependencies 132 | 133 | ## 2022-08-16 134 | 135 | - Enable experimental Next.js `scrollRestoration` flag 136 | 137 | ## 2022-07-26 138 | 139 | - Fix issue with table customizations ([#1278](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1278)) 140 | 141 | ## 2022-07-25 142 | 143 | - Update Next.js and React dependencies 144 | 145 | ## 2022-07-11 146 | 147 | - Add `.env.example` file ([#1260](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1260)) 148 | 149 | ## 2022-07-07 150 | 151 | - Fix duplicated empty lines in code blocks 152 | 153 | ## 2022-07-06 154 | 155 | - Replace `next/image` with `next/future/image` 156 | 157 | ## 2022-06-23 158 | 159 | - Initial release 160 | -------------------------------------------------------------------------------- /apps/docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | # Tailwind Plus License 2 | 3 | ## Personal License 4 | 5 | Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates. 6 | 7 | The license grants permission to **one individual** (the Licensee) to access and use the Components and Templates. 8 | 9 | You **can**: 10 | 11 | - Use the Components and Templates to create unlimited End Products. 12 | - Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license. 13 | - Use the Components and Templates to create unlimited End Products for unlimited Clients. 14 | - Use the Components and Templates to create End Products where the End Product is sold to End Users. 15 | - Use the Components and Templates to create End Products that are open source and freely available to End Users. 16 | 17 | You **cannot**: 18 | 19 | - Use the Components and Templates to create End Products that are designed to allow an End User to build their own End Products using the Components and Templates or derivatives of the Components and Templates. 20 | - Re-distribute the Components and Templates or derivatives of the Components and Templates separately from an End Product, neither in code or as design assets. 21 | - Share your access to the Components and Templates with any other individuals. 22 | - Use the Components and Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc. 23 | 24 | ### Example usage 25 | 26 | Examples of usage **allowed** by the license: 27 | 28 | - Creating a personal website by yourself. 29 | - Creating a website or web application for a client that will be owned by that client. 30 | - Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application. 31 | - Creating a commercial self-hosted web application that is sold to end users for a one-time fee. 32 | - Creating a web application where the primary purpose is clearly not to simply re-distribute the components (like a conference organization app that uses the components for its UI for example) that is free and open source, where the source code is publicly available. 33 | 34 | Examples of usage **not allowed** by the license: 35 | 36 | - Creating a repository of your favorite Tailwind Plus components or templates (or derivatives based on Tailwind Plus components or templates) and publishing it publicly. 37 | - Creating a React or Vue version of Tailwind Plus and making it available either for sale or for free. 38 | - Create a Figma or Sketch UI kit based on the Tailwind Plus component designs. 39 | - Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind Plus. 40 | - Creating a theme, template, or project starter kit using the components or templates and making it available either for sale or for free. 41 | - Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free. 42 | 43 | In simple terms, use Tailwind Plus for anything you like as long as it doesn't compete with Tailwind Plus. 44 | 45 | ### Personal License Definitions 46 | 47 | Licensee is the individual who has purchased a Personal License. 48 | 49 | Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind Plus license. 50 | 51 | End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates. 52 | 53 | End User is a user of an End Product. 54 | 55 | Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document. 56 | 57 | ## Team License 58 | 59 | Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates. 60 | 61 | The license grants permission for **up to 25 Employees and Contractors of the Licensee** to access and use the Components and Templates. 62 | 63 | You **can**: 64 | 65 | - Use the Components and Templates to create unlimited End Products. 66 | - Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license. 67 | - Use the Components and Templates to create unlimited End Products for unlimited Clients. 68 | - Use the Components and Templates to create End Products where the End Product is sold to End Users. 69 | - Use the Components and Templates to create End Products that are open source and freely available to End Users. 70 | 71 | You **cannot**: 72 | 73 | - Use the Components or Templates to create End Products that are designed to allow an End User to build their own End Products using the Components or Templates or derivatives of the Components or Templates. 74 | - Re-distribute the Components or Templates or derivatives of the Components or Templates separately from an End Product. 75 | - Use the Components or Templates to create End Products that are the property of any individual or entity other than the Licensee or Clients of the Licensee. 76 | - Use the Components or Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc. 77 | 78 | ### Example usage 79 | 80 | Examples of usage **allowed** by the license: 81 | 82 | - Creating a website for your company. 83 | - Creating a website or web application for a client that will be owned by that client. 84 | - Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application. 85 | - Creating a commercial self-hosted web application that is sold to end users for a one-time fee. 86 | - Creating a web application where the primary purpose is clearly not to simply re-distribute the components or templates (like a conference organization app that uses the components or a template for its UI for example) that is free and open source, where the source code is publicly available. 87 | 88 | Examples of use **not allowed** by the license: 89 | 90 | - Creating a repository of your favorite Tailwind Plus components or template (or derivatives based on Tailwind Plus components or templates) and publishing it publicly. 91 | - Creating a React or Vue version of Tailwind Plus and making it available either for sale or for free. 92 | - Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind Plus. 93 | - Creating a theme or template using the components or templates and making it available either for sale or for free. 94 | - Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free. 95 | - Creating any End Product that is not the sole property of either your company or a client of your company. For example your employees/contractors can't use your company Tailwind Plus license to build their own websites or side projects. 96 | 97 | ### Team License Definitions 98 | 99 | Licensee is the business entity who has purchased a Team License. 100 | 101 | Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind Plus license. 102 | 103 | End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates. 104 | 105 | End User is a user of an End Product. 106 | 107 | Employee is a full-time or part-time employee of the Licensee. 108 | 109 | Contractor is an individual or business entity contracted to perform services for the Licensee. 110 | 111 | Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document. 112 | 113 | ## Enforcement 114 | 115 | If you are found to be in violation of the license, access to your Tailwind Plus account will be terminated, and a refund may be issued at our discretion. When license violation is blatant and malicious (such as intentionally redistributing the Components or Templates through private warez channels), no refund will be issued. 116 | 117 | The copyright of the Components and Templates is owned by Tailwind Labs Inc. You are granted only the permissions described in this license; all other rights are reserved. Tailwind Labs Inc. reserves the right to pursue legal remedies for any unauthorized use of the Components or Templates outside the scope of this license. 118 | 119 | ## Liability 120 | 121 | Tailwind Labs Inc.’s liability to you for costs, damages, or other losses arising from your use of the Components or Templates — including third-party claims against you — is limited to a refund of your license fee. Tailwind Labs Inc. may not be held liable for any consequential damages related to your use of the Components or Templates. 122 | 123 | This Agreement is governed by the laws of the Province of Ontario and the applicable laws of Canada. Legal proceedings related to this Agreement may only be brought in the courts of Ontario. You agree to service of process at the e-mail address on your original order. 124 | 125 | ## Questions? 126 | 127 | Unsure which license you need, or unsure if your use case is covered by our licenses? 128 | 129 | Email us at [support@tailwindcss.com](mailto:support@tailwindcss.com) with your questions. 130 | -------------------------------------------------------------------------------- /apps/docs/README.md: -------------------------------------------------------------------------------- 1 | # Syntax 2 | 3 | Syntax is a [Tailwind Plus](https://tailwindcss.com/plus) site template built using [Tailwind CSS](https://tailwindcss.com) and [Next.js](https://nextjs.org). 4 | 5 | ## Getting started 6 | 7 | To get started with this template, first install the npm dependencies: 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | Next, run the development server: 14 | 15 | ```bash 16 | npm run dev 17 | ``` 18 | 19 | Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website. 20 | 21 | ## Customizing 22 | 23 | You can start editing this template by modifying the files in the `/src` folder. The site will auto-update as you edit these files. 24 | 25 | ## Global search 26 | 27 | This template includes a global search that's powered by the [FlexSearch](https://github.com/nextapps-de/flexsearch) library. It's available by clicking the search input or by using the `⌘K` shortcut. 28 | 29 | This feature requires no configuration, and works out of the box by automatically scanning your documentation pages to build its index. You can adjust the search parameters by editing the `/src/markdoc/search.mjs` file. 30 | 31 | ## License 32 | 33 | This site template is a commercial product and is licensed under the [Tailwind Plus license](https://tailwindcss.com/plus/license). 34 | 35 | ## Learn more 36 | 37 | To learn more about the technologies used in this site template, see the following resources: 38 | 39 | - [Tailwind CSS](https://tailwindcss.com/docs) - the official Tailwind CSS documentation 40 | - [Next.js](https://nextjs.org/docs) - the official Next.js documentation 41 | - [Headless UI](https://headlessui.dev) - the official Headless UI documentation 42 | - [Markdoc](https://markdoc.io) - the official Markdoc documentation 43 | - [Algolia Autocomplete](https://www.algolia.com/doc/ui-libraries/autocomplete/introduction/what-is-autocomplete/) - the official Algolia Autocomplete documentation 44 | - [FlexSearch](https://github.com/nextapps-de/flexsearch) - the official FlexSearch documentation 45 | -------------------------------------------------------------------------------- /apps/docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/docs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import withMarkdoc from '@markdoc/next.js' 2 | 3 | import withSearch from './src/markdoc/search.mjs' 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | pageExtensions: ['js', 'jsx', 'md', 'ts', 'tsx'], 8 | } 9 | 10 | export default withSearch( 11 | withMarkdoc({ schemaPath: './src/markdoc' })(nextConfig), 12 | ) 13 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mikecousins-react-pdf-docs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "browserslist": "defaults, not ie <= 11", 12 | "dependencies": { 13 | "@algolia/autocomplete-core": "^1.18.1", 14 | "@headlessui/react": "^2.2.0", 15 | "@markdoc/markdoc": "^0.4.0", 16 | "@markdoc/next.js": "^0.4.0", 17 | "@sindresorhus/slugify": "^2.2.1", 18 | "@tailwindcss/postcss": "^4.0.17", 19 | "@tailwindcss/typography": "^0.5.16", 20 | "@types/node": "^22.15.3", 21 | "@types/react": "^18.3.20", 22 | "@types/react-dom": "^18.3.5", 23 | "@types/react-highlight-words": "^0.16.7", 24 | "clsx": "^2.1.1", 25 | "fast-glob": "^3.3.3", 26 | "flexsearch": "^0.8.158", 27 | "js-yaml": "^4.1.0", 28 | "next": "^14.2.26", 29 | "next-themes": "^0.2.1", 30 | "prism-react-renderer": "^2.4.1", 31 | "react": "^18.3.1", 32 | "react-dom": "^18.3.1", 33 | "react-highlight-words": "^0.20.0", 34 | "simple-functional-loader": "^1.2.1", 35 | "tailwindcss": "^4.0.17", 36 | "typescript": "^5.8.2" 37 | }, 38 | "devDependencies": { 39 | "eslint": "^8.57.1", 40 | "eslint-config-next": "^14.2.26", 41 | "prettier": "^3.5.3", 42 | "prettier-plugin-tailwindcss": "^0.6.11", 43 | "sharp": "0.33.1" 44 | }, 45 | "packageManager": "pnpm@10.7.0" 46 | } 47 | -------------------------------------------------------------------------------- /apps/docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /apps/docs/prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Options} */ 2 | module.exports = { 3 | singleQuote: true, 4 | semi: false, 5 | plugins: ['prettier-plugin-tailwindcss'], 6 | tailwindStylesheet: './src/styles/tailwind.css', 7 | } 8 | -------------------------------------------------------------------------------- /apps/docs/src/app/docs/basic-usage/page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic Usage 3 | nextjs: 4 | metadata: 5 | title: Basic Usage 6 | description: How to quickly embed a PDF. 7 | --- 8 | 9 | The library uses a headless philosophy and allows you to more easily customize the 10 | embedding of your PDFs. You can draw your own canvas and then render to it by providing it's ref to 11 | the `usePdf` hook. 12 | 13 | --- 14 | 15 | ## Basic Code 16 | 17 | Here is a basic implementation of our library. It also has pagination built-in, 18 | using simple back/next buttons. You can extend this however you want, you just need to set the page 19 | variable that's passed to the hook. 20 | 21 | ```typescript 22 | import { useState, useRef } from 'react'; 23 | import { usePdf } from '@mikecousins/react-pdf'; 24 | 25 | const MyPdfViewer = () => { 26 | const [page, setPage] = useState(1); 27 | const canvasRef = useRef(null); 28 | 29 | const { pdfDocument, pdfPage } = usePdf({ 30 | file: 'test.pdf', 31 | page, 32 | canvasRef, 33 | }); 34 | 35 | return ( 36 |
37 | {!pdfDocument && Loading...} 38 | 39 | {Boolean(pdfDocument && pdfDocument.numPages) && ( 40 | 57 | )} 58 |
59 | ); 60 | }; 61 | ``` 62 | -------------------------------------------------------------------------------- /apps/docs/src/app/docs/customization/page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Customization 3 | nextjs: 4 | metadata: 5 | title: Customization 6 | description: Take your PDFs to the next level with the rich customization of our library. 7 | --- 8 | 9 | We expose most of the inner workings of our library for easy customization. See below for all of the props you can pass in and the return values you get out of our hook. 10 | 11 | --- 12 | 13 | ## Props 14 | 15 | When you call usePdf you'll want to pass in a subset of these props, like this: 16 | 17 | ```typescript 18 | const { pdfDocument, pdfPage } = usePdf({ 19 | canvasRef, 20 | file: 'https://example.com/test.pdf', 21 | page, 22 | }); 23 | ``` 24 | 25 | ### canvasRef 26 | A reference to the canvas element. Create with: 27 | 28 | const canvasRef = useRef(null); 29 | 30 | and then render it like: 31 | 32 | ```typescript 33 | 34 | ``` 35 | 36 | and then pass it into usePdf. 37 | 38 | ### file 39 | URL of the PDF file. 40 | 41 | ### onDocumentLoadSuccess 42 | Allows you to specify a callback that is called when the PDF document data will be fully loaded. Callback is called with PDFDocumentProxy as an only argument. 43 | 44 | ### onDocumentLoadFail 45 | Allows you to specify a callback that is called after an error occurred during PDF document data loading. 46 | 47 | ### onPageLoadSuccess 48 | Allows you to specify a callback that is called when the PDF page data will be fully loaded. Callback is called with PDFPageProxy as an only argument. 49 | 50 | ### onPageLoadFail 51 | Allows you to specify a callback that is called after an error occurred during PDF page data loading. 52 | 53 | ### onPageRenderSuccess 54 | Allows you to specify a callback that is called when the PDF page will be fully rendered into the DOM. Callback is called with PDFPageProxy as an only argument. 55 | 56 | ### onPageRenderFail 57 | Allows you to specify a callback that is called after an error occurred during PDF page rendering. 58 | 59 | ### page 60 | Specify the page that you want to display. Default = 1, 61 | 62 | ### scale 63 | Allows you to scale the PDF. Default = 1. 64 | 65 | ### rotate 66 | Allows you to rotate the PDF. Number is in degrees. Default = 0. 67 | 68 | ### cMapUrl 69 | Allows you to specify a cmap url. Default = '../node_modules/pdfjs-dist/cmaps/'. 70 | 71 | ### cMapPacked 72 | Allows you to specify whether the cmaps are packed or not. Default = false. 73 | 74 | ### workerSrc 75 | Allows you to specify a custom pdf worker url. Default = '//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js'. 76 | 77 | ### withCredentials 78 | Allows you to add the withCredentials flag. Default = false. 79 | 80 | ## Returned values 81 | ### pdfDocument 82 | pdfjs's PDFDocumentProxy object. This can be undefined if the document has not been loaded yet. 83 | 84 | ### pdfPage 85 | pdfjs's PDFPageProxy object This can be undefined if the page has not been loaded yet. 86 | -------------------------------------------------------------------------------- /apps/docs/src/app/docs/examples/page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples 3 | nextjs: 4 | metadata: 5 | title: Examples 6 | description: View some of our examples in various React frameworks. 7 | --- 8 | 9 | We have several examples written in various frameworks to show you how it works! 10 | 11 | --- 12 | 13 | ## Demos 14 | 15 | Check out our examples below in a framework similar to your situation. 16 | 17 | ### React Router (Remix) 18 | 19 | Live Demo: [https://react-pdf-js-demo-react-router.vercel.app/](https://react-pdf-js-demo-react-router.vercel.app/) 20 | 21 | Code: [https://github.com/mikecousins/react-pdf-js/tree/main/apps/demo-react-router](https://github.com/mikecousins/react-pdf-js/tree/main/apps/demo-react-router) 22 | 23 | ### Vite/SPA 24 | 25 | Live Demo: [https://react-pdf-js-demo-vite.vercel.app/](https://react-pdf-js-demo-vite.vercel.app/) 26 | 27 | Code: [https://github.com/mikecousins/react-pdf-js/tree/main/apps/demo](https://github.com/mikecousins/react-pdf-js/tree/main/apps/demo) 28 | -------------------------------------------------------------------------------- /apps/docs/src/app/docs/installation/page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | nextjs: 4 | metadata: 5 | title: Installation 6 | description: Quidem magni aut exercitationem maxime rerum eos. 7 | --- 8 | 9 | Installation of @mikecousins/react-pdf is very easy. 10 | 11 | --- 12 | 13 | ## How To Install 14 | 15 | There are only three peer dependencies that our library requires: 16 | 17 | - react 18 | - react-dom 19 | - pdfjs-dist 20 | 21 | We will assume you already have react & react-dom installed and will focus on @mikecousins/react-pdf and pdfjs-dist. 22 | 23 | ### pnpm 24 | 25 | To install using the pnpm package manager: 26 | 27 | ```bash 28 | pnpm add @mikecousins/react-pdf pdfjs-dist 29 | ``` 30 | 31 | ### npm 32 | 33 | To install using the npm package manager: 34 | 35 | ```bash 36 | npm i @mikecousins/react-pdf pdfjs-dist 37 | ``` 38 | 39 | ### yarn 40 | 41 | To install using the yarn package manager: 42 | 43 | ```bash 44 | yarn add @mikecousins/react-pdf pdfjs-dist 45 | ``` 46 | 47 | --- 48 | 49 | ## Next Steps 50 | 51 | That's it! Next you'll want to choose how to use the library and for that we have two modes, [component](/docs/component-version) for basic use cases and [hook](/docs/hook-version) for more customization. 52 | 53 | -------------------------------------------------------------------------------- /apps/docs/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikecousins/react-pdf-js/911db12532691cfafa62c7d95a4f5b95c6c9704f/apps/docs/src/app/favicon.ico -------------------------------------------------------------------------------- /apps/docs/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import localFont from 'next/font/local' 4 | import clsx from 'clsx' 5 | 6 | import { Providers } from '@/app/providers' 7 | import { Layout } from '@/components/Layout' 8 | 9 | import '@/styles/tailwind.css' 10 | 11 | const inter = Inter({ 12 | subsets: ['latin'], 13 | display: 'swap', 14 | variable: '--font-inter', 15 | }) 16 | 17 | // Use local version of Lexend so that we can use OpenType features 18 | const lexend = localFont({ 19 | src: '../fonts/lexend.woff2', 20 | display: 'swap', 21 | variable: '--font-lexend', 22 | }) 23 | 24 | export const metadata: Metadata = { 25 | title: { 26 | template: '%s - Docs', 27 | default: '@mikecousins/react-pdf - Embed PDFs in React.', 28 | }, 29 | description: 30 | 'Cache every single thing your app could ever do ahead of time, so your code never even has to run at all.', 31 | } 32 | 33 | export default function RootLayout({ 34 | children, 35 | }: { 36 | children: React.ReactNode 37 | }) { 38 | return ( 39 | 44 | 50 | 51 | 52 | {children} 53 | 54 | 55 | 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /apps/docs/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |
7 |

8 | 404 9 |

10 |

11 | Page not found 12 |

13 |

14 | Sorry, we couldn’t find the page you’re looking for. 15 |

16 | 20 | Go back home 21 | 22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /apps/docs/src/app/page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | --- 4 | 5 | Learn how to get PDFs embedded in your React-based website quickly and easily. {% .lead %} 6 | 7 | {% quick-links %} 8 | 9 | {% quick-link title="Installation" icon="installation" href="/docs/installation" description="Step-by-step guides to setting up your system and installing the library." /%} 10 | 11 | {% quick-link title="Basic Usage" icon="presets" href="/docs/basic-usage" description="Learn how to use the library to quickly get going." /%} 12 | 13 | {% quick-link title="Customization" icon="theming" href="/docs/customization" description="Learn how to customize our library to get the results you want." /%} 14 | 15 | {% quick-link title="Examples" icon="lightbulb" href="/docs/examples" description="View some of our examples in various React frameworks." /%} 16 | 17 | {% /quick-links %} 18 | 19 | Select a section to get started learning about `@mikecousins/react-pdf` 20 | 21 | --- 22 | 23 | ## About 24 | 25 | This library is maintained by [Mike Cousins](https://mike.cousins.ai). 26 | 27 | ## Links 28 | 29 | ### Github 30 | 31 | ![github stars badge](https://badgen.net/github/stars/mikecousins/react-pdf-js) 32 | 33 | 34 | [https://www.github.com/mikecousins/react-pdf-js](https://www.github.com/mikecousins/react-pdf-js) 35 | 36 | 37 | ### npm 38 | 39 | ![npm version badge](https://badgen.net/npm/v/@mikecousins/react-pdf) 40 | ![npm downloads badge](https://badgen.net/npm/dm/@mikecousins/react-pdf) 41 | 42 | 43 | [https://www.npmjs.com/package/@mikecousins/react-pdf](https://www.npmjs.com/package/@mikecousins/react-pdf) 44 | 45 | ### Bundlephobia 46 | ![Bundlephobia badge](https://badgen.net/bundlephobia/minzip/@mikecousins/react-pdf) 47 | [https://bundlephobia.com/package/@mikecousins/react-pdf](https://bundlephobia.com/package/@mikecousins/react-pdf) 48 | 49 | ### Publint 50 | 51 | [https://publint.dev/@mikecousins/react-pdf](https://publint.dev/@mikecousins/react-pdf) 52 | 53 | ### Are The Types Wrong? 54 | 55 | [https://arethetypeswrong.github.io/?p=%40mikecousins%2Freact-pdf](https://arethetypeswrong.github.io/?p=%40mikecousins%2Freact-pdf) 56 | -------------------------------------------------------------------------------- /apps/docs/src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ThemeProvider } from 'next-themes' 4 | 5 | export function Providers({ children }: { children: React.ReactNode }) { 6 | return ( 7 | 8 | {children} 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /apps/docs/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import clsx from 'clsx' 3 | 4 | const variantStyles = { 5 | primary: 6 | 'rounded-full bg-sky-300 py-2 px-4 text-sm font-semibold text-slate-900 hover:bg-sky-200 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-300/50 active:bg-sky-500', 7 | secondary: 8 | 'rounded-full bg-slate-800 py-2 px-4 text-sm font-medium text-white hover:bg-slate-700 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/50 active:text-slate-400', 9 | } 10 | 11 | type ButtonProps = { 12 | variant?: keyof typeof variantStyles 13 | } & ( 14 | | React.ComponentPropsWithoutRef 15 | | (React.ComponentPropsWithoutRef<'button'> & { href?: undefined }) 16 | ) 17 | 18 | export function Button({ 19 | variant = 'primary', 20 | className, 21 | ...props 22 | }: ButtonProps) { 23 | className = clsx(variantStyles[variant], className) 24 | 25 | return typeof props.href === 'undefined' ? ( 26 | 58 | 61 | 62 | 63 | 64 |
65 |
66 | 67 |
68 |
69 | 78 | 87 |
88 |
89 |
90 |
91 |
92 |
93 | 94 |
95 | {tabs.map((tab) => ( 96 |
105 |
111 | {tab.name} 112 |
113 |
114 | ))} 115 |
116 |
117 | 130 | 135 | {({ 136 | className, 137 | style, 138 | tokens, 139 | getLineProps, 140 | getTokenProps, 141 | }) => ( 142 |
149 |                           
150 |                             {tokens.map((line, lineIndex) => (
151 |                               
152 | {line.map((token, tokenIndex) => ( 153 | 157 | ))} 158 |
159 | ))} 160 |
161 |
162 | )} 163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | ) 173 | } 174 | -------------------------------------------------------------------------------- /apps/docs/src/components/HeroBackground.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from 'react' 2 | 3 | export function HeroBackground(props: React.ComponentPropsWithoutRef<'svg'>) { 4 | let id = useId() 5 | 6 | return ( 7 | 187 | ) 188 | } 189 | -------------------------------------------------------------------------------- /apps/docs/src/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from 'react' 2 | import clsx from 'clsx' 3 | 4 | import { InstallationIcon } from '@/components/icons/InstallationIcon' 5 | import { LightbulbIcon } from '@/components/icons/LightbulbIcon' 6 | import { PluginsIcon } from '@/components/icons/PluginsIcon' 7 | import { PresetsIcon } from '@/components/icons/PresetsIcon' 8 | import { ThemingIcon } from '@/components/icons/ThemingIcon' 9 | import { WarningIcon } from '@/components/icons/WarningIcon' 10 | 11 | const icons = { 12 | installation: InstallationIcon, 13 | presets: PresetsIcon, 14 | plugins: PluginsIcon, 15 | theming: ThemingIcon, 16 | lightbulb: LightbulbIcon, 17 | warning: WarningIcon, 18 | } 19 | 20 | const iconStyles = { 21 | blue: '[--icon-foreground:var(--color-slate-900)] [--icon-background:var(--color-white)]', 22 | amber: 23 | '[--icon-foreground:var(--color-amber-900)] [--icon-background:var(--color-amber-100)]', 24 | } 25 | 26 | export function Icon({ 27 | icon, 28 | color = 'blue', 29 | className, 30 | ...props 31 | }: { 32 | color?: keyof typeof iconStyles 33 | icon: keyof typeof icons 34 | } & Omit, 'color'>) { 35 | let id = useId() 36 | let IconComponent = icons[icon] 37 | 38 | return ( 39 | 48 | ) 49 | } 50 | 51 | const gradients = { 52 | blue: [ 53 | { stopColor: '#0EA5E9' }, 54 | { stopColor: '#22D3EE', offset: '.527' }, 55 | { stopColor: '#818CF8', offset: 1 }, 56 | ], 57 | amber: [ 58 | { stopColor: '#FDE68A', offset: '.08' }, 59 | { stopColor: '#F59E0B', offset: '.837' }, 60 | ], 61 | } 62 | 63 | export function Gradient({ 64 | color = 'blue', 65 | ...props 66 | }: { 67 | color?: keyof typeof gradients 68 | } & Omit, 'color'>) { 69 | return ( 70 | 77 | {gradients[color].map((stop, stopIndex) => ( 78 | 79 | ))} 80 | 81 | ) 82 | } 83 | 84 | export function LightMode({ 85 | className, 86 | ...props 87 | }: React.ComponentPropsWithoutRef<'g'>) { 88 | return 89 | } 90 | 91 | export function DarkMode({ 92 | className, 93 | ...props 94 | }: React.ComponentPropsWithoutRef<'g'>) { 95 | return 96 | } 97 | -------------------------------------------------------------------------------- /apps/docs/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect, useState } from 'react' 4 | import Link from 'next/link' 5 | import { usePathname } from 'next/navigation' 6 | import clsx from 'clsx' 7 | 8 | import { Hero } from '@/components/Hero' 9 | import { Logo, Logomark } from '@/components/Logo' 10 | import { MobileNavigation } from '@/components/MobileNavigation' 11 | import { Navigation } from '@/components/Navigation' 12 | import { Search } from '@/components/Search' 13 | import { ThemeSelector } from '@/components/ThemeSelector' 14 | 15 | function GitHubIcon(props: React.ComponentPropsWithoutRef<'svg'>) { 16 | return ( 17 | 20 | ) 21 | } 22 | 23 | function Header() { 24 | let [isScrolled, setIsScrolled] = useState(false) 25 | 26 | useEffect(() => { 27 | function onScroll() { 28 | setIsScrolled(window.scrollY > 0) 29 | } 30 | onScroll() 31 | window.addEventListener('scroll', onScroll, { passive: true }) 32 | return () => { 33 | window.removeEventListener('scroll', onScroll) 34 | } 35 | }, []) 36 | 37 | return ( 38 |
46 |
47 | 48 |
49 |
50 | 51 | 52 | 53 | 54 |
55 |
56 | 57 |
58 |
59 | 60 | 61 | 62 | 63 |
64 |
65 | ) 66 | } 67 | 68 | export function Layout({ children }: { children: React.ReactNode }) { 69 | let pathname = usePathname() 70 | let isHomePage = pathname === '/' 71 | 72 | return ( 73 |
74 |
75 | 76 | {isHomePage && } 77 | 78 |
79 |
80 |
81 |
82 |
83 |
84 | 85 |
86 |
87 | {children} 88 |
89 |
90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /apps/docs/src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | function LogomarkPaths() { 2 | return ( 3 | 4 | 5 | 6 | 7 | ) 8 | } 9 | 10 | export function Logomark(props: React.ComponentPropsWithoutRef<'svg'>) { 11 | return ( 12 | 15 | ) 16 | } 17 | 18 | export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) { 19 | return ( 20 |
21 | 24 |
@mikecousins/react-pdf
25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /apps/docs/src/components/MobileNavigation.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Suspense, useCallback, useEffect, useState } from 'react' 4 | import Link from 'next/link' 5 | import { usePathname, useSearchParams } from 'next/navigation' 6 | import { Dialog, DialogPanel } from '@headlessui/react' 7 | 8 | import { Logomark } from '@/components/Logo' 9 | import { Navigation } from '@/components/Navigation' 10 | 11 | function MenuIcon(props: React.ComponentPropsWithoutRef<'svg'>) { 12 | return ( 13 | 23 | ) 24 | } 25 | 26 | function CloseIcon(props: React.ComponentPropsWithoutRef<'svg'>) { 27 | return ( 28 | 38 | ) 39 | } 40 | 41 | function CloseOnNavigation({ close }: { close: () => void }) { 42 | let pathname = usePathname() 43 | let searchParams = useSearchParams() 44 | 45 | useEffect(() => { 46 | close() 47 | }, [pathname, searchParams, close]) 48 | 49 | return null 50 | } 51 | 52 | export function MobileNavigation() { 53 | let [isOpen, setIsOpen] = useState(false) 54 | let close = useCallback(() => setIsOpen(false), [setIsOpen]) 55 | 56 | function onLinkClick(event: React.MouseEvent) { 57 | let link = event.currentTarget 58 | if ( 59 | link.pathname + link.search + link.hash === 60 | window.location.pathname + window.location.search + window.location.hash 61 | ) { 62 | close() 63 | } 64 | } 65 | 66 | return ( 67 | <> 68 | 76 | 77 | 78 | 79 | close()} 82 | className="fixed inset-0 z-50 flex items-start overflow-y-auto bg-slate-900/50 pr-10 backdrop-blur-sm lg:hidden" 83 | aria-label="Navigation" 84 | > 85 | 86 |
87 | 94 | 95 | 96 | 97 |
98 | 99 |
100 |
101 | 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /apps/docs/src/components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { usePathname } from 'next/navigation' 3 | import clsx from 'clsx' 4 | 5 | import { navigation } from '@/lib/navigation' 6 | 7 | export function Navigation({ 8 | className, 9 | onLinkClick, 10 | }: { 11 | className?: string 12 | onLinkClick?: React.MouseEventHandler 13 | }) { 14 | let pathname = usePathname() 15 | 16 | return ( 17 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /apps/docs/src/components/PrevNextLinks.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Link from 'next/link' 4 | import { usePathname } from 'next/navigation' 5 | import clsx from 'clsx' 6 | 7 | import { navigation } from '@/lib/navigation' 8 | 9 | function ArrowIcon(props: React.ComponentPropsWithoutRef<'svg'>) { 10 | return ( 11 | 14 | ) 15 | } 16 | 17 | function PageLink({ 18 | title, 19 | href, 20 | dir = 'next', 21 | ...props 22 | }: Omit, 'dir' | 'title'> & { 23 | title: string 24 | href: string 25 | dir?: 'previous' | 'next' 26 | }) { 27 | return ( 28 |
29 |
30 | {dir === 'next' ? 'Next' : 'Previous'} 31 |
32 |
33 | 40 | {title} 41 | 47 | 48 |
49 |
50 | ) 51 | } 52 | 53 | export function PrevNextLinks() { 54 | let pathname = usePathname() 55 | let allLinks = navigation.flatMap((section) => section.links) 56 | let linkIndex = allLinks.findIndex((link) => link.href === pathname) 57 | let previousPage = linkIndex > -1 ? allLinks[linkIndex - 1] : null 58 | let nextPage = linkIndex > -1 ? allLinks[linkIndex + 1] : null 59 | 60 | if (!nextPage && !previousPage) { 61 | return null 62 | } 63 | 64 | return ( 65 |
66 | {previousPage && } 67 | {nextPage && } 68 |
69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /apps/docs/src/components/Prose.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | 3 | export function Prose({ 4 | as, 5 | className, 6 | ...props 7 | }: React.ComponentPropsWithoutRef & { 8 | as?: T 9 | }) { 10 | let Component = as ?? 'div' 11 | 12 | return ( 13 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /apps/docs/src/components/QuickLinks.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import { Icon } from '@/components/Icon' 4 | 5 | export function QuickLinks({ children }: { children: React.ReactNode }) { 6 | return ( 7 |
8 | {children} 9 |
10 | ) 11 | } 12 | 13 | export function QuickLink({ 14 | title, 15 | description, 16 | href, 17 | icon, 18 | }: { 19 | title: string 20 | description: string 21 | href: string 22 | icon: React.ComponentProps['icon'] 23 | }) { 24 | return ( 25 |
26 |
27 |
28 | 29 |

30 | 31 | 32 | {title} 33 | 34 |

35 |

36 | {description} 37 |

38 |
39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /apps/docs/src/components/Search.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | forwardRef, 5 | Fragment, 6 | Suspense, 7 | useCallback, 8 | useEffect, 9 | useId, 10 | useRef, 11 | useState, 12 | } from 'react' 13 | import Highlighter from 'react-highlight-words' 14 | import { usePathname, useRouter, useSearchParams } from 'next/navigation' 15 | import { 16 | type AutocompleteApi, 17 | type AutocompleteCollection, 18 | type AutocompleteState, 19 | createAutocomplete, 20 | } from '@algolia/autocomplete-core' 21 | import { Dialog, DialogPanel } from '@headlessui/react' 22 | import clsx from 'clsx' 23 | 24 | import { navigation } from '@/lib/navigation' 25 | import { type Result } from '@/markdoc/search.mjs' 26 | 27 | type EmptyObject = Record 28 | 29 | type Autocomplete = AutocompleteApi< 30 | Result, 31 | React.SyntheticEvent, 32 | React.MouseEvent, 33 | React.KeyboardEvent 34 | > 35 | 36 | function SearchIcon(props: React.ComponentPropsWithoutRef<'svg'>) { 37 | return ( 38 | 41 | ) 42 | } 43 | 44 | function useAutocomplete({ 45 | close, 46 | }: { 47 | close: (autocomplete: Autocomplete) => void 48 | }) { 49 | let id = useId() 50 | let router = useRouter() 51 | let [autocompleteState, setAutocompleteState] = useState< 52 | AutocompleteState | EmptyObject 53 | >({}) 54 | 55 | function navigate({ itemUrl }: { itemUrl?: string }) { 56 | if (!itemUrl) { 57 | return 58 | } 59 | 60 | router.push(itemUrl) 61 | 62 | if ( 63 | itemUrl === 64 | window.location.pathname + window.location.search + window.location.hash 65 | ) { 66 | close(autocomplete) 67 | } 68 | } 69 | 70 | let [autocomplete] = useState(() => 71 | createAutocomplete< 72 | Result, 73 | React.SyntheticEvent, 74 | React.MouseEvent, 75 | React.KeyboardEvent 76 | >({ 77 | id, 78 | placeholder: 'Find something...', 79 | defaultActiveItemId: 0, 80 | onStateChange({ state }) { 81 | setAutocompleteState(state) 82 | }, 83 | shouldPanelOpen({ state }) { 84 | return state.query !== '' 85 | }, 86 | navigator: { 87 | navigate, 88 | }, 89 | getSources({ query }) { 90 | return import('@/markdoc/search.mjs').then(({ search }) => { 91 | return [ 92 | { 93 | sourceId: 'documentation', 94 | getItems() { 95 | return search(query, { limit: 5 }) 96 | }, 97 | getItemUrl({ item }) { 98 | return item.url 99 | }, 100 | onSelect: navigate, 101 | }, 102 | ] 103 | }) 104 | }, 105 | }), 106 | ) 107 | 108 | return { autocomplete, autocompleteState } 109 | } 110 | 111 | function LoadingIcon(props: React.ComponentPropsWithoutRef<'svg'>) { 112 | let id = useId() 113 | 114 | return ( 115 | 137 | ) 138 | } 139 | 140 | function HighlightQuery({ text, query }: { text: string; query: string }) { 141 | return ( 142 | 148 | ) 149 | } 150 | 151 | function SearchResult({ 152 | result, 153 | autocomplete, 154 | collection, 155 | query, 156 | }: { 157 | result: Result 158 | autocomplete: Autocomplete 159 | collection: AutocompleteCollection 160 | query: string 161 | }) { 162 | let id = useId() 163 | 164 | let sectionTitle = navigation.find((section) => 165 | section.links.find((link) => link.href === result.url.split('#')[0]), 166 | )?.title 167 | let hierarchy = [sectionTitle, result.pageTitle].filter( 168 | (x): x is string => typeof x === 'string', 169 | ) 170 | 171 | return ( 172 |
  • 180 | 187 | {hierarchy.length > 0 && ( 188 | 208 | )} 209 |
  • 210 | ) 211 | } 212 | 213 | function SearchResults({ 214 | autocomplete, 215 | query, 216 | collection, 217 | }: { 218 | autocomplete: Autocomplete 219 | query: string 220 | collection: AutocompleteCollection 221 | }) { 222 | if (collection.items.length === 0) { 223 | return ( 224 |

    225 | No results for “ 226 | 227 | {query} 228 | 229 | ” 230 |

    231 | ) 232 | } 233 | 234 | return ( 235 |
      236 | {collection.items.map((result) => ( 237 | 244 | ))} 245 |
    246 | ) 247 | } 248 | 249 | const SearchInput = forwardRef< 250 | React.ElementRef<'input'>, 251 | { 252 | autocomplete: Autocomplete 253 | autocompleteState: AutocompleteState | EmptyObject 254 | onClose: () => void 255 | } 256 | >(function SearchInput({ autocomplete, autocompleteState, onClose }, inputRef) { 257 | let inputProps = autocomplete.getInputProps({ inputElement: null }) 258 | 259 | return ( 260 |
    261 | 262 | { 271 | if ( 272 | event.key === 'Escape' && 273 | !autocompleteState.isOpen && 274 | autocompleteState.query === '' 275 | ) { 276 | // In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the 277 | // bottom of the page. This is a workaround for that until we can figure out a proper fix in Headless UI. 278 | if (document.activeElement instanceof HTMLElement) { 279 | document.activeElement.blur() 280 | } 281 | 282 | onClose() 283 | } else { 284 | inputProps.onKeyDown(event) 285 | } 286 | }} 287 | /> 288 | {autocompleteState.status === 'stalled' && ( 289 |
    290 | 291 |
    292 | )} 293 |
    294 | ) 295 | }) 296 | 297 | function CloseOnNavigation({ 298 | close, 299 | autocomplete, 300 | }: { 301 | close: (autocomplete: Autocomplete) => void 302 | autocomplete: Autocomplete 303 | }) { 304 | let pathname = usePathname() 305 | let searchParams = useSearchParams() 306 | 307 | useEffect(() => { 308 | close(autocomplete) 309 | }, [pathname, searchParams, close, autocomplete]) 310 | 311 | return null 312 | } 313 | 314 | function SearchDialog({ 315 | open, 316 | setOpen, 317 | className, 318 | }: { 319 | open: boolean 320 | setOpen: (open: boolean) => void 321 | className?: string 322 | }) { 323 | let formRef = useRef>(null) 324 | let panelRef = useRef>(null) 325 | let inputRef = useRef>(null) 326 | 327 | let close = useCallback( 328 | (autocomplete: Autocomplete) => { 329 | setOpen(false) 330 | autocomplete.setQuery('') 331 | }, 332 | [setOpen], 333 | ) 334 | 335 | let { autocomplete, autocompleteState } = useAutocomplete({ 336 | close() { 337 | close(autocomplete) 338 | }, 339 | }) 340 | 341 | useEffect(() => { 342 | if (open) { 343 | return 344 | } 345 | 346 | function onKeyDown(event: KeyboardEvent) { 347 | if (event.key === 'k' && (event.metaKey || event.ctrlKey)) { 348 | event.preventDefault() 349 | setOpen(true) 350 | } 351 | } 352 | 353 | window.addEventListener('keydown', onKeyDown) 354 | 355 | return () => { 356 | window.removeEventListener('keydown', onKeyDown) 357 | } 358 | }, [open, setOpen]) 359 | 360 | return ( 361 | <> 362 | 363 | 364 | 365 | close(autocomplete)} 368 | className={clsx('fixed inset-0 z-50', className)} 369 | > 370 |
    371 | 372 |
    373 | 374 |
    375 |
    381 | setOpen(false)} 386 | /> 387 |
    392 | {autocompleteState.isOpen && ( 393 | 398 | )} 399 |
    400 | 401 |
    402 |
    403 |
    404 |
    405 | 406 | ) 407 | } 408 | 409 | function useSearchProps() { 410 | let buttonRef = useRef>(null) 411 | let [open, setOpen] = useState(false) 412 | 413 | return { 414 | buttonProps: { 415 | ref: buttonRef, 416 | onClick() { 417 | setOpen(true) 418 | }, 419 | }, 420 | dialogProps: { 421 | open, 422 | setOpen: useCallback((open: boolean) => { 423 | let { width = 0, height = 0 } = 424 | buttonRef.current?.getBoundingClientRect() ?? {} 425 | if (!open || (width !== 0 && height !== 0)) { 426 | setOpen(open) 427 | } 428 | }, []), 429 | }, 430 | } 431 | } 432 | 433 | export function Search() { 434 | let [modifierKey, setModifierKey] = useState() 435 | let { buttonProps, dialogProps } = useSearchProps() 436 | 437 | useEffect(() => { 438 | setModifierKey( 439 | /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl ', 440 | ) 441 | }, []) 442 | 443 | return ( 444 | <> 445 | 461 | 462 | 463 | ) 464 | } 465 | -------------------------------------------------------------------------------- /apps/docs/src/components/TableOfContents.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useCallback, useEffect, useState } from 'react' 4 | import Link from 'next/link' 5 | import clsx from 'clsx' 6 | 7 | import { type Section, type Subsection } from '@/lib/sections' 8 | 9 | export function TableOfContents({ 10 | tableOfContents, 11 | }: { 12 | tableOfContents: Array
    13 | }) { 14 | let [currentSection, setCurrentSection] = useState(tableOfContents[0]?.id) 15 | 16 | let getHeadings = useCallback((tableOfContents: Array
    ) => { 17 | return tableOfContents 18 | .flatMap((node) => [node.id, ...node.children.map((child) => child.id)]) 19 | .map((id) => { 20 | let el = document.getElementById(id) 21 | if (!el) return null 22 | 23 | let style = window.getComputedStyle(el) 24 | let scrollMt = parseFloat(style.scrollMarginTop) 25 | 26 | let top = window.scrollY + el.getBoundingClientRect().top - scrollMt 27 | return { id, top } 28 | }) 29 | .filter((x): x is { id: string; top: number } => x !== null) 30 | }, []) 31 | 32 | useEffect(() => { 33 | if (tableOfContents.length === 0) return 34 | let headings = getHeadings(tableOfContents) 35 | function onScroll() { 36 | let top = window.scrollY 37 | let current = headings[0].id 38 | for (let heading of headings) { 39 | if (top >= heading.top - 10) { 40 | current = heading.id 41 | } else { 42 | break 43 | } 44 | } 45 | setCurrentSection(current) 46 | } 47 | window.addEventListener('scroll', onScroll, { passive: true }) 48 | onScroll() 49 | return () => { 50 | window.removeEventListener('scroll', onScroll) 51 | } 52 | }, [getHeadings, tableOfContents]) 53 | 54 | function isActive(section: Section | Subsection) { 55 | if (section.id === currentSection) { 56 | return true 57 | } 58 | if (!section.children) { 59 | return false 60 | } 61 | return section.children.findIndex(isActive) > -1 62 | } 63 | 64 | return ( 65 |
    66 | 117 |
    118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /apps/docs/src/components/ThemeSelector.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { useTheme } from 'next-themes' 3 | import { 4 | Label, 5 | Listbox, 6 | ListboxButton, 7 | ListboxOption, 8 | ListboxOptions, 9 | } from '@headlessui/react' 10 | import clsx from 'clsx' 11 | 12 | const themes = [ 13 | { name: 'Light', value: 'light', icon: LightIcon }, 14 | { name: 'Dark', value: 'dark', icon: DarkIcon }, 15 | { name: 'System', value: 'system', icon: SystemIcon }, 16 | ] 17 | 18 | function LightIcon(props: React.ComponentPropsWithoutRef<'svg'>) { 19 | return ( 20 | 27 | ) 28 | } 29 | 30 | function DarkIcon(props: React.ComponentPropsWithoutRef<'svg'>) { 31 | return ( 32 | 39 | ) 40 | } 41 | 42 | function SystemIcon(props: React.ComponentPropsWithoutRef<'svg'>) { 43 | return ( 44 | 51 | ) 52 | } 53 | 54 | export function ThemeSelector( 55 | props: React.ComponentPropsWithoutRef>, 56 | ) { 57 | let { theme, setTheme } = useTheme() 58 | let [mounted, setMounted] = useState(false) 59 | 60 | useEffect(() => { 61 | setMounted(true) 62 | }, []) 63 | 64 | if (!mounted) { 65 | return
    66 | } 67 | 68 | return ( 69 | 70 | 71 | 75 | 81 | 87 | 88 | 89 | {themes.map((theme) => ( 90 | 94 | clsx( 95 | 'flex cursor-pointer items-center rounded-[0.625rem] p-1 select-none', 96 | { 97 | 'text-sky-500': selected, 98 | 'text-slate-900 dark:text-white': focus && !selected, 99 | 'text-slate-700 dark:text-slate-400': !focus && !selected, 100 | 'bg-slate-100 dark:bg-slate-900/40': focus, 101 | }, 102 | ) 103 | } 104 | > 105 | {({ selected }) => ( 106 | <> 107 |
    108 | 116 |
    117 |
    {theme.name}
    118 | 119 | )} 120 |
    121 | ))} 122 |
    123 |
    124 | ) 125 | } 126 | -------------------------------------------------------------------------------- /apps/docs/src/components/icons/InstallationIcon.tsx: -------------------------------------------------------------------------------- 1 | import { DarkMode, Gradient, LightMode } from '@/components/Icon' 2 | 3 | export function InstallationIcon({ 4 | id, 5 | color, 6 | }: { 7 | id: string 8 | color?: React.ComponentProps['color'] 9 | }) { 10 | return ( 11 | <> 12 | 13 | 18 | 23 | 24 | 25 | 26 | 34 | 35 | 36 | 44 | 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /apps/docs/src/components/icons/LightbulbIcon.tsx: -------------------------------------------------------------------------------- 1 | import { DarkMode, Gradient, LightMode } from '@/components/Icon' 2 | 3 | export function LightbulbIcon({ 4 | id, 5 | color, 6 | }: { 7 | id: string 8 | color?: React.ComponentProps['color'] 9 | }) { 10 | return ( 11 | <> 12 | 13 | 18 | 23 | 24 | 25 | 26 | 33 | 37 | 41 | 42 | 43 | 49 | 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /apps/docs/src/components/icons/PluginsIcon.tsx: -------------------------------------------------------------------------------- 1 | import { DarkMode, Gradient, LightMode } from '@/components/Icon' 2 | 3 | export function PluginsIcon({ 4 | id, 5 | color, 6 | }: { 7 | id: string 8 | color?: React.ComponentProps['color'] 9 | }) { 10 | return ( 11 | <> 12 | 13 | 18 | 23 | 28 | 29 | 30 | 31 | 38 | 39 | 40 | 41 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 61 | 66 | 67 | 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /apps/docs/src/components/icons/PresetsIcon.tsx: -------------------------------------------------------------------------------- 1 | import { DarkMode, Gradient, LightMode } from '@/components/Icon' 2 | 3 | export function PresetsIcon({ 4 | id, 5 | color, 6 | }: { 7 | id: string 8 | color?: React.ComponentProps['color'] 9 | }) { 10 | return ( 11 | <> 12 | 13 | 18 | 23 | 24 | 25 | 26 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /apps/docs/src/components/icons/ThemingIcon.tsx: -------------------------------------------------------------------------------- 1 | import { DarkMode, Gradient, LightMode } from '@/components/Icon' 2 | 3 | export function ThemingIcon({ 4 | id, 5 | color, 6 | }: { 7 | id: string 8 | color?: React.ComponentProps['color'] 9 | }) { 10 | return ( 11 | <> 12 | 13 | 18 | 23 | 24 | 25 | 26 | 34 | 39 | 46 | 54 | 55 | 56 | 62 | 63 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /apps/docs/src/components/icons/WarningIcon.tsx: -------------------------------------------------------------------------------- 1 | import { DarkMode, Gradient, LightMode } from '@/components/Icon' 2 | 3 | export function WarningIcon({ 4 | id, 5 | color, 6 | }: { 7 | id: string 8 | color?: React.ComponentProps['color'] 9 | }) { 10 | return ( 11 | <> 12 | 13 | 18 | 23 | 24 | 25 | 26 | 34 | 41 | 50 | 51 | 52 | 58 | 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /apps/docs/src/fonts/lexend.txt: -------------------------------------------------------------------------------- 1 | Copyright 2018 The Lexend Project Authors (https://github.com/googlefonts/lexend), with Reserved Font Name “RevReading Lexend”. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /apps/docs/src/fonts/lexend.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikecousins/react-pdf-js/911db12532691cfafa62c7d95a4f5b95c6c9704f/apps/docs/src/fonts/lexend.woff2 -------------------------------------------------------------------------------- /apps/docs/src/images/blur-cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikecousins/react-pdf-js/911db12532691cfafa62c7d95a4f5b95c6c9704f/apps/docs/src/images/blur-cyan.png -------------------------------------------------------------------------------- /apps/docs/src/images/blur-indigo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikecousins/react-pdf-js/911db12532691cfafa62c7d95a4f5b95c6c9704f/apps/docs/src/images/blur-indigo.png -------------------------------------------------------------------------------- /apps/docs/src/lib/navigation.ts: -------------------------------------------------------------------------------- 1 | export const navigation = [ 2 | { 3 | title: 'Introduction', 4 | links: [ 5 | { title: 'Getting started', href: '/' }, 6 | { title: 'Installation', href: '/docs/installation' }, 7 | ], 8 | }, 9 | { 10 | title: 'Usage', 11 | links: [ 12 | { title: 'Basic Usage', href: '/docs/basic-usage' }, 13 | { title: 'Customization', href: '/docs/customization' }, 14 | { title: 'Examples', href: '/docs/examples' }, 15 | ], 16 | }, 17 | ] 18 | -------------------------------------------------------------------------------- /apps/docs/src/lib/sections.ts: -------------------------------------------------------------------------------- 1 | import { type Node } from '@markdoc/markdoc' 2 | import { slugifyWithCounter } from '@sindresorhus/slugify' 3 | 4 | interface HeadingNode extends Node { 5 | type: 'heading' 6 | attributes: { 7 | level: 1 | 2 | 3 | 4 | 5 | 6 8 | id?: string 9 | [key: string]: unknown 10 | } 11 | } 12 | 13 | type H2Node = HeadingNode & { 14 | attributes: { 15 | level: 2 16 | } 17 | } 18 | 19 | type H3Node = HeadingNode & { 20 | attributes: { 21 | level: 3 22 | } 23 | } 24 | 25 | function isHeadingNode(node: Node): node is HeadingNode { 26 | return ( 27 | node.type === 'heading' && 28 | [1, 2, 3, 4, 5, 6].includes(node.attributes.level) && 29 | (typeof node.attributes.id === 'string' || 30 | typeof node.attributes.id === 'undefined') 31 | ) 32 | } 33 | 34 | function isH2Node(node: Node): node is H2Node { 35 | return isHeadingNode(node) && node.attributes.level === 2 36 | } 37 | 38 | function isH3Node(node: Node): node is H3Node { 39 | return isHeadingNode(node) && node.attributes.level === 3 40 | } 41 | 42 | function getNodeText(node: Node) { 43 | let text = '' 44 | for (let child of node.children ?? []) { 45 | if (child.type === 'text') { 46 | text += child.attributes.content 47 | } 48 | text += getNodeText(child) 49 | } 50 | return text 51 | } 52 | 53 | export type Subsection = H3Node['attributes'] & { 54 | id: string 55 | title: string 56 | children?: undefined 57 | } 58 | 59 | export type Section = H2Node['attributes'] & { 60 | id: string 61 | title: string 62 | children: Array 63 | } 64 | 65 | export function collectSections( 66 | nodes: Array, 67 | slugify = slugifyWithCounter(), 68 | ) { 69 | let sections: Array
    = [] 70 | 71 | for (let node of nodes) { 72 | if (isH2Node(node) || isH3Node(node)) { 73 | let title = getNodeText(node) 74 | if (title) { 75 | let id = slugify(title) 76 | if (isH3Node(node)) { 77 | if (!sections[sections.length - 1]) { 78 | throw new Error( 79 | 'Cannot add `h3` to table of contents without a preceding `h2`', 80 | ) 81 | } 82 | sections[sections.length - 1].children.push({ 83 | ...node.attributes, 84 | id, 85 | title, 86 | }) 87 | } else { 88 | sections.push({ ...node.attributes, id, title, children: [] }) 89 | } 90 | } 91 | } 92 | 93 | sections.push(...collectSections(node.children ?? [], slugify)) 94 | } 95 | 96 | return sections 97 | } 98 | -------------------------------------------------------------------------------- /apps/docs/src/markdoc/nodes.js: -------------------------------------------------------------------------------- 1 | import { nodes as defaultNodes, Tag } from '@markdoc/markdoc' 2 | import { slugifyWithCounter } from '@sindresorhus/slugify' 3 | import yaml from 'js-yaml' 4 | 5 | import { DocsLayout } from '@/components/DocsLayout' 6 | import { Fence } from '@/components/Fence' 7 | 8 | let documentSlugifyMap = new Map() 9 | 10 | const nodes = { 11 | document: { 12 | ...defaultNodes.document, 13 | render: DocsLayout, 14 | transform(node, config) { 15 | documentSlugifyMap.set(config, slugifyWithCounter()) 16 | 17 | return new Tag( 18 | this.render, 19 | { 20 | frontmatter: yaml.load(node.attributes.frontmatter), 21 | nodes: node.children, 22 | }, 23 | node.transformChildren(config), 24 | ) 25 | }, 26 | }, 27 | heading: { 28 | ...defaultNodes.heading, 29 | transform(node, config) { 30 | let slugify = documentSlugifyMap.get(config) 31 | let attributes = node.transformAttributes(config) 32 | let children = node.transformChildren(config) 33 | let text = children.filter((child) => typeof child === 'string').join(' ') 34 | let id = attributes.id ?? slugify(text) 35 | 36 | return new Tag( 37 | `h${node.attributes.level}`, 38 | { ...attributes, id }, 39 | children, 40 | ) 41 | }, 42 | }, 43 | th: { 44 | ...defaultNodes.th, 45 | attributes: { 46 | ...defaultNodes.th.attributes, 47 | scope: { 48 | type: String, 49 | default: 'col', 50 | }, 51 | }, 52 | }, 53 | fence: { 54 | render: Fence, 55 | attributes: { 56 | language: { 57 | type: String, 58 | }, 59 | }, 60 | }, 61 | } 62 | 63 | export default nodes 64 | -------------------------------------------------------------------------------- /apps/docs/src/markdoc/search.mjs: -------------------------------------------------------------------------------- 1 | import Markdoc from '@markdoc/markdoc' 2 | import { slugifyWithCounter } from '@sindresorhus/slugify' 3 | import glob from 'fast-glob' 4 | import * as fs from 'fs' 5 | import * as path from 'path' 6 | import { createLoader } from 'simple-functional-loader' 7 | import * as url from 'url' 8 | 9 | const __filename = url.fileURLToPath(import.meta.url) 10 | const slugify = slugifyWithCounter() 11 | 12 | function toString(node) { 13 | let str = 14 | node.type === 'text' && typeof node.attributes?.content === 'string' 15 | ? node.attributes.content 16 | : '' 17 | if ('children' in node) { 18 | for (let child of node.children) { 19 | str += toString(child) 20 | } 21 | } 22 | return str 23 | } 24 | 25 | function extractSections(node, sections, isRoot = true) { 26 | if (isRoot) { 27 | slugify.reset() 28 | } 29 | if (node.type === 'heading' || node.type === 'paragraph') { 30 | let content = toString(node).trim() 31 | if (node.type === 'heading' && node.attributes.level <= 2) { 32 | let hash = node.attributes?.id ?? slugify(content) 33 | sections.push([content, hash, []]) 34 | } else { 35 | sections.at(-1)[2].push(content) 36 | } 37 | } else if ('children' in node) { 38 | for (let child of node.children) { 39 | extractSections(child, sections, false) 40 | } 41 | } 42 | } 43 | 44 | export default function withSearch(nextConfig = {}) { 45 | let cache = new Map() 46 | 47 | return Object.assign({}, nextConfig, { 48 | webpack(config, options) { 49 | config.module.rules.push({ 50 | test: __filename, 51 | use: [ 52 | createLoader(function () { 53 | let pagesDir = path.resolve('./src/app') 54 | this.addContextDependency(pagesDir) 55 | 56 | let files = glob.sync('**/page.md', { cwd: pagesDir }) 57 | let data = files.map((file) => { 58 | let url = 59 | file === 'page.md' ? '/' : `/${file.replace(/\/page\.md$/, '')}` 60 | let md = fs.readFileSync(path.join(pagesDir, file), 'utf8') 61 | 62 | let sections 63 | 64 | if (cache.get(file)?.[0] === md) { 65 | sections = cache.get(file)[1] 66 | } else { 67 | let ast = Markdoc.parse(md) 68 | let title = 69 | ast.attributes?.frontmatter?.match( 70 | /^title:\s*(.*?)\s*$/m, 71 | )?.[1] 72 | sections = [[title, null, []]] 73 | extractSections(ast, sections) 74 | cache.set(file, [md, sections]) 75 | } 76 | 77 | return { url, sections } 78 | }) 79 | 80 | // When this file is imported within the application 81 | // the following module is loaded: 82 | return ` 83 | import FlexSearch from 'flexsearch' 84 | 85 | let sectionIndex = new FlexSearch.Document({ 86 | tokenize: 'full', 87 | document: { 88 | id: 'url', 89 | index: 'content', 90 | store: ['title', 'pageTitle'], 91 | }, 92 | context: { 93 | resolution: 9, 94 | depth: 2, 95 | bidirectional: true 96 | } 97 | }) 98 | 99 | let data = ${JSON.stringify(data)} 100 | 101 | for (let { url, sections } of data) { 102 | for (let [title, hash, content] of sections) { 103 | sectionIndex.add({ 104 | url: url + (hash ? ('#' + hash) : ''), 105 | title, 106 | content: [title, ...content].join('\\n'), 107 | pageTitle: hash ? sections[0][0] : undefined, 108 | }) 109 | } 110 | } 111 | 112 | export function search(query, options = {}) { 113 | let result = sectionIndex.search(query, { 114 | ...options, 115 | enrich: true, 116 | }) 117 | if (result.length === 0) { 118 | return [] 119 | } 120 | return result[0].result.map((item) => ({ 121 | url: item.id, 122 | title: item.doc.title, 123 | pageTitle: item.doc.pageTitle, 124 | })) 125 | } 126 | ` 127 | }), 128 | ], 129 | }) 130 | 131 | if (typeof nextConfig.webpack === 'function') { 132 | return nextConfig.webpack(config, options) 133 | } 134 | 135 | return config 136 | }, 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /apps/docs/src/markdoc/tags.js: -------------------------------------------------------------------------------- 1 | import { Callout } from '@/components/Callout' 2 | import { QuickLink, QuickLinks } from '@/components/QuickLinks' 3 | 4 | const tags = { 5 | callout: { 6 | attributes: { 7 | title: { type: String }, 8 | type: { 9 | type: String, 10 | default: 'note', 11 | matches: ['note', 'warning'], 12 | errorLevel: 'critical', 13 | }, 14 | }, 15 | render: Callout, 16 | }, 17 | figure: { 18 | selfClosing: true, 19 | attributes: { 20 | src: { type: String }, 21 | alt: { type: String }, 22 | caption: { type: String }, 23 | }, 24 | render: ({ src, alt = '', caption }) => ( 25 |
    26 | {/* eslint-disable-next-line @next/next/no-img-element */} 27 | {alt} 28 |
    {caption}
    29 |
    30 | ), 31 | }, 32 | 'quick-links': { 33 | render: QuickLinks, 34 | }, 35 | 'quick-link': { 36 | selfClosing: true, 37 | render: QuickLink, 38 | attributes: { 39 | title: { type: String }, 40 | description: { type: String }, 41 | icon: { type: String }, 42 | href: { type: String }, 43 | }, 44 | }, 45 | } 46 | 47 | export default tags 48 | -------------------------------------------------------------------------------- /apps/docs/src/styles/prism.css: -------------------------------------------------------------------------------- 1 | pre[class*='language-'] { 2 | color: var(--color-slate-50); 3 | } 4 | 5 | .token.tag, 6 | .token.class-name, 7 | .token.selector, 8 | .token.selector .class, 9 | .token.selector.class, 10 | .token.function { 11 | color: var(--color-pink-400); 12 | } 13 | 14 | .token.attr-name, 15 | .token.keyword, 16 | .token.rule, 17 | .token.pseudo-class, 18 | .token.important { 19 | color: var(--color-slate-300); 20 | } 21 | 22 | .token.module { 23 | color: var(--color-pink-400); 24 | } 25 | 26 | .token.attr-value, 27 | .token.class, 28 | .token.string, 29 | .token.property { 30 | color: var(--color-sky-300); 31 | } 32 | 33 | .token.punctuation, 34 | .token.attr-equals { 35 | color: var(--color-slate-500); 36 | } 37 | 38 | .token.unit, 39 | .language-css .token.function { 40 | color: var(--color-teal-200); 41 | } 42 | 43 | .token.comment, 44 | .token.operator, 45 | .token.combinator { 46 | color: var(--color-slate-400); 47 | } 48 | -------------------------------------------------------------------------------- /apps/docs/src/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import './prism.css'; 3 | 4 | @plugin '@tailwindcss/typography'; 5 | 6 | @custom-variant dark (&:where(.dark, .dark *)); 7 | 8 | @theme { 9 | --text-*: initial; 10 | --text-xs: 0.75rem; 11 | --text-xs--line-height: 1rem; 12 | --text-sm: 0.875rem; 13 | --text-sm--line-height: 1.5rem; 14 | --text-base: 1rem; 15 | --text-base--line-height: 2rem; 16 | --text-lg: 1.125rem; 17 | --text-lg--line-height: 1.75rem; 18 | --text-xl: 1.25rem; 19 | --text-xl--line-height: 2rem; 20 | --text-2xl: 1.5rem; 21 | --text-2xl--line-height: 2.5rem; 22 | --text-3xl: 2rem; 23 | --text-3xl--line-height: 2.5rem; 24 | --text-4xl: 2.5rem; 25 | --text-4xl--line-height: 3rem; 26 | --text-5xl: 3rem; 27 | --text-5xl--line-height: 3.5rem; 28 | --text-6xl: 3.75rem; 29 | --text-6xl--line-height: 1; 30 | --text-7xl: 4.5rem; 31 | --text-7xl--line-height: 1; 32 | --text-8xl: 6rem; 33 | --text-8xl--line-height: 1; 34 | --text-9xl: 8rem; 35 | --text-9xl--line-height: 1; 36 | 37 | --font-sans: var(--font-inter); 38 | --font-display: var(--font-lexend); 39 | --font-display--font-feature-settings: 'ss01'; 40 | 41 | --container-8xl: 88rem; 42 | } 43 | 44 | @layer base { 45 | [inert] ::-webkit-scrollbar { 46 | display: none; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /apps/docs/types.d.ts: -------------------------------------------------------------------------------- 1 | import { type SearchOptions } from 'flexsearch' 2 | 3 | declare module '@/markdoc/search.mjs' { 4 | export type Result = { 5 | url: string 6 | title: string 7 | pageTitle?: string 8 | } 9 | 10 | export function search(query: string, options?: SearchOptions): Array 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pdf-js-mono", 3 | "version": "5.5.1", 4 | "description": "Simple React component to wrap up PDF.js. The easiest way to render PDFs in your React app.", 5 | "author": "mikecousins", 6 | "license": "MIT", 7 | "repository": "mikecousins/react-pdf-js", 8 | "workspaces": [ 9 | "apps/*", 10 | "packages/*" 11 | ], 12 | "scripts": { 13 | "build": "turbo run build", 14 | "lint": "turbo run lint", 15 | "test": "turbo run test", 16 | "format": "prettier --write ." 17 | }, 18 | "prettier": { 19 | "printWidth": 80, 20 | "semi": true, 21 | "singleQuote": true, 22 | "trailingComma": "es5" 23 | }, 24 | "devDependencies": { 25 | "eslint": "^8.57.1", 26 | "prettier": "^3.5.3", 27 | "turbo": "^2.4.4", 28 | "typescript": "^5.8.2" 29 | }, 30 | "packageManager": "pnpm@10.7.0" 31 | } 32 | -------------------------------------------------------------------------------- /packages/react-pdf-js/README.md: -------------------------------------------------------------------------------- 1 | # react-pdf-js 2 | 3 | `react-pdf-js` provides a component for rendering PDF documents using [PDF.js](http://mozilla.github.io/pdf.js/). 4 | 5 | --- 6 | 7 | [![NPM Version](https://img.shields.io/npm/v/@mikecousins/react-pdf.svg?style=flat-square)](https://www.npmjs.com/package/@mikecousins/react-pdf) 8 | [![NPM Downloads](https://img.shields.io/npm/dm/@mikecousins/react-pdf.svg?style=flat-square)](https://www.npmjs.com/package/@mikecousins/react-pdf) 9 | [![codecov](https://codecov.io/gh/mikecousins/react-pdf-js/branch/master/graph/badge.svg)](https://codecov.io/gh/mikecousins/react-pdf-js) 10 | 11 | # Demo 12 | 13 | https://react-pdf.cousins.ai/ 14 | 15 | # Usage 16 | 17 | Install with `pnpm add @mikecousins/react-pdf pdfjs-dist`, `yarn add @mikecousins/react-pdf pdfjs-dist` or `npm install @mikecousins/react-pdf pdfjs-dist` 18 | 19 | ## `usePdf` hook 20 | 21 | Use the hook in your app (showing some basic pagination as well): 22 | 23 | ```js 24 | import React, { useState, useRef } from 'react'; 25 | import { usePdf } from '@mikecousins/react-pdf'; 26 | 27 | const MyPdfViewer = () => { 28 | const [page, setPage] = useState(1); 29 | const canvasRef = useRef(null); 30 | 31 | const { pdfDocument, pdfPage } = usePdf({ 32 | file: 'test.pdf', 33 | page, 34 | canvasRef, 35 | }); 36 | 37 | return ( 38 |
    39 | {!pdfDocument && Loading...} 40 | 41 | {Boolean(pdfDocument && pdfDocument.numPages) && ( 42 | 59 | )} 60 |
    61 | ); 62 | }; 63 | ``` 64 | 65 | ## Props 66 | 67 | When you call usePdf you'll want to pass in a subset of these props, like this: 68 | 69 | > `const { pdfDocument, pdfPage } = usePdf({ canvasRef, file: 'https://example.com/test.pdf', page });` 70 | 71 | ### canvasRef 72 | 73 | A reference to the canvas element. Create with: 74 | 75 | > `const canvasRef = useRef(null);` 76 | 77 | and then render it like: 78 | 79 | > `` 80 | 81 | and then pass it into usePdf. 82 | 83 | ### file 84 | 85 | URL of the PDF file. 86 | 87 | ### onDocumentLoadSuccess 88 | 89 | Allows you to specify a callback that is called when the PDF document data will be fully loaded. 90 | Callback is called with [PDFDocumentProxy](https://github.com/mozilla/pdf.js/blob/master/src/display/api.js#L579) 91 | as an only argument. 92 | 93 | ### onDocumentLoadFail 94 | 95 | Allows you to specify a callback that is called after an error occurred during PDF document data loading. 96 | 97 | ### onPageLoadSuccess 98 | 99 | Allows you to specify a callback that is called when the PDF page data will be fully loaded. 100 | Callback is called with [PDFPageProxy](https://github.com/mozilla/pdf.js/blob/master/src/display/api.js#L897) 101 | as an only argument. 102 | 103 | ### onPageLoadFail 104 | 105 | Allows you to specify a callback that is called after an error occurred during PDF page data loading. 106 | 107 | ### onPageRenderSuccess 108 | 109 | Allows you to specify a callback that is called when the PDF page will be fully rendered into the DOM. 110 | Callback is called with [PDFPageProxy](https://github.com/mozilla/pdf.js/blob/master/src/display/api.js#L897) 111 | as an only argument. 112 | 113 | ### onPageRenderFail 114 | 115 | Allows you to specify a callback that is called after an error occurred during PDF page rendering. 116 | 117 | ### page 118 | 119 | Specify the page that you want to display. Default = 1, 120 | 121 | ### scale 122 | 123 | Allows you to scale the PDF. Default = 1. 124 | 125 | ### rotate 126 | 127 | Allows you to rotate the PDF. Number is in degrees. Default = 0. 128 | 129 | ### cMapUrl 130 | 131 | Allows you to specify a cmap url. Default = '../node_modules/pdfjs-dist/cmaps/'. 132 | 133 | ### cMapPacked 134 | 135 | Allows you to specify whether the cmaps are packed or not. Default = false. 136 | 137 | ### workerSrc 138 | 139 | Allows you to specify a custom pdf worker url. Default = '//cdnjs.cloudflare.com/ajax/libs/pdf.js/\${pdfjs.version}/pdf.worker.js'. 140 | 141 | ### withCredentials 142 | 143 | Allows you to add the withCredentials flag. Default = false. 144 | 145 | ## Returned values 146 | 147 | ### pdfDocument 148 | 149 | `pdfjs`'s `PDFDocumentProxy` [object](https://github.com/mozilla/pdf.js/blob/master/src/display/api.js#L579). 150 | This can be undefined if document has not been loaded yet. 151 | 152 | ### pdfPage 153 | 154 | `pdfjs`'s `PDFPageProxy` [object](https://github.com/mozilla/pdf.js/blob/master/src/display/api.js#L897) 155 | This can be undefined if page has not been loaded yet. 156 | 157 | # License 158 | 159 | MIT © [mikecousins](https://github.com/mikecousins) 160 | -------------------------------------------------------------------------------- /packages/react-pdf-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mikecousins/react-pdf", 3 | "version": "8.0.1", 4 | "description": "Simple React component to wrap up PDF.js. The easiest way to render PDFs in your React app.", 5 | "author": "mikecousins", 6 | "license": "MIT", 7 | "sideEffects": false, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/mikecousins/react-pdf-js.git" 11 | }, 12 | "type": "module", 13 | "files": [ 14 | "dist" 15 | ], 16 | "main": "./dist/index.js", 17 | "types": "./dist/index.d.ts", 18 | "exports": { 19 | ".": { 20 | "import": { 21 | "types": "./dist/index.d.ts", 22 | "default": "./dist/index.js" 23 | }, 24 | "require": { 25 | "types": "./dist/index.d.cts", 26 | "default": "./dist/index.cjs" 27 | } 28 | } 29 | }, 30 | "scripts": { 31 | "build": "tsup src/index.tsx --dts --format esm,cjs", 32 | "lint": "eslint" 33 | }, 34 | "prettier": { 35 | "printWidth": 80, 36 | "semi": true, 37 | "singleQuote": true, 38 | "trailingComma": "es5" 39 | }, 40 | "devDependencies": { 41 | "@testing-library/react": "^16.2.0", 42 | "@types/jest": "^29.5.14", 43 | "@types/react": "^19.0.12", 44 | "@types/react-dom": "^19.0.4", 45 | "@vitejs/plugin-react": "^4.3.4", 46 | "jest-canvas-mock": "^2.5.2", 47 | "pdfjs-dist": "^5.0.375", 48 | "tsup": "^8.4.0", 49 | "typescript": "^5.8.2" 50 | }, 51 | "peerDependencies": { 52 | "pdfjs-dist": "^5", 53 | "react": "^19", 54 | "react-dom": "^19" 55 | }, 56 | "bundleDependencies": [] 57 | } 58 | -------------------------------------------------------------------------------- /packages/react-pdf-js/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import { GlobalWorkerOptions, getDocument, version } from 'pdfjs-dist'; 3 | import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist'; 4 | import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api'; 5 | 6 | function isFunction(value: any): value is Function { 7 | return typeof value === 'function'; 8 | } 9 | 10 | type PDFRenderTask = ReturnType; 11 | 12 | type HookProps = { 13 | canvasRef: React.RefObject; 14 | file: string; 15 | onDocumentLoadSuccess?: (document: PDFDocumentProxy) => void; 16 | onDocumentLoadFail?: () => void; 17 | onPageLoadSuccess?: (page: PDFPageProxy) => void; 18 | onPageLoadFail?: () => void; 19 | onPageRenderSuccess?: (page: PDFPageProxy) => void; 20 | onPageRenderFail?: () => void; 21 | scale?: number; 22 | rotate?: number; 23 | page?: number; 24 | cMapUrl?: string; 25 | cMapPacked?: boolean; 26 | workerSrc?: string; 27 | withCredentials?: boolean; 28 | }; 29 | 30 | type HookReturnValues = { 31 | pdfDocument: PDFDocumentProxy | undefined; 32 | pdfPage: PDFPageProxy | undefined; 33 | }; 34 | 35 | export const usePdf = ({ 36 | canvasRef, 37 | file, 38 | onDocumentLoadSuccess, 39 | onDocumentLoadFail, 40 | onPageLoadSuccess, 41 | onPageLoadFail, 42 | onPageRenderSuccess, 43 | onPageRenderFail, 44 | scale = 1, 45 | rotate = 0, 46 | page = 1, 47 | cMapUrl, 48 | cMapPacked, 49 | workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${version}/pdf.worker.mjs`, 50 | withCredentials = false, 51 | }: HookProps): HookReturnValues => { 52 | const [pdfDocument, setPdfDocument] = useState(); 53 | const [pdfPage, setPdfPage] = useState(); 54 | const renderTask = useRef(null); 55 | const lastPageRequestedRenderRef = useRef(null); 56 | const onDocumentLoadSuccessRef = useRef(onDocumentLoadSuccess); 57 | const onDocumentLoadFailRef = useRef(onDocumentLoadFail); 58 | const onPageLoadSuccessRef = useRef(onPageLoadSuccess); 59 | const onPageLoadFailRef = useRef(onPageLoadFail); 60 | const onPageRenderSuccessRef = useRef(onPageRenderSuccess); 61 | const onPageRenderFailRef = useRef(onPageRenderFail); 62 | 63 | // assign callbacks to refs to avoid redrawing 64 | useEffect(() => { 65 | onDocumentLoadSuccessRef.current = onDocumentLoadSuccess; 66 | }, [onDocumentLoadSuccess]); 67 | 68 | useEffect(() => { 69 | onDocumentLoadFailRef.current = onDocumentLoadFail; 70 | }, [onDocumentLoadFail]); 71 | 72 | useEffect(() => { 73 | onPageLoadSuccessRef.current = onPageLoadSuccess; 74 | }, [onPageLoadSuccess]); 75 | 76 | useEffect(() => { 77 | onPageLoadFailRef.current = onPageLoadFail; 78 | }, [onPageLoadFail]); 79 | 80 | useEffect(() => { 81 | onPageRenderSuccessRef.current = onPageRenderSuccess; 82 | }, [onPageRenderSuccess]); 83 | 84 | useEffect(() => { 85 | onPageRenderFailRef.current = onPageRenderFail; 86 | }, [onPageRenderFail]); 87 | 88 | useEffect(() => { 89 | GlobalWorkerOptions.workerSrc = workerSrc; 90 | }, [workerSrc]); 91 | 92 | useEffect(() => { 93 | const config: DocumentInitParameters = { url: file, withCredentials }; 94 | if (cMapUrl) { 95 | config.cMapUrl = cMapUrl; 96 | config.cMapPacked = cMapPacked; 97 | } 98 | 99 | getDocument(config).promise.then( 100 | (loadedPdfDocument) => { 101 | setPdfDocument(loadedPdfDocument); 102 | 103 | if (isFunction(onDocumentLoadSuccessRef.current)) { 104 | onDocumentLoadSuccessRef.current(loadedPdfDocument); 105 | } 106 | }, 107 | () => { 108 | if (isFunction(onDocumentLoadFailRef.current)) { 109 | onDocumentLoadFailRef.current(); 110 | } 111 | } 112 | ); 113 | }, [file, withCredentials, cMapUrl, cMapPacked]); 114 | 115 | useEffect(() => { 116 | // draw a page of the pdf 117 | const drawPDF = (page: PDFPageProxy) => { 118 | // Because this page's rotation option overwrites pdf default rotation value, 119 | // calculating page rotation option value from pdf default and this component prop rotate. 120 | const rotation = rotate === 0 ? page.rotate : page.rotate + rotate; 121 | const viewport = page.getViewport({ scale, rotation }); 122 | const canvasEl = canvasRef!.current; 123 | if (!canvasEl) { 124 | return; 125 | } 126 | 127 | const canvasContext = canvasEl.getContext('2d'); 128 | if (!canvasContext) { 129 | return; 130 | } 131 | 132 | canvasEl.height = viewport.height * window.devicePixelRatio; 133 | canvasEl.width = viewport.width * window.devicePixelRatio; 134 | 135 | canvasContext.scale(window.devicePixelRatio, window.devicePixelRatio); 136 | 137 | // if previous render isn't done yet, we cancel it 138 | if (renderTask.current) { 139 | lastPageRequestedRenderRef.current = page; 140 | renderTask.current.cancel(); 141 | return; 142 | } 143 | 144 | renderTask.current = page.render({ 145 | canvasContext, 146 | viewport, 147 | }); 148 | 149 | return renderTask.current.promise.then( 150 | () => { 151 | renderTask.current = null; 152 | 153 | if (isFunction(onPageRenderSuccessRef.current)) { 154 | onPageRenderSuccessRef.current(page); 155 | } 156 | }, 157 | (reason: Error) => { 158 | renderTask.current = null; 159 | 160 | if (reason && reason.name === 'RenderingCancelledException') { 161 | const lastPageRequestedRender = lastPageRequestedRenderRef.current ?? page; 162 | lastPageRequestedRenderRef.current = null; 163 | drawPDF(lastPageRequestedRender); 164 | } else if (isFunction(onPageRenderFailRef.current)) { 165 | onPageRenderFailRef.current(); 166 | } 167 | } 168 | ); 169 | }; 170 | 171 | if (pdfDocument) { 172 | pdfDocument.getPage(page).then( 173 | (loadedPdfPage) => { 174 | setPdfPage(loadedPdfPage); 175 | 176 | if (isFunction(onPageLoadSuccessRef.current)) { 177 | onPageLoadSuccessRef.current(loadedPdfPage); 178 | } 179 | 180 | drawPDF(loadedPdfPage); 181 | }, 182 | () => { 183 | if (isFunction(onPageLoadFailRef.current)) { 184 | onPageLoadFailRef.current(); 185 | } 186 | } 187 | ); 188 | } 189 | }, [canvasRef, page, pdfDocument, rotate, scale]); 190 | 191 | return { pdfDocument, pdfPage }; 192 | }; 193 | -------------------------------------------------------------------------------- /packages/react-pdf-js/test/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, wait, waitForDomChange } from '@testing-library/react'; 3 | import Pdf from '../src'; 4 | import { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api'; 5 | 6 | jest.mock('pdfjs-dist', () => ({ 7 | version: '1.0', 8 | GlobalWorkerOptions: { 9 | workerSrc: '', 10 | }, 11 | getDocument: jest.fn((config: DocumentInitParameters) => ({ 12 | promise: config.url?.includes('fail_document') 13 | ? Promise.reject() 14 | : Promise.resolve({ 15 | getPage: jest.fn(() => 16 | config.url?.includes('fail_page') 17 | ? Promise.reject() 18 | : Promise.resolve({ 19 | getViewport: jest.fn(() => ({ width: 0, height: 0 })), 20 | render: jest.fn(() => ({ 21 | promise: config.url?.includes('fail_render') 22 | ? Promise.reject() 23 | : Promise.resolve(), 24 | })), 25 | }) 26 | ), 27 | }), 28 | })), 29 | })); 30 | 31 | describe('Pdf', () => { 32 | it('renders children', async () => { 33 | const { getByText } = render( 34 | 35 | {({ canvas }) => ( 36 |
    37 | {canvas} 38 |
    Test
    39 |
    40 | )} 41 |
    42 | ); 43 | 44 | await waitForDomChange(); 45 | 46 | getByText('Test'); 47 | }); 48 | 49 | it('calls render function with proper params', async () => { 50 | const renderFunc = jest.fn(({ canvas }) => canvas); 51 | 52 | render({renderFunc}); 53 | 54 | expect(renderFunc).toBeCalledWith({ 55 | canvas: expect.any(Object), 56 | pdfDocument: undefined, 57 | pdfPage: undefined, 58 | }); 59 | 60 | await wait(); 61 | 62 | expect(renderFunc).toBeCalledWith({ 63 | canvas: expect.any(Object), 64 | pdfDocument: expect.any(Object), 65 | pdfPage: expect.any(Object), 66 | }); 67 | }); 68 | 69 | describe('callbacks', () => { 70 | const onDocLoadSuccess = jest.fn(); 71 | const onDocLoadFail = jest.fn(); 72 | const onPageLoadSuccess = jest.fn(); 73 | const onPageLoadFail = jest.fn(); 74 | const onPageRenderSuccess = jest.fn(); 75 | const onPageRenderFail = jest.fn(); 76 | 77 | beforeEach(() => { 78 | onDocLoadSuccess.mockClear(); 79 | onDocLoadFail.mockClear(); 80 | onPageLoadSuccess.mockClear(); 81 | onPageLoadFail.mockClear(); 82 | onPageRenderSuccess.mockClear(); 83 | onPageRenderFail.mockClear(); 84 | }); 85 | 86 | const renderPdf = (file: string) => 87 | render( 88 | 97 | {({ canvas }) => canvas} 98 | 99 | ); 100 | 101 | it('calls proper callbacks when fully successful', async () => { 102 | renderPdf('basic.33e35a62.pdf'); 103 | 104 | await wait(); 105 | 106 | expect(onDocLoadSuccess).toBeCalledWith(expect.any(Object)); 107 | expect(onDocLoadFail).not.toBeCalled(); 108 | expect(onPageLoadSuccess).toBeCalledWith(expect.any(Object)); 109 | expect(onPageLoadFail).not.toBeCalled(); 110 | expect(onPageRenderSuccess).toBeCalledWith(expect.any(Object)); 111 | expect(onPageRenderFail).not.toBeCalled(); 112 | }); 113 | 114 | it('calls proper callbacks when render failed', async () => { 115 | renderPdf('fail_render'); 116 | 117 | await wait(); 118 | 119 | expect(onDocLoadSuccess).toBeCalledWith(expect.any(Object)); 120 | expect(onDocLoadFail).not.toBeCalled(); 121 | expect(onPageLoadSuccess).toBeCalledWith(expect.any(Object)); 122 | expect(onPageLoadFail).not.toBeCalled(); 123 | expect(onPageRenderSuccess).not.toBeCalled(); 124 | expect(onPageRenderFail).toBeCalled(); 125 | }); 126 | 127 | it('calls proper callbacks when page load failed', async () => { 128 | renderPdf('fail_page'); 129 | 130 | await wait(); 131 | 132 | expect(onDocLoadSuccess).toBeCalledWith(expect.any(Object)); 133 | expect(onDocLoadFail).not.toBeCalled(); 134 | expect(onPageLoadSuccess).not.toBeCalled(); 135 | expect(onPageLoadFail).toBeCalled(); 136 | expect(onPageRenderSuccess).not.toBeCalled(); 137 | expect(onPageRenderFail).not.toBeCalled(); 138 | }); 139 | 140 | it('calls proper callbacks when document load failed', async () => { 141 | renderPdf('fail_document'); 142 | 143 | await wait(); 144 | 145 | expect(onDocLoadSuccess).not.toBeCalled(); 146 | expect(onDocLoadFail).toBeCalled(); 147 | expect(onPageLoadSuccess).not.toBeCalled(); 148 | expect(onPageLoadFail).not.toBeCalled(); 149 | expect(onPageRenderSuccess).not.toBeCalled(); 150 | expect(onPageRenderFail).not.toBeCalled(); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /packages/react-pdf-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "Node", 23 | "baseUrl": "./", 24 | "paths": { 25 | "*": ["packages/react-pdf-js/src/*", "node_modules/*"] 26 | }, 27 | "jsx": "react", 28 | "esModuleInterop": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - apps/* 3 | - packages/* 4 | onlyBuiltDependencies: 5 | - esbuild 6 | - sharp 7 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": [ 6 | "^build" 7 | ], 8 | "outputs": ["dist/**", "build/**"] 9 | }, 10 | "lint": {}, 11 | "test": {} 12 | } 13 | } 14 | --------------------------------------------------------------------------------