refactor(docs): using starlight theme (#59)

This commit is contained in:
Corentin THOMASSET
2025-01-10 20:31:50 +01:00
committed by GitHub
parent 81aa273378
commit 5f044e281d
34 changed files with 1428 additions and 3090 deletions

View File

@@ -1,6 +1,5 @@
# build output
dist/
# generated types
.astro/
@@ -13,12 +12,10 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

View File

@@ -1,16 +1,54 @@
# Papra docs
# Starlight Starter Kit: Basics
## Introduction
[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)
This Papra documentation website. It is built with Astro, Unocss, solidjs and shadcn-solid.
## Getting started
To get started, you can clone the repository and run the development server.
```bash
git clone https://github.com/papra-hq/papra.git
cd apps/docs
pnpm i
pnpm dev
```
npm create astro@latest -- --template starlight
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro + Starlight project, you'll see the following folders and files:
```
.
├── public/
├── src/
│ ├── assets/
│ ├── content/
│ │ ├── docs/
│ └── content.config.ts
├── astro.config.mjs
├── package.json
└── tsconfig.json
```
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
Static assets, like favicons, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Check out [Starlights docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).

View File

@@ -1,24 +1,60 @@
import sitemap from '@astrojs/sitemap';
import starlight from '@astrojs/starlight';
import { defineConfig } from 'astro/config';
import UnoCSS from 'unocss/astro';
import starlightThemeRapide from 'starlight-theme-rapide';
// https://astro.build/config
export default defineConfig({
site: 'https://docs.papra.app',
integrations: [
UnoCSS({
injectReset: true,
}),
sitemap(),
],
markdown: {
shikiConfig: {
themes: {
light: 'vitesse-light',
dark: 'vitesse-dark',
starlight({
plugins: [starlightThemeRapide()],
title: 'Papra Docs',
logo: {
dark: './src/assets/logo-dark.svg',
light: './src/assets/logo-light.svg',
alt: 'Papra Logo',
},
},
},
social: {
blueSky: 'https://bsky.app/profile/papra.app',
github: 'https://github.com/papra-hq/papra',
},
editLink: {
baseUrl: 'https://github.com/papra-hq/papra/edit/main/apps/docs/',
},
sidebar: [
{
label: 'Getting Started',
items: [
{ label: 'Introduction', slug: '' },
],
},
{
label: 'Self Hosting',
items: [
{ label: 'Using Docker', slug: 'self-hosting/using-docker' },
{ label: 'Using Docker Compose', slug: 'self-hosting/using-docker-compose' },
],
},
{
label: 'Configuration',
items: [
{ label: 'Environment variables', slug: 'configuration/environment-variables' },
],
},
],
favicon: '/favicon.svg',
head: [
// Add ICO favicon fallback for Safari.
{
tag: 'link',
attrs: {
rel: 'icon',
href: '/favicon.ico',
sizes: '32x32',
},
},
],
customCss: ['./src/assets/app.css'],
}),
],
});

View File

@@ -1,9 +1,10 @@
{
"name": "@papra/docs",
"name": "docs",
"type": "module",
"version": "0.0.1-beta.1",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
@@ -12,23 +13,17 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@astrojs/sitemap": "^3.2.1",
"astro": "^5.1.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"markdown-to-txt": "^2.0.1",
"minisearch": "^7.1.1",
"tailwind-merge": "^2.6.0",
"unstorage": "^1.14.4"
"@astrojs/starlight": "^0.30.5",
"astro": "^5.0.2",
"sharp": "^0.32.5",
"starlight-theme-rapide": "^0.3.0"
},
"devDependencies": {
"@antfu/eslint-config": "^3.12.2",
"@antfu/eslint-config": "^3.13.0",
"@iconify-json/tabler": "^1.1.120",
"@unocss/reset": "^0.64.0",
"eslint": "^9.17.0",
"eslint-plugin-astro": "^1.3.1",
"typescript": "^5.7.2",
"unocss": "0.65.0-beta.2",
"unocss-preset-animations": "^1.1.0"
"typescript": "^5.7.3"
}
}

