Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions FRAMEWORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ Generated output lands in `public/docs-content/` (and per-collection indexes). T
| OpenAPI explorer (Scalar) | Yes |
| Edit on GitHub footer | Yes |
| Version / i18n scaffolding | Yes |
| Language switcher UI + palette actions | Yes — `chromeConfig.languageSwitcher` (auto when multi-locale) |
| Branding / attribution chrome | Yes — `brandingConfig` + `SiteAttribution` (no hardcoded credit) |
| Sidebar variants | Yes — `chromeConfig.sidebarVariant`: `tree` \| `groups` |
| Tree sync | `npm run check:docs-tree` |
| Starter CLI | `npm create papers` |
| DESIGN.md design book (`/design`) | Yes — `designBookConfig` + `npm run generate:design-book` |
Expand All @@ -47,10 +50,30 @@ In `shared/documentation-config.js`:

```js
export const versionConfig = { current: '1.0', versions: ['1.0'], enabled: true };
export const i18nConfig = { enabled: true, defaultLocale: 'en', locales: ['en', 'fr'] };
export const i18nConfig = {
enabled: true,
defaultLocale: 'en',
locales: ['en', 'fr'],
labels: { en: 'EN', fr: 'FR' },
};
export const chromeConfig = {
languageSwitcher: { enabled: 'auto', placement: ['sidebar', 'command-palette'] },
sidebarVariant: 'groups', // or 'tree'
};
export const brandingConfig = {
productName: 'papers',
logo: null, // or { src: '/images/logo.png', alt: '…' }
copyright: { holder: 'Your Org' },
attribution: null, // or { text: 'Built with papers', href: '…' }
social: [{ name: 'GitHub', href: 'https://github.com/…', icon: 'mingcute:github-fill' }],
};
```

Add variant content under `src/docs/content/variants/`.
Add locale overlays under `src/docs/content/variants/locales/{locale}/…` (same paths as default). The nav tree **excludes** `variants/` — locales are selected via chrome / palette, not as a separate folder in the sidebar.

## OpenAPI

Already shipped: set `openapiConfig.enabled` and point `specs[].url` at YAML/JSON under `public/` (or remote). Interactive Scalar explorer at `/docs/{pagePath}`.

## Lazy feature invariants

Expand All @@ -59,6 +82,11 @@ Add variant content under `src/docs/content/variants/`.

Enforced by `src/framework/lazyFeatureBoundaries.ts` and architecture tests.

## Instance vs framework

- **Framework (this repo):** routing, chrome, OpenAPI viewer, generators, sample template docs.
- **Product instances** (e.g. companion docs apps): own Markdown content + `brandingConfig` / i18n locales only. Do not land product copy in papers.

## Near-term improvements

1. SSR prerender for first paint
Expand Down
7 changes: 6 additions & 1 deletion scripts/generate-doc-tree.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,12 @@ function isExcludedFile(relativePath) {
}

function isDirectoryExcluded(relativeDir) {
return EXCLUDED_PUBLIC_DOC_PATHS.has(toPosix(relativeDir));
const posix = toPosix(relativeDir);
if (EXCLUDED_PUBLIC_DOC_PATHS.has(posix)) return true;
// Locale/version overlays are not separate nav trees — selected via chrome/i18n only
if (posix === 'variants' || posix.startsWith('variants/')) return true;
if (posix === 'generated' || posix.startsWith('generated/')) return true;
return false;
}

