feat(docs): added doc website base (#50)
40
.github/workflows/ci-apps-docs.yaml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: CI - Docs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci-apps-docs:
|
||||
name: CI - Docs
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/docs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
corepack: true
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
working-directory: ./
|
||||
|
||||
- name: Run linters
|
||||
run: pnpm lint
|
||||
|
||||
# - name: Type check
|
||||
# run: pnpm typecheck
|
||||
|
||||
# - name: Run unit test
|
||||
# run: pnpm test
|
||||
|
||||
- name: Build the app
|
||||
run: pnpm build
|
||||
@@ -15,9 +15,9 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://demo.papra.app">Demo</a>
|
||||
<!-- <span> • </span>
|
||||
<a href="https://docs.papra.app">Docs</a>
|
||||
<span> • </span>
|
||||
<a href="https://docs.papra.app">Docs</a>
|
||||
<!-- <span> • </span>
|
||||
<a href="https://docs.papra.app/self-hosting/docker">Self-hosting</a> -->
|
||||
<span> • </span>
|
||||
<a href="https://github.com/orgs/papra-hq/projects/2">Roadmap</a>
|
||||
|
||||
24
apps/docs/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# build output
|
||||
dist/
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
16
apps/docs/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Papra docs
|
||||
|
||||
## Introduction
|
||||
|
||||
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
|
||||
```
|
||||
24
apps/docs/astro.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import { defineConfig } from 'astro/config';
|
||||
import UnoCSS from 'unocss/astro';
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://docs.papra.app',
|
||||
|
||||
integrations: [
|
||||
UnoCSS({
|
||||
injectReset: true,
|
||||
}),
|
||||
sitemap(),
|
||||
],
|
||||
|
||||
markdown: {
|
||||
|
||||
shikiConfig: {
|
||||
themes: {
|
||||
light: 'vitesse-light',
|
||||
dark: 'vitesse-dark',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
23
apps/docs/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import antfu from '@antfu/eslint-config';
|
||||
|
||||
export default antfu({
|
||||
astro: true,
|
||||
|
||||
stylistic: {
|
||||
semi: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
// To allow export on top of files
|
||||
'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
|
||||
'curly': ['error', 'all'],
|
||||
'vitest/consistent-test-it': ['error', { fn: 'test' }],
|
||||
'ts/consistent-type-definitions': ['error', 'type'],
|
||||
'style/brace-style': ['error', '1tbs', { allowSingleLine: false }],
|
||||
'unused-imports/no-unused-vars': ['error', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
}],
|
||||
},
|
||||
});
|
||||
34
apps/docs/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@papra/docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^3.12.2",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
1342
apps/docs/pnpm-lock.yaml
generated
Normal file
BIN
apps/docs/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
apps/docs/public/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
apps/docs/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/docs/public/favicon.png
Normal file
|
After Width: | Height: | Size: 414 KiB |
11
apps/docs/public/favicon.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<g fill="none" 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>
|
||||
<style>
|
||||
path { stroke: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { stroke: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 430 B |
BIN
apps/docs/public/og-image.png
Normal file
|
After Width: | Height: | Size: 414 KiB |
21
apps/docs/public/site.webmanifest
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Papra docs",
|
||||
"short_name": "Papra docs",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
BIN
apps/docs/public/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
apps/docs/public/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
41
apps/docs/src/components/Button.astro
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
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>
|
||||
40
apps/docs/src/components/GitHubStarsButton.astro
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
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'}>
|
||||
<div class="i-tabler-brand-github size-4.5"></div>
|
||||
{stars && <span>{formattedStars}</span>}
|
||||
</Button>
|
||||
49
apps/docs/src/components/Header.astro
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
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 sm:min-w-48 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>
|
||||
|
||||
<!-- <span class="text-xs text-muted-foreground px-1.5 py-0.5 bg-muted rounded-sm">
|
||||
Ctrl + K
|
||||
</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">
|
||||
<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');
|
||||
|
||||
searchButton.appendChild(shortcut);
|
||||
</script>
|
||||
37
apps/docs/src/components/PrevNextDocCard.astro
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
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>
|
||||
188
apps/docs/src/components/SearchModal.astro
Normal file
@@ -0,0 +1,188 @@
|
||||
---
|
||||
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>
|
||||
81
apps/docs/src/components/Sidenav.astro
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
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>
|
||||
32
apps/docs/src/components/ToC.astro
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
25
apps/docs/src/components/ToggleThemeButton.astro
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
import Button from './Button.astro';
|
||||
---
|
||||
|
||||
<Button variant="outline" size="icon" class="toggle-theme-button">
|
||||
<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>
|
||||
13
apps/docs/src/content/config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
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 };
|
||||
11
apps/docs/src/docs/01-getting-started/01-introduction.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: Introduction
|
||||
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.
|
||||
9
apps/docs/src/docs/02-self-hosting/01-using-docker.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Using Docker
|
||||
description: Self-host Papra using Docker.
|
||||
slug: self-hosting/using-docker
|
||||
---
|
||||
|
||||
# Using Docker
|
||||
|
||||
Coming soon.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Using Docker Compose
|
||||
description: Self-host Papra using Docker Compose.
|
||||
slug: self-hosting/using-docker-compose
|
||||
---
|
||||
|
||||
# Using Docker Compose
|
||||
|
||||
Coming soon.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Environment variables
|
||||
description: Environment variables for Papra.
|
||||
slug: configuration/environment-variables
|
||||
---
|
||||
|
||||
# Environment variables
|
||||
|
||||
Coming soon.
|
||||
78
apps/docs/src/layouts/Layout.astro
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
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>
|
||||
15
apps/docs/src/pages/404.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
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>
|
||||
|
||||
41
apps/docs/src/pages/[...slug].astro
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
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 sm: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>
|
||||
15
apps/docs/src/pages/robots.txt.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
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));
|
||||
};
|
||||
38
apps/docs/src/pages/search.json.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
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',
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
97
apps/docs/src/styles/app.css
Normal file
@@ -0,0 +1,97 @@
|
||||
: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;
|
||||
}
|
||||
5
apps/docs/src/utils/cn.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { ClassValue } from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists));
|
||||
22
apps/docs/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"allowJs": true,
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
121
apps/docs/uno.config.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
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}`),
|
||||
],
|
||||
});
|
||||