1342
apps/docs/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
.site-title {
color:inherit !important;
gap: 0.5rem !important;
}
.site-title img {
width: 1.8rem !important;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#c4bdbd" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2zM9 9h1m-1 4h6m-6 4h6"/></g></svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#403a3a" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2zM9 9h1m-1 4h6m-6 4h6"/></g></svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@@ -1,41 +0,0 @@
---
import type { HTMLAttributes } from 'astro/types';
import { cn } from '@/utils/cn';
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium focus-visible:(outline-none ring-1.5 ring-ring) disabled:(pointer-events-none opacity-50) bg-inherit',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:(bg-accent text-accent-foreground)',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 px-3 text-xs',
lg: 'h-10 px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
export type Props<T extends 'a' | 'button' = 'button'> = { as?: T } & VariantProps<typeof buttonVariants> & HTMLAttributes<T>;
const { as: Element = 'button', class: className, variant, size, ...props } = Astro.props;
---
<Element class={cn(buttonVariants({ variant, size }), className)} {...props}>
<slot />
</Element>

View File

@@ -1,40 +0,0 @@
---
import { createStorage } from 'unstorage';
import fsLiteDriver from 'unstorage/drivers/fs-lite';
import Button from './Button.astro';
const repo = 'papra-hq/papra';
const repoUrl = `https://github.com/${repo}`;
const apiUrl = `https://ungh.cc/repos/${repo}`;
const storage = createStorage<{ stars: number; formattedStars: string }>({
driver: fsLiteDriver({ base: './.cache/stars' }),
});
async function getStars() {
const cachedStars = await storage.getItem(repo);
if (cachedStars) {
return cachedStars;
}
const stars: number = await fetch(apiUrl)
.then(res => res.json())
.then(data => data?.repo?.stars);
const formattedStars = new Intl.NumberFormat('en-US', { notation: 'compact' }).format(stars).toLowerCase();
await storage.setItem(repo, { stars, formattedStars });
return { stars, formattedStars };
}
const { stars, formattedStars } = await getStars();
---
<Button as="a" variant="outline" href={repoUrl} target="_blank" rel="noopener noreferrer" class="flex items-center gap-2" size={stars ? undefined : 'icon'} aria-label="GitHub repository">
<div class="i-tabler-brand-github size-4.5"></div>
{stars && <span>{formattedStars}</span>}
</Button>

View File

@@ -1,45 +0,0 @@
---
import Button from './Button.astro';
import GitHubStarsButton from './GitHubStarsButton.astro';
import ToggleThemeButton from './ToggleThemeButton.astro';
---
<nav class="flex justify-between items-center py-4">
<div class="flex items-center gap-2">
<Button variant="ghost" aria-label="Toggle menu" size="icon" class="sm:hidden" data-open-sidebar>
<div class="i-tabler-menu-2 size-4"></div>
</Button>
<Button variant="outline" class="justify-start items-center gap-2 md:min-w-60 bg-card" data-open-search-modal>
<div class="i-tabler-search size-4"></div>
<span class="flex-1 text-left">Search...</span>
</Button>
</div>
<div class="hidden sm:flex items-center gap-2">
<Button as="a" href="https://demo.papra.app" target="_blank" rel="noreferrer" variant="ghost" class="text-foreground flex items-center gap-2">
Demo app
<div class="i-tabler-external-link size-4"></div>
</Button>
<GitHubStarsButton />
<ToggleThemeButton />
</div>
<div class="flex sm:hidden items-center gap-2">
<Button as="a" href="https://github.com/papra-hq/papra" target="_blank" rel="noreferrer" variant="ghost" size="icon" aria-label="GitHub repository">
<div class="i-tabler-brand-github size-4.5"></div>
</Button>
</div>
</nav>
<script>
const searchButton = document.querySelector('[data-open-search-modal]')!;
// @ts-expect-error navigator.userAgentData is not supported in all browsers
const isMac = navigator.userAgentData?.platform?.toLowerCase().includes('mac') ?? navigator.userAgent.toLowerCase().includes('mac');
const shortcut = document.createElement('span');
shortcut.textContent = isMac ? '⌘ + K' : 'Ctrl + K';
shortcut.classList.add('text-xs', 'text-muted-foreground', 'px-1.5', 'py-0.5', 'bg-muted', 'rounded-sm', 'hidden', 'md:inline');
searchButton.appendChild(shortcut);
</script>

View File

@@ -1,37 +0,0 @@
---
import type { HTMLAttributes } from 'astro/types';
import { cn } from '@/utils/cn';
type Props = {
slug: string;
title: string;
direction: 'prev' | 'next';
} & HTMLAttributes<'a'>;
const { slug: rawSlug, title, direction, class: className, ...props } = Astro.props;
const slug = `/${rawSlug.replace(/^\//, '')}`;
const variants
= direction === 'prev'
? {
textAlign: 'text-left',
labelWrapper: 'justify-start',
icon: 'i-tabler-arrow-left',
label: 'Previous',
}
: {
textAlign: 'text-right',
labelWrapper: 'flex-row-reverse',
icon: 'i-tabler-arrow-right',
label: 'Next',
};
---
<a href={slug} class={cn('w-full p-6 bg-card rounded-lg border border-border hover:bg-accent hover:text-accent-foreground', variants.textAlign, className)} {...props}>
<div class={cn('flex items-center justify-start gap-2 text-sm text-muted-foreground', variants.labelWrapper)}>
<div class={cn('size-4', variants.icon)}></div>
<span>{variants.label}</span>
</div>
<div class="text-base font-semibold">{title}</div>
</a>

View File

@@ -1,188 +0,0 @@
---
import Button from './Button.astro';
---
<div class="fixed inset-0 bg-black backdrop-blur-sm bg-opacity-50 flex justify-center items-center z-60 hidden!" role="dialog" aria-labelledby="search-title" aria-hidden="true" id="search-modal">
<div class="absolute inset-0" role="button" tabindex="0" aria-label="Close Search" data-close-search-modal></div>
<div class="bg-card border rounded-lg max-w-lg w-full z-10">
<header class="flex items-center border-b py-1.5 pl-4 pr-1.5 gap-3">
<div class="i-tabler-search size-4"></div>
<div class="flex-1">
<label for="search-input" class="sr-only">Search Query</label>
<input
type="text"
id="search-input"
class="bg-transparent border-none focus:ring-none outline-none w-full text-base"
placeholder="Type to search..."
aria-describedby="search-results"
autofocus
/>
</div>
<Button variant="ghost" aria-label="Close" data-close-search-modal size="icon">
<div class="i-tabler-x size-4"></div>
</Button>
</header>
<div class="text-muted-foreground text-center pt-8 pb-4" id="no-results">No results found</div>
<ul id="search-results" class="flex flex-col gap-2 p-2" role="list" />
</div>
</div>
<script>
import MiniSearch, { type SearchResult } from 'minisearch';
// eslint-disable-next-line antfu/no-top-level-await
const docsIndex = await fetch('/search.json').then(res => res.json());
type Doc = {
title: string;
description: string;
slug: string;
};
const miniSearch = MiniSearch.loadJS<Doc>(docsIndex, {
idField: 'slug',
fields: ['title', 'description', 'content'],
storeFields: ['title', 'description', 'slug'],
searchOptions: {
fuzzy: 0.2,
},
});
const modalContainer = document.getElementById('search-modal')!;
const resultsList = document.getElementById('search-results')!;
const noResults = document.getElementById('no-results')!;
const searchInput = document.getElementById('search-input')!;
const closeSearch = document.querySelectorAll('[data-close-search-modal]');
const openSearch = document.querySelectorAll('[data-open-search-modal]');
let currentIndex = -1;
let filteredResults: SearchResult[] = [];
function openModal() {
modalContainer.setAttribute('aria-hidden', 'false');
modalContainer.classList.remove('hidden!');
searchInput.focus();
}
function closeModal() {
modalContainer.setAttribute('aria-hidden', 'true');
modalContainer.classList.add('hidden!');
}
function filterResults(event: Event) {
const query = (event.target as HTMLInputElement).value.toLowerCase();
resultsList.innerHTML = '';
currentIndex = -1;
filteredResults = miniSearch
.search(query, {
boost: { title: 2 },
prefix: true,
})
.slice(0, 10);
if (filteredResults.length === 0) {
noResults.style.display = 'block';
currentIndex = -1;
} else {
currentIndex = 0;
noResults.style.display = 'none';
const resultItems = filteredResults.map((item, index) => {
const li = document.createElement('li');
li.className = 'py-1.5 px-3 rounded cursor-pointer hover:bg-accent';
li.dataset.index = String(index);
const titleDiv = document.createElement('div');
titleDiv.className = 'text-base font-semibold';
titleDiv.textContent = item.title;
li.appendChild(titleDiv);
const contentDiv = document.createElement('div');
contentDiv.className = 'text-muted-foreground';
contentDiv.textContent = item.description;
li.appendChild(contentDiv);
li.onclick = () => navigateTo(item.slug);
resultsList.appendChild(li);
return li;
});
highlightResult(resultItems);
}
}
function handleKeyDown(event: KeyboardEvent) {
const resultItems = Array.from(resultsList.querySelectorAll('li'));
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
if (currentIndex < filteredResults.length - 1) {
currentIndex++;
highlightResult(resultItems);
}
break;
case 'ArrowUp':
event.preventDefault();
if (currentIndex > 0) {
currentIndex--;
highlightResult(resultItems);
}
break;
case 'Enter':
if (currentIndex >= 0 && filteredResults.length > 0) {
event.preventDefault();
selectResult(currentIndex);
}
break;
case 'Escape':
closeModal();
break;
}
}
function highlightResult(resultItems: Element[]) {
resultItems.forEach((item, index) => {
if (index === currentIndex) {
item.classList.add('bg-accent');
item.scrollIntoView({ block: 'nearest' });
} else {
item.classList.remove('bg-accent');
}
});
}
function selectResult(index: number) {
const selectedResult = filteredResults[index];
navigateTo(selectedResult.slug);
closeModal();
}
function navigateTo(slug: string) {
const url = slug === 'index' ? '/' : `/${slug}`;
window.location.href = url;
}
// open modal on Ctrl/Cmd + K
document.addEventListener('keydown', (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault();
openModal();
}
});
searchInput.addEventListener('input', event => filterResults(event));
searchInput.addEventListener('keydown', event => handleKeyDown(event));
openSearch.forEach(button => button.addEventListener('click', openModal));
closeSearch.forEach(button => button.addEventListener('click', closeModal));
</script>