function titleizeSegment(segment) {
Expand Down
49 changes: 48 additions & 1 deletion shared/documentation-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,58 @@ export const versionConfig = {
enabled: false,
};

/** @type {{ enabled: boolean, defaultLocale: string, locales: string[] }} */
/** @type {{ enabled: boolean, defaultLocale: string, locales: string[], labels?: Record<string, string> }} */
export const i18nConfig = {
enabled: false,
defaultLocale: 'en',
locales: ['en'],
labels: {
en: 'EN',
},
};

/**
* Site chrome — language switcher placement + sidebar skin.
* @type {{
* languageSwitcher: { enabled: 'auto' | boolean, placement: Array<'sidebar' | 'command-palette'> },
* sidebarVariant: 'tree' | 'groups',
* }}
*/
export const chromeConfig = {
languageSwitcher: {
enabled: 'auto',
placement: ['sidebar', 'command-palette'],
},
/** groups = section headers + page list; tree = classic file tree */
sidebarVariant: 'tree',
};

/**
* Instance branding — product mark, copyright, optional attribution, social links.
* Set attribution to null to hide the credit line.
* @type {{
* productName: string,
* logo?: { src: string, alt: string } | null,
* copyright: { holder: string, year?: number, notice?: string },
* attribution: { text: string, href?: string } | null,
* social: Array<{ name: string, href: string, icon: string }>,
* }}
*/
export const brandingConfig = {
productName: 'papers',
logo: null,
copyright: {
holder: 'papers',
notice: 'MIT License.',
},
attribution: null,
social: [
{
name: 'GitHub',
href: 'https://github.com/thomasjvu/papers',
icon: 'mingcute:github-fill',
},
],
};

/** @type {import('./documentation-config.js').OpenApiConfig} */
Expand Down
71 changes: 67 additions & 4 deletions src/components/CommandPalette/useSearchLogic.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { Icon } from '@iconify/react';
import { useMemo, createElement, useState, useEffect, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

import { documentationTree } from '../../data/documentation';
import { useDebounce } from '../../hooks/useDebounce';
import { usePagefind } from '../../hooks/usePagefind';
import { useTheme } from '../../providers/ThemeProvider';
import type { FileItem } from '../../types/documentation';
import { buildCanonicalDocsPath } from '../../../shared/docsRouting.js';
import {
buildCanonicalDocsPath,
parseDocsRoutePath,
} from '../../../shared/docsRouting.js';
import { homepageConfig } from '../../../shared/documentation-config.js';
import {
getLocaleLabel,
i18nConfig,
languageSwitcherShows,
} from '../../lib/chrome';

import type { SearchResultType } from './SearchResult';
import { combineSearchResults } from './searchUtils';
Expand All @@ -23,6 +32,11 @@ const FAQ_ITEMS = [
answer:
'Use Cmd + K (Mac) or Ctrl + K (Windows/Linux) to open this command palette, then type your search query.',
},
{
question: 'How do I change the language?',
answer:
'When multiple locales are enabled in i18nConfig, use the language control in the sidebar or open this command palette and type "language" or a locale label to switch while staying on the same page.',
},
{
question: 'How can I contribute to the documentation?',
answer:
Expand All @@ -36,7 +50,7 @@ const FAQ_ITEMS = [
{
question: 'How do I navigate between pages?',
answer:
'Use Shift + Left/Right Arrow on doc pages, the file tree on the left sidebar, the interactive mindmap on the right, or the Previous/Next buttons at the bottom of each page. You can also use this command palette to quickly jump to any page.',
'Use Shift + Left/Right Arrow on doc pages, the sidebar navigation, the interactive mindmap on the right, or the Previous/Next buttons at the bottom of each page. You can also use this command palette to quickly jump to any page.',
},
];

Expand Down Expand Up @@ -116,6 +130,15 @@ export const useSearchLogic = (query: string) => {
};
}, [debouncedQuery, pagefindAvailable, pagefindSearch]);

const navigate = useNavigate();
const location = useLocation();
const routeContext = useMemo(() => {
const slug = location.pathname.startsWith('/docs')
? location.pathname.replace(/^\/docs\/?/, '')
: location.pathname.replace(/^\/+|\/+$/g, '');
return parseDocsRoutePath(slug);
}, [location.pathname]);

const preferenceCommands = useMemo<SearchResultType[]>(() => {
const commands: SearchResultType[] = [
{
Expand Down Expand Up @@ -149,8 +172,45 @@ export const useSearchLogic = (query: string) => {
});
});

if (languageSwitcherShows('command-palette')) {
const activeLocale = routeContext.activeLocale || i18nConfig.defaultLocale;
const docPath = routeContext.docPath || 'index';
for (const locale of i18nConfig.locales || []) {
if (locale === activeLocale) continue;
const label = getLocaleLabel(locale);
commands.push({
title: `Switch to ${label}`,
path: `locale:${locale}`,
type: 'action',
description: `Change documentation language to ${label}`,
action: () => {
navigate(
buildCanonicalDocsPath(docPath, {
version: routeContext.activeVersion,
locale,
}),
);
},
icon: createElement(Icon, {
icon: 'mingcute:translate-2-line',
className: 'w-5 h-5',
}),
});
}
}

return commands;
}, [isDarkMode, prefersReducedMotion, setFontFamily, toggleDarkMode, toggleReducedMotion]);
}, [
isDarkMode,
prefersReducedMotion,
setFontFamily,
toggleDarkMode,
toggleReducedMotion,
navigate,
routeContext.activeLocale,
routeContext.activeVersion,
routeContext.docPath,
]);

