├── .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 | [](https://www.npmjs.com/package/@mikecousins/react-pdf)
8 | [](https://www.npmjs.com/package/@mikecousins/react-pdf)
9 | [](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 |
43 |
44 |
45 | setPage(page - 1)}>
46 | Previous
47 |
48 |
49 |
50 | setPage(page + 1)}
53 | >
54 | Next
55 |
56 |
57 |
58 |
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 | [](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 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
setPage(page - 1)}
61 | className={clsx(
62 | previousDisabled ? 'text-gray-300' : 'text-gray-900'
63 | )}
64 | >
65 |
66 |
67 |
68 |
69 |
70 | {!pdfDocument && Loading... }
71 |
72 |
73 |
74 |
75 |
setPage(page + 1)}
78 | className={clsx(
79 | nextDisabled ? 'text-gray-300' : 'text-gray-900'
80 | )}
81 | >
82 |
83 |
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 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
setPage(page - 1)}
50 | className={clsx(previousDisabled && 'text-gray-300')}
51 | >
52 |
53 |
54 |
55 |
56 |
57 | {!pdfDocument && Loading... }
58 |
59 |
60 |
61 |
62 |
setPage(page + 1)}
65 | className={clsx(nextDisabled && 'text-gray-300')}
66 | >
67 |
68 |
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 |
41 |
42 |
43 | setPage(page - 1)}>
44 | Previous
45 |
46 |
47 |
48 | setPage(page + 1)}
51 | >
52 | Next
53 |
54 |
55 |
56 |
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 | 
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 | 
40 | 
41 |
42 |
43 | [https://www.npmjs.com/package/@mikecousins/react-pdf](https://www.npmjs.com/package/@mikecousins/react-pdf)
44 |
45 | ### Bundlephobia
46 | 
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 |
27 | ) : (
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/apps/docs/src/components/Callout.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 |
3 | import { Icon } from '@/components/Icon'
4 |
5 | const styles = {
6 | note: {
7 | container:
8 | 'bg-sky-50 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10',
9 | title: 'text-sky-900 dark:text-sky-400',
10 | body: 'text-sky-800 [--tw-prose-background:var(--color-sky-50)] prose-a:text-sky-900 prose-code:text-sky-900 dark:text-slate-300 dark:prose-code:text-slate-300',
11 | },
12 | warning: {
13 | container:
14 | 'bg-amber-50 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10',
15 | title: 'text-amber-900 dark:text-amber-500',
16 | body: 'text-amber-800 [--tw-prose-underline:var(--color-amber-400)] [--tw-prose-background:var(--color-amber-50)] prose-a:text-amber-900 prose-code:text-amber-900 dark:text-slate-300 dark:[--tw-prose-underline:var(--color-sky-700)] dark:prose-code:text-slate-300',
17 | },
18 | }
19 |
20 | const icons = {
21 | note: (props: { className?: string }) => ,
22 | warning: (props: { className?: string }) => (
23 |
24 | ),
25 | }
26 |
27 | export function Callout({
28 | title,
29 | children,
30 | type = 'note',
31 | }: {
32 | title: string
33 | children: React.ReactNode
34 | type?: keyof typeof styles
35 | }) {
36 | let IconComponent = icons[type]
37 |
38 | return (
39 |
40 |
41 |
42 |
45 | {title}
46 |
47 |
48 | {children}
49 |
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/apps/docs/src/components/DocsHeader.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { usePathname } from 'next/navigation'
4 |
5 | import { navigation } from '@/lib/navigation'
6 |
7 | export function DocsHeader({ title }: { title?: string }) {
8 | let pathname = usePathname()
9 | let section = navigation.find((section) =>
10 | section.links.find((link) => link.href === pathname),
11 | )
12 |
13 | if (!title && !section) {
14 | return null
15 | }
16 |
17 | return (
18 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/apps/docs/src/components/DocsLayout.tsx:
--------------------------------------------------------------------------------
1 | import { type Node } from '@markdoc/markdoc'
2 |
3 | import { DocsHeader } from '@/components/DocsHeader'
4 | import { PrevNextLinks } from '@/components/PrevNextLinks'
5 | import { Prose } from '@/components/Prose'
6 | import { TableOfContents } from '@/components/TableOfContents'
7 | import { collectSections } from '@/lib/sections'
8 |
9 | export function DocsLayout({
10 | children,
11 | frontmatter: { title },
12 | nodes,
13 | }: {
14 | children: React.ReactNode
15 | frontmatter: { title?: string }
16 | nodes: Array
17 | }) {
18 | let tableOfContents = collectSections(nodes)
19 |
20 | return (
21 | <>
22 |
23 |
24 |
25 | {children}
26 |
27 |
28 |
29 |
30 | >
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/apps/docs/src/components/Fence.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Fragment } from 'react'
4 | import { Highlight } from 'prism-react-renderer'
5 |
6 | export function Fence({
7 | children,
8 | language,
9 | }: {
10 | children: string
11 | language: string
12 | }) {
13 | return (
14 |
19 | {({ className, style, tokens, getTokenProps }) => (
20 |
21 |
22 | {tokens.map((line, lineIndex) => (
23 |
24 | {line
25 | .filter((token) => !token.empty)
26 | .map((token, tokenIndex) => (
27 |
28 | ))}
29 | {'\n'}
30 |
31 | ))}
32 |
33 |
34 | )}
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/apps/docs/src/components/Hero.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from 'react'
2 | import Image from 'next/image'
3 | import clsx from 'clsx'
4 | import { Highlight } from 'prism-react-renderer'
5 |
6 | import { Button } from '@/components/Button'
7 | import { HeroBackground } from '@/components/HeroBackground'
8 | import blurCyanImage from '@/images/blur-cyan.png'
9 | import blurIndigoImage from '@/images/blur-indigo.png'
10 |
11 | const codeLanguage = 'typescript'
12 | const code = ` const { pdfDocument, pdfPage } = usePdf({
13 | file: 'test.pdf',
14 | page,
15 | canvasRef,
16 | });`
17 |
18 | const tabs = [
19 | { name: 'pdf-viewer.tsx', isActive: true },
20 | { name: 'package.json', isActive: false },
21 | ]
22 |
23 | function TrafficLightsIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | export function Hero() {
34 | return (
35 |
36 |
37 |
38 |
39 |
48 |
49 |
50 | The simplest way to embed PDFs.
51 |
52 |
53 | Under 1kB in size. Modern React hook architecture. Easily add embedded
54 | PDFs in your modern React web app.
55 |
56 |
57 | Get started
58 |
59 | View on GitHub
60 |
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 |
121 | {Array.from({
122 | length: code.split('\n').length,
123 | }).map((_, index) => (
124 |
125 | {(index + 1).toString().padStart(2, '0')}
126 |
127 |
128 | ))}
129 |
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 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
30 |
34 |
38 |
46 |
53 |
60 |
68 |
76 |
84 |
93 |
101 |
108 |
115 |
122 |
130 |
138 |
147 |
155 |
163 |
171 |
178 |
185 |
186 |
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 |
46 |
47 |
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 |
18 |
19 |
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 |
13 |
14 |
15 | )
16 | }
17 |
18 | export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
19 | return (
20 |
21 |
22 |
23 |
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 |
21 |
22 |
23 | )
24 | }
25 |
26 | function CloseIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
27 | return (
28 |
36 |
37 |
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 | setIsOpen(true)}
71 | className="relative"
72 | aria-label="Open navigation"
73 | >
74 |
75 |
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 | close()}
90 | aria-label="Close navigation"
91 | >
92 |
93 |
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 |
18 |
19 | {navigation.map((section) => (
20 |
21 |
22 | {section.title}
23 |
24 |
28 | {section.links.map((link) => (
29 |
30 |
40 | {link.title}
41 |
42 |
43 | ))}
44 |
45 |
46 | ))}
47 |
48 |
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 |
12 |
13 |
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 |
39 |
40 |
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 |
116 |
117 |
123 |
124 |
132 |
133 |
134 |
135 |
136 |
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 |
185 |
186 |
187 | {hierarchy.length > 0 && (
188 |
193 | {hierarchy.map((item, itemIndex, items) => (
194 |
195 |
196 |
203 | /
204 |
205 |
206 | ))}
207 |
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 |
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 |
450 |
451 |
452 | Search docs
453 |
454 | {modifierKey && (
455 |
456 | {modifierKey}
457 | K
458 |
459 | )}
460 |
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 |
67 | {tableOfContents.length > 0 && (
68 | <>
69 |
73 | On this page
74 |
75 |
76 | {tableOfContents.map((section) => (
77 |
78 |
79 |
87 | {section.title}
88 |
89 |
90 | {section.children.length > 0 && (
91 |
95 | {section.children.map((subSection) => (
96 |
97 |
105 | {subSection.title}
106 |
107 |
108 | ))}
109 |
110 | )}
111 |
112 | ))}
113 |
114 | >
115 | )}
116 |
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 |
21 |
26 |
27 | )
28 | }
29 |
30 | function DarkIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
31 | return (
32 |
33 |
38 |
39 | )
40 | }
41 |
42 | function SystemIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
43 | return (
44 |
45 |
50 |
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 | Theme
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 |
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 | [](https://www.npmjs.com/package/@mikecousins/react-pdf)
8 | [](https://www.npmjs.com/package/@mikecousins/react-pdf)
9 | [](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 |
43 |
44 |
45 | setPage(page - 1)}>
46 | Previous
47 |
48 |
49 |
50 | setPage(page + 1)}
53 | >
54 | Next
55 |
56 |
57 |
58 |
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 |
--------------------------------------------------------------------------------