View File

@@ -1,81 +0,0 @@
---
import Button from './Button.astro';
const navigation: { title: string; links: { title: string; href: string }[] }[] = [
{
title: 'Getting Started',
links: [
{ title: 'Introduction', href: '/' },
],
},
{
title: 'Self-hosting',
links: [
{ title: 'Using Docker', href: '/self-hosting/using-docker' },
{ title: 'Using Docker Compose', href: '/self-hosting/using-docker-compose' },
],
},
{
title: 'Configuration',
links: [
{ title: 'Environment variables', href: '/configuration/environment-variables' },
],
},
];
---
<aside id="sidebar" class="bg-card flex-1 justify-end sm:sticky top-0 h-screen border-r sm:flex absolute top-0 left-0 z-50 -translate-x-full sm:translate-x-0 transition-transform duration-200 ease-in-out">
<div class="min-w-260px p-4">
<a href="/" class="flex items-center gap-2 px-3 py-1.5">
<div class="i-tabler-file-text size-6"></div>
<div class="text-lg font-semibold">Papra Docs</div>
</a>
<div class="flex flex-col gap-0.5 mt-6">
{
navigation?.map(section => (
<div class="mb-6">
<div class="text-sm pl-3 py-1.5 font-semibold">{section.title}</div>
<ul class="flex flex-col ">
{section.links.map(link => (
<li>
<Button as="a" href={link.href} class="w-full flex justify-start p-0 py-1.5 pl-3 h-auto text-muted-foreground hover:bg-accent hover:text-accent-foreground" variant="ghost">
{link.title}
</Button>
</li>
))}
</ul>
</div>
))
}
</div>
</div>
</aside>
<div id="overlay" class="fixed inset-0 bg-black/40 z-40 sm:hidden transition-opacity duration-200 ease-in-out opacity-0 pointer-events-none backdrop-blur-sm"></div>
<script>
const menuButtons = document.querySelectorAll('[data-open-sidebar]');
const sidebar = document.querySelector<HTMLDivElement>('#sidebar')!;
const overlay = document.querySelector('#overlay')!;
function openSidebar() {
sidebar.classList.remove('-translate-x-full');
sidebar.classList.add('translate-x-0');
overlay.classList.remove('opacity-0');
overlay.classList.remove('pointer-events-none');
}
function closeSidebar() {
sidebar.classList.remove('translate-x-0');
sidebar.classList.add('-translate-x-full');
overlay.classList.add('opacity-0');
overlay.classList.add('pointer-events-none');
}
menuButtons.forEach(button => button.addEventListener('click', openSidebar));
overlay.addEventListener('click', closeSidebar);
</script>