const searchIndex = useMemo(() => {
const results: SearchResultType[] = [...preferenceCommands];
Expand Down Expand Up @@ -181,7 +241,10 @@ export const useSearchLogic = (query: string) => {
if (item.type === 'file') {
results.push({
title: item.name.replace(/\.md$/, ''),
path: buildCanonicalDocsPath(item.path),
path: buildCanonicalDocsPath(item.path, {
version: routeContext.activeVersion,
locale: routeContext.activeLocale,
}),
type: 'page',
description: parentPath,
icon: createElement(Icon, { icon: 'mingcute:file-line', className: 'w-5 h-5' }),
Expand Down
10 changes: 6 additions & 4 deletions src/components/ContentRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ const ContentRenderer = memo(function ContentRenderer({
className="doc-page-bottom doc-page-bottom--anchored mt-auto w-full max-w-4xl mx-auto border-t pt-4 pb-6 px-6 md:pb-8 md:px-8 lg:pb-12 lg:px-12"
style={{ borderColor: 'var(--border-unified)' }}
>
<div className="doc-page-bottom-row">
{/* Row 1: adjacent docs — always in document flow under the article */}
<nav className="doc-page-bottom-row" aria-label="Adjacent documentation pages">
<div className="doc-page-bottom-nav doc-page-bottom-nav--prev">
{prevPage ? (
<button
Expand All @@ -143,8 +144,6 @@ const ContentRenderer = memo(function ContentRenderer({
) : null}
</div>

<DocPageFooter path={path} sourcePath={sourcePath} />

<div className="doc-page-bottom-nav doc-page-bottom-nav--next">
{nextPage ? (
<button
Expand All @@ -163,7 +162,10 @@ const ContentRenderer = memo(function ContentRenderer({
</button>
) : null}
</div>
</div>
</nav>

{/* Row 2: edit / issue / source (GitHub) — in flow, not absolute-positioned */}
<DocPageFooter path={path} sourcePath={sourcePath} />
</motion.div>
</div>
</div>
Expand Down
11 changes: 9 additions & 2 deletions src/components/DocPageFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ type DocPageFooterProps = {
sourcePath?: string;
};

/**
* Bottom-of-page GitHub affordances for every doc.
* Renders nothing when VITE_GITHUB_URL is unset (links would be broken).
*/
export default function DocPageFooter({ path, sourcePath }: DocPageFooterProps) {
const githubUrl = import.meta.env.VITE_GITHUB_URL;
const githubUrl = import.meta.env.VITE_GITHUB_URL?.trim();
if (!githubUrl) {
return null;
}
Expand All @@ -19,12 +23,13 @@ export default function DocPageFooter({ path, sourcePath }: DocPageFooterProps)

return (
<footer className="doc-page-footer">
<nav className="doc-page-footer-links" aria-label="Edit this page">
<nav className="doc-page-footer-links" aria-label="Page source actions">
<a
href={editUrl}
target="_blank"
rel="noopener noreferrer"
className="doc-page-footer-link"
aria-label={`Edit ${path} on GitHub`}
>
edit
</a>
Expand All @@ -33,6 +38,7 @@ export default function DocPageFooter({ path, sourcePath }: DocPageFooterProps)
target="_blank"
rel="noopener noreferrer"
className="doc-page-footer-link"
aria-label={`Open a GitHub issue about ${path}`}
>
issue
</a>
Expand All @@ -41,6 +47,7 @@ export default function DocPageFooter({ path, sourcePath }: DocPageFooterProps)
target="_blank"
rel="noopener noreferrer"
className="doc-page-footer-link"
aria-label={`View source for ${path} on GitHub`}
>
source
</a>
Expand Down
Loading
Loading