diff --git a/FRAMEWORK.md b/FRAMEWORK.md index d77445d..fc28f62 100644 --- a/FRAMEWORK.md +++ b/FRAMEWORK.md @@ -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` | @@ -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 @@ -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 diff --git a/scripts/generate-doc-tree.mjs b/scripts/generate-doc-tree.mjs index 285ca6d..dbf915c 100644 --- a/scripts/generate-doc-tree.mjs +++ b/scripts/generate-doc-tree.mjs @@ -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) { diff --git a/shared/documentation-config.js b/shared/documentation-config.js index 2bbf2f0..2cd7e16 100644 --- a/shared/documentation-config.js +++ b/shared/documentation-config.js @@ -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 }} */ 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} */ diff --git a/src/components/CommandPalette/useSearchLogic.ts b/src/components/CommandPalette/useSearchLogic.ts index 37b29b1..293ae73 100644 --- a/src/components/CommandPalette/useSearchLogic.ts +++ b/src/components/CommandPalette/useSearchLogic.ts @@ -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'; @@ -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: @@ -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.', }, ]; @@ -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(() => { const commands: SearchResultType[] = [ { @@ -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]; @@ -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' }), diff --git a/src/components/ContentRenderer.tsx b/src/components/ContentRenderer.tsx index 7eaa106..4e8e0bd 100644 --- a/src/components/ContentRenderer.tsx +++ b/src/components/ContentRenderer.tsx @@ -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)' }} > -
+ {/* Row 1: adjacent docs — always in document flow under the article */} +
+ + + {/* Row 2: edit / issue / source (GitHub) — in flow, not absolute-positioned */} + diff --git a/src/components/DocPageFooter.tsx b/src/components/DocPageFooter.tsx index be57b3c..7daa7b1 100644 --- a/src/components/DocPageFooter.tsx +++ b/src/components/DocPageFooter.tsx @@ -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; } @@ -19,12 +23,13 @@ export default function DocPageFooter({ path, sourcePath }: DocPageFooterProps) return (