View File

@@ -1,32 +0,0 @@
---
import type { MarkdownHeading } from 'astro';
import Button from './Button.astro';
type Props = {
headings?: MarkdownHeading[];
};
const { headings: allHeadings } = Astro.props;
const headings = allHeadings?.filter(item => item.depth > 1);
---
{
headings && headings.length > 0 && (
<>
<div class="text-sm font-semibold flex items-center gap-2">
<div class="i-tabler-align-justified size-4" />
On this page
</div>
<ul class="flex flex-col gap-0.5 mt-2 border-l pl-3 ml-2 py-1">
{headings.map(item => (
<li>
<Button as="a" href={`#${item.slug}`} class={`w-full flex justify-start p-0 py-0.5 h-auto text-muted-foreground pl-${(item.depth - 2) * 4}`} variant="link">
{item.text}
</Button>
</li>
))}
</ul>
</>
)
}

View File

@@ -1,25 +0,0 @@
---
import Button from './Button.astro';
---
<Button variant="outline" size="icon" class="toggle-theme-button" aria-label="Toggle theme">
<div class="i-tabler-moon dark:i-tabler-sun size-4.5!"></div>
</Button>
<script>
const theme = localStorage.getItem('theme');
if (theme) {
document.body.dataset.kbTheme = theme;
} else {
const isDarkPreferred = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.body.dataset.kbTheme = isDarkPreferred ? 'dark' : 'light';
}
const toggleThemeButtons = document.querySelectorAll('.toggle-theme-button');
toggleThemeButtons.forEach((button) => {
button.addEventListener('click', () => {
document.body.dataset.kbTheme = document.body.dataset.kbTheme === 'dark' ? 'light' : 'dark';
localStorage.setItem('theme', document.body.dataset.kbTheme);
});
});
</script>

View File

@@ -0,0 +1,10 @@
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
import { defineCollection } from 'astro:content';
export const collections = {
docs: defineCollection({
loader: docsLoader(),
schema: docsSchema(),
}),
};

View File

@@ -1,13 +0,0 @@
import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';
export const docsCollection = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/docs' }),
schema: z.object({
title: z.string(),
description: z.string(),
slug: z.string(),
}),
});
export const collections = { docs: docsCollection };

View File

@@ -0,0 +1,38 @@
---
title: Installing Papra using Docker
description: Self-host Papra using Docker.
slug: self-hosting/using-docker
---
Papra can be easily installed and run using Docker. This method is recommended for users who want a quick and straightforward way to deploy their own instance of Papra with minimal setup.
## Prerequisites
Before you begin, ensure that you have Docker installed on your system. You can download and install Docker from the official Docker website.
## Root and Rootless installation
Papra can be installed in two different ways:
- **Root**: This is the default installation method. It requires root privileges to run. The images are suffixed with `-root` like `corentinth/papra:latest-root` or `corentinth/papra:1.0.0-root`.
- **Rootless**: This method does not require root privileges to run. The images are suffixed with `-rootless` like `corentinth/papra:latest-rootless` or `corentinth/papra:1.0.0-rootless`.
## Image Sources
Papra Docker images are available on both **Docker Hub** and **GitHub Container Registry** (GHCR). You can choose the source that best suits your needs.
```bash frame="none"
# Using Docker Hub
docker pull corentinth/papra:latest-root
docker pull corentinth/papra:latest-rootless
# Using GitHub Container Registry
docker pull ghcr.io/corentinth/papra:latest-root
docker pull ghcr.io/corentinth/papra:latest-rootless
```
## Basic Usage
```bash frame="none"
docker run -d --name papra --restart unless-stopped -p 1221:1221 corentinth/papra:latest-root
```

View File

@@ -1,9 +1,6 @@
---
title: Using Docker Compose
description: Self-host Papra using Docker Compose.
slug: self-hosting/using-docker-compose
---
# Using Docker Compose
Coming soon.

View File

@@ -1,9 +1,6 @@
---
title: Environment variables
description: Environment variables for Papra.
slug: configuration/environment-variables
---
# Environment variables
Coming soon.

View File

@@ -1,11 +1,8 @@
---
title: Introduction
title: Papra docs
description: Papra documentation.
slug: index
---
# Papra docs
**WIP**: This is still a work in progress. The documentation is not yet complete.
Papra is a minimalistic document management and archiving platform. It is designed to be simple to use and accessible to everyone. Papra is a plateform for long-term document storage and management, like a digital archive for your documents.

View File

@@ -1,9 +0,0 @@
---
title: Using Docker
description: Self-host Papra using Docker.
slug: self-hosting/using-docker
---
# Using Docker
Coming soon.

View File

@@ -1,78 +0,0 @@
---
import type { MarkdownHeading } from 'astro';
import Header from '@/components/Header.astro';
import SearchModal from '@/components/SearchModal.astro';
import Sidenav from '@/components/Sidenav.astro';
import ToC from '@/components/ToC.astro';
import '../styles/app.css';
type Props = {
title?: string;
description?: string;
headings?: MarkdownHeading[];
};
const defaultTitle = 'Papra Docs';
const defaultDescription = 'Documentation for Papra, the open-source document management platform.';
const { title, description, headings } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{title ?? defaultTitle}</title>
<meta name="description" content={description ?? defaultDescription} />
<meta name="author" content="Corentin Thomasset" />
<link rel="sitemap" href="/sitemap-index.xml" />
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="Papra docs" />
<link rel="manifest" href="/site.webmanifest" />
<meta property="og:title" content={title ?? defaultTitle} />
<meta property="og:description" content={description ?? defaultDescription} />
<meta property="og:image" content={new URL('/og-image.png', Astro.url).toString()} />
<meta property="og:url" content={Astro.url} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Papra Docs" />
<meta property="og:locale" content="en_US" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Papra Docs" />
<meta property="og:image:type" content="image/png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content="@cthmsst" />
<meta name="twitter:title" content={title ?? defaultTitle} />
<meta name="twitter:description" content={description ?? defaultDescription} />
<meta name="twitter:image" content={new URL('/og-image.png', Astro.url).toString()} />
<link rel="canonical" href="https://docs.papra.app" />
</head>
<body class="min-h-screen font-sans text-sm bg-background">
<SearchModal />
<main class="flex min-h-screen items-start">
<Sidenav />
<article class="max-w-860px w-full px-4 sm:px-6 z-10 pb-42">
<Header />
<slot />
</article>
<aside class="sticky top-0 flex-1 hidden lg:block">
<div class="min-w-260px p-4 mt-16">
<ToC headings={headings} />
</div>
</aside>
</main>
</body>
</html>

View File

@@ -1,15 +0,0 @@
---
import Layout from '@/layouts/Layout.astro';
---
<Layout title="404 - Not Found" description="The page you are looking for does not exist.">
<div class="pt-12 flex items-center justify-center gap-4 flex-col sm:flex-row text-center sm:text-left">
<div class="i-tabler-coffee size-20 mb-4"></div>
<div>
<h1 class="text-lg font-medium">404 - Not Found</h1>
<p class="text-sm text-muted-foreground">The page you are looking for does not exist.</p>
</div>
</div>
</Layout>

View File

@@ -1,41 +0,0 @@
---
import PrevNextDocCard from '@/components/PrevNextDocCard.astro';
import Layout from '@/layouts/Layout.astro';
import { getCollection, render } from 'astro:content';
const docs = await getCollection('docs');
export async function getStaticPaths() {
const docs = await getCollection('docs');
return docs.map(doc => ({
params: { slug: doc.data.slug === 'index' ? undefined : doc.data.slug },
props: { doc },
}));
}
const { doc } = Astro.props;
const { Content, headings } = await render(doc);
const currentDocIndex = docs.findIndex(d => d.data.slug === doc.data.slug);
const nextDoc = docs[currentDocIndex + 1];
const prevDoc = docs[currentDocIndex - 1];
---
<Layout title={doc.data.title} description={doc.data.description} headings={headings}>
<div class="prose max-w-none text-base prose-coolgray dark:prose-invert mb-12">
<Content />
</div>
<a class="text-sm flex gap-2 items-center" href={`https://github.com/papra-hq/papra/blob/main/apps/docs/${doc.filePath}`} target="_blank" rel="noopener noreferrer">
<div class="i-tabler-edit size-4.5"></div>
Edit this page on GitHub
</a>
<hr class="my-4" />
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{ prevDoc && (<PrevNextDocCard direction="prev" {...prevDoc.data} />)}
{ nextDoc && (<PrevNextDocCard direction="next" {...nextDoc.data} class={!prevDoc ? 'grid-col-start-2' : ''} />) }
</div>
</Layout>

View File

@@ -1,15 +0,0 @@
import type { APIRoute } from 'astro';
function getRobotsTxt(sitemapURL: URL) {
return `
User-agent: *
Allow: /
Sitemap: ${sitemapURL.href}
`;
}
export const GET: APIRoute = ({ site }) => {
const sitemapURL = new URL('sitemap-index.xml', site);
return new Response(getRobotsTxt(sitemapURL));
};

View File

@@ -1,38 +0,0 @@
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
import { markdownToTxt } from 'markdown-to-txt';
import MiniSearch from 'minisearch';
function getRawContent(docsMarkdown: string | undefined) {
return markdownToTxt(docsMarkdown ?? '').replace(/\s+/g, ' ');
}
export const GET: APIRoute = async () => {
const docs = await getCollection('docs');
const docsWithContent = docs.map(doc => ({
...doc.data,
content: getRawContent(doc.body),
}));
const stopWords = new Set(['the', 'is', 'in', 'to', 'of', 'at', 'by', 'with', 'from', 'up', 'down', 'out', 'over', 'under', 'again', 'further', 'then', 'once', 'this', 'that', 'these', 'those', 'which', 'who', 'whom', 'whose', 'what', 'why', 'how', 'all', 'any', 'some', 'a', 'an', 'and', 'as', 'but', 'if', 'or', 'because', 'as', 'until', 'while']);
const miniSearch = new MiniSearch({
idField: 'slug',
fields: ['title', 'description', 'content'],
storeFields: ['title', 'description', 'slug'],
searchOptions: { fuzzy: 0.2 },
processTerm: term => term.toLowerCase().split(' ').filter(word => !stopWords.has(word)).join(' '),
});
miniSearch.addAll(docsWithContent);
return new Response(
JSON.stringify(miniSearch),
{
headers: {
'Content-Type': 'application/json',
},
},
);
};

View File

@@ -1,97 +0,0 @@
:root {
--background: 0 0% 100%;
--foreground: 168 4% 25%;
--card: 0 0% 98%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 252 94% 69%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--warning: 31 98% 50%;
--warning-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
[data-kb-theme="dark"] {
--background: 240 4% 10%;
--foreground: 0 0% 98%;
--card: 240 4% 8%;
--card-foreground: 0 0% 98%;
--popover: 240 4% 8%;
--popover-foreground: 0 0% 98%;
--primary: 256 100% 73%;
--primary: 77 100% 74%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--warning: 31 98% 50%;
--warning-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
[data-kb-theme="dark"] .astro-code,
[data-kb-theme="dark"] .astro-code span {
--shiki-dark-bg: hsl(var(--card)) !important;
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
.astro-code{
background-color: hsl(var(--card)) !important;
/* border: 1px solid hsl(var(--border) / 0.5) !important; */
}
.astro-code span {
background-color: hsl(var(--card)) !important;
}

View File

@@ -1,5 +0,0 @@
import type { ClassValue } from 'clsx';
import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';
export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists));

View File

@@ -1,22 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js",
"baseUrl": "./",
"paths": {
"@/*": [
"./src/*"
]
},
"allowJs": true,
"strictNullChecks": true
},
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist"
]
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}

View File

@@ -1,121 +0,0 @@
import {
defineConfig,
presetIcons,
presetTypography,
presetUno,
presetWebFonts,
transformerDirectives,
transformerVariantGroup,
} from 'unocss';
import presetAnimations from 'unocss-preset-animations';
export default defineConfig({
presets: [
presetUno({
dark: {
dark: '[data-kb-theme="dark"]',
light: '[data-kb-theme="light"]',
},
prefix: '',
}),
presetAnimations(),
presetIcons(),
presetTypography({
cssExtend: {
h1: {
'font-size': '1.875rem',
'font-weight': '700',
'line-height': '2.25rem',
},
pre: {
position: 'relative',
},
},
}),
presetWebFonts({
provider: 'bunny',
fonts: {
sans: 'DM Sans:300,400,500,600,700,800',
},
}),
],
transformers: [transformerVariantGroup(), transformerDirectives()],
theme: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
animation: {
keyframes: {
'accordion-down':
'{ from { height: 0 } to { height: var(--kb-accordion-content-height) } }',
'accordion-up':
'{ from { height: var(--kb-accordion-content-height) } to { height: 0 } }',
'collapsible-down':
'{ from { height: 0 } to { height: var(--kb-collapsible-content-height) } }',
'collapsible-up':
'{ from { height: var(--kb-collapsible-content-height) } to { height: 0 } }',
'caret-blink': '{ 0%,70%,100% { opacity: 1 } 20%,50% { opacity: 0 } }',
},
timingFns: {
'accordion-down': 'ease-out',
'accordion-up': 'ease-out',
'collapsible-down': 'ease-out',
'collapsible-up': 'ease-out',
'caret-blink': 'ease-out',
},
durations: {
'accordion-down': '0.2s',
'accordion-up': '0.2s',
'collapsible-down': '0.2s',
'collapsible-up': '0.2s',
'caret-blink': '1.25s',
},
counts: {
'caret-blink': 'infinite',
},
},
},
safelist: [
...'hidden'.split(' ').flatMap(word => [word, `${word}!`]),
...Array.from({ length: 30 }, (_, i) => `pl-${i}`),
],
});

2003
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff