@umami/shiso 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/shiso.mjs +0 -0
- package/dist/chunks/App.js +310 -205
- package/dist/chunks/architectureDiagram-5GKGNRK7.js +1 -1
- package/dist/chunks/chunk-GMAD6QVW.js +1 -1
- package/dist/chunks/cose-bilkent-JH36ORCC.js +1 -1
- package/dist/chunks/dist.js +1 -1
- package/dist/chunks/docs.js +848 -202
- package/dist/chunks/ganttDiagram-EL5Y4UJY.js +1 -1
- package/dist/chunks/src.js +1 -1
- package/dist/entry-client.js +1 -1
- package/dist/entry-server.js +2 -2
- package/docs.schema.json +1135 -1003
- package/package.json +1 -1
- package/scripts/check-content.mjs +2 -0
- package/scripts/load-shiso-config.mjs +30 -1
- package/src/App.tsx +41 -23
- package/src/components/CodeBlock.tsx +3 -1
- package/src/components/DocContent.tsx +15 -6
- package/src/components/Docs.tsx +3 -1
- package/src/components/Footer.tsx +3 -1
- package/src/components/Header.tsx +4 -3
- package/src/components/LanguageSwitcher.tsx +27 -7
- package/src/components/OpenApiOperation.tsx +22 -15
- package/src/components/PageActions.tsx +135 -0
- package/src/components/docs/Changelog.tsx +27 -13
- package/src/components/docs/CodeGroup.tsx +3 -1
- package/src/components/docs/Mermaid.tsx +8 -6
- package/src/components/docs/PropertiesTable.tsx +11 -6
- package/src/components/docs/Tabs.tsx +3 -1
- package/src/components/docs/Tree.tsx +3 -1
- package/src/components/docs/ZoomableImage.tsx +4 -2
- package/src/components/ui/dialog.tsx +7 -2
- package/src/components/ui/sheet.tsx +5 -2
- package/src/lib/label-context.tsx +7 -0
- package/src/lib/labels.ts +37 -0
- package/src/lib/locale.ts +25 -0
- package/src/lib/openapi.ts +21 -8
- package/src/lib/site-config.ts +66 -2
- package/src/lib/site-model.ts +4 -32
- package/src/lib/standalone-pages.ts +15 -1
- package/src/lib/translations/de.json +74 -0
- package/src/lib/translations/en.json +74 -0
- package/src/lib/translations/es.json +74 -0
- package/src/lib/translations/fr.json +74 -0
- package/src/lib/translations/ja.json +74 -0
- package/src/lib/translations/zh-Hans.json +74 -0
- package/src/lib/translations/zh-Hant.json +74 -0
- package/src/lib/types.ts +44 -32
- package/src/styles/global.css +1 -1
- package/types/config.d.ts +7 -1
- package/types/labels.d.ts +76 -0
package/package.json
CHANGED
|
@@ -87,6 +87,8 @@ function collectPageReferences(navigation) {
|
|
|
87
87
|
|
|
88
88
|
function visitContainer(container) {
|
|
89
89
|
if (!container || typeof container !== 'object') return;
|
|
90
|
+
// Groups reached through tabs/dropdowns/languages carry their own landing page.
|
|
91
|
+
if (typeof container.root === 'string') references.push(container.root);
|
|
90
92
|
if (Array.isArray(container.pages)) visitItems(container.pages);
|
|
91
93
|
for (const key of ['tabs', 'dropdowns', 'groups', 'versions', 'languages']) {
|
|
92
94
|
if (Array.isArray(container[key])) container[key].forEach(visitContainer);
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { createJiti } from 'jiti';
|
|
5
|
+
import englishLabels from '../src/lib/translations/en.json' with { type: 'json' };
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Loads the optional project code config (shiso.config.ts/.mjs/.js).
|
|
@@ -16,7 +17,7 @@ import { createJiti } from 'jiti';
|
|
|
16
17
|
export const SHISO_CONFIG_FILES = ['shiso.config.ts', 'shiso.config.mjs', 'shiso.config.js'];
|
|
17
18
|
|
|
18
19
|
const STRING_KEYS = ['docsPrefix', 'contentDir', 'siteUrl', 'locale'];
|
|
19
|
-
const KNOWN_KEYS = [...STRING_KEYS, 'mdx'];
|
|
20
|
+
const KNOWN_KEYS = [...STRING_KEYS, 'mdx', 'translations'];
|
|
20
21
|
const MDX_KEYS = ['remarkPlugins', 'rehypePlugins'];
|
|
21
22
|
|
|
22
23
|
let importGeneration = 0;
|
|
@@ -88,6 +89,31 @@ function resolveMdxConfig(value, sourcePath) {
|
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
function resolveTranslations(value, sourcePath) {
|
|
93
|
+
if (value === undefined) return undefined;
|
|
94
|
+
const invalid = message => {
|
|
95
|
+
throw new ShisoConfigLoadError(message, { code: 'INVALID_OPTION', sourcePath });
|
|
96
|
+
};
|
|
97
|
+
if (!isPlainObject(value)) invalid('Shiso config option "translations" must be an object.');
|
|
98
|
+
const result = {};
|
|
99
|
+
for (const [locale, labels] of Object.entries(value)) {
|
|
100
|
+
let canonical;
|
|
101
|
+
try {
|
|
102
|
+
canonical = Intl.getCanonicalLocales(locale)[0];
|
|
103
|
+
} catch {
|
|
104
|
+
invalid(`Invalid translations locale "${locale}". Use a BCP 47 tag such as "fr" or "pt-BR".`);
|
|
105
|
+
}
|
|
106
|
+
if (!isPlainObject(labels)) invalid(`translations.${locale} must be an object.`);
|
|
107
|
+
for (const [key, label] of Object.entries(labels)) {
|
|
108
|
+
if (!Object.hasOwn(englishLabels, key))
|
|
109
|
+
invalid(`Unknown UI label "translations.${locale}.${key}".`);
|
|
110
|
+
if (typeof label !== 'string') invalid(`translations.${locale}.${key} must be a string.`);
|
|
111
|
+
}
|
|
112
|
+
result[canonical] = { ...result[canonical], ...labels };
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
91
117
|
/**
|
|
92
118
|
* Applies defaults and normalization. Single source of truth for resolved
|
|
93
119
|
* values, so runtime and build-time consumers never re-implement defaulting.
|
|
@@ -103,6 +129,9 @@ export function resolveShisoConfig(raw = {}, sourcePath = null) {
|
|
|
103
129
|
siteUrl: raw.siteUrl?.trim().replace(/\/+$/, '') || undefined,
|
|
104
130
|
locale: raw.locale?.trim() || 'en-US',
|
|
105
131
|
mdx: resolveMdxConfig(raw.mdx, sourcePath),
|
|
132
|
+
...(raw.translations === undefined
|
|
133
|
+
? {}
|
|
134
|
+
: { translations: resolveTranslations(raw.translations, sourcePath) }),
|
|
106
135
|
};
|
|
107
136
|
}
|
|
108
137
|
|
package/src/App.tsx
CHANGED
|
@@ -3,43 +3,61 @@ import '@fontsource/jetbrains-mono/400.css';
|
|
|
3
3
|
import '@umami/shiso/styles.css';
|
|
4
4
|
|
|
5
5
|
import { MDXProvider } from '@mdx-js/react';
|
|
6
|
-
import { Navigate, Route, Routes } from 'react-router';
|
|
6
|
+
import { Navigate, Route, Routes, useLocation } from 'react-router';
|
|
7
7
|
import { CodeBlock } from '@/components/CodeBlock';
|
|
8
8
|
import * as docsComponents from '@/components/docs/index';
|
|
9
9
|
import { Layout } from '@/components/Layout';
|
|
10
10
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
|
11
|
-
import {
|
|
11
|
+
import { LabelContext } from '@/lib/label-context';
|
|
12
|
+
import {
|
|
13
|
+
docsHomeUrl,
|
|
14
|
+
getSiteModelByPathname,
|
|
15
|
+
hasRootStandalonePage,
|
|
16
|
+
standalonePages,
|
|
17
|
+
} from '@/lib/site-config';
|
|
12
18
|
import { DocPage } from '@/pages/DocPage';
|
|
13
19
|
import { StandalonePageView } from '@/pages/StandalonePage';
|
|
14
20
|
|
|
21
|
+
// Runtime helpers are public exports, but cannot be rendered as MDX components.
|
|
22
|
+
const {
|
|
23
|
+
mermaidSource: _mermaidSource,
|
|
24
|
+
usePanelContent: _usePanelContent,
|
|
25
|
+
useSetPanelContent: _useSetPanelContent,
|
|
26
|
+
...mdxDocsComponents
|
|
27
|
+
} = docsComponents;
|
|
28
|
+
|
|
15
29
|
const mdxComponents = {
|
|
16
|
-
...
|
|
30
|
+
...mdxDocsComponents,
|
|
17
31
|
img: docsComponents.ZoomableImage,
|
|
18
32
|
pre: CodeBlock,
|
|
19
33
|
};
|
|
20
34
|
|
|
21
35
|
export function App() {
|
|
36
|
+
const { pathname } = useLocation();
|
|
37
|
+
const siteModel = getSiteModelByPathname(pathname);
|
|
22
38
|
return (
|
|
23
|
-
<
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
<LabelContext.Provider value={siteModel.labels}>
|
|
40
|
+
<TooltipProvider>
|
|
41
|
+
<MDXProvider components={mdxComponents}>
|
|
42
|
+
<Layout site={siteModel}>
|
|
43
|
+
<Routes>
|
|
44
|
+
{standalonePages.map(page => (
|
|
45
|
+
<Route
|
|
46
|
+
key={page.path}
|
|
47
|
+
path={page.path}
|
|
48
|
+
element={<StandalonePageView page={page} site={siteModel} />}
|
|
49
|
+
/>
|
|
50
|
+
))}
|
|
51
|
+
{/* When the default scope's landing page is the root, or a
|
|
35
52
|
standalone page owns "/", there is nothing to redirect. */}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
{docsHomeUrl !== '/' && !hasRootStandalonePage ? (
|
|
54
|
+
<Route path="/" element={<Navigate to={docsHomeUrl} replace />} />
|
|
55
|
+
) : null}
|
|
56
|
+
<Route path="*" element={<DocPage site={siteModel} />} />
|
|
57
|
+
</Routes>
|
|
58
|
+
</Layout>
|
|
59
|
+
</MDXProvider>
|
|
60
|
+
</TooltipProvider>
|
|
61
|
+
</LabelContext.Provider>
|
|
44
62
|
);
|
|
45
63
|
}
|
|
@@ -3,6 +3,7 @@ import { type ComponentProps, type CSSProperties, useRef, useState } from 'react
|
|
|
3
3
|
import { CheckIcon, Copy } from '@/components/icons';
|
|
4
4
|
import { Button } from '@/components/ui/button';
|
|
5
5
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
6
|
+
import { useLabels } from '@/lib/label-context';
|
|
6
7
|
import { cn } from '@/lib/utils';
|
|
7
8
|
import { Mermaid, type MermaidPlacement } from './docs/Mermaid';
|
|
8
9
|
|
|
@@ -60,6 +61,7 @@ function copyText(pre: HTMLPreElement | null, language?: string): string {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export function CodeBlock({ children, className, style, ...rest }: CodeBlockProps) {
|
|
64
|
+
const labels = useLabels();
|
|
63
65
|
const {
|
|
64
66
|
'data-title': title,
|
|
65
67
|
'data-language': language,
|
|
@@ -137,7 +139,7 @@ export function CodeBlock({ children, className, style, ...rest }: CodeBlockProp
|
|
|
137
139
|
title ? 'top-1' : 'top-2.5',
|
|
138
140
|
)}
|
|
139
141
|
onClick={handleCopy}
|
|
140
|
-
aria-label=
|
|
142
|
+
aria-label={copied ? labels.copied : labels.copyCode}
|
|
141
143
|
>
|
|
142
144
|
{copied ? <CheckIcon className="size-3.5 text-primary" /> : <Copy className="size-3.5" />}
|
|
143
145
|
</Button>
|
|
@@ -3,6 +3,7 @@ import { ContextualMenu } from '@/components/ContextualMenu';
|
|
|
3
3
|
import { Badge } from '@/components/docs/Badge';
|
|
4
4
|
import { ArrowLeft, ArrowRight, FileText } from '@/components/icons';
|
|
5
5
|
import { OpenApiOperation } from '@/components/OpenApiOperation';
|
|
6
|
+
import { PageActions } from '@/components/PageActions';
|
|
6
7
|
import { getLastModified } from '@/lib/content';
|
|
7
8
|
import { getScopeForPage } from '@/lib/docs-config';
|
|
8
9
|
import { resolveLocale } from '@/lib/locale';
|
|
@@ -135,12 +136,20 @@ export function DocContent({ page, doc, site }: DocContentProps) {
|
|
|
135
136
|
<Content />
|
|
136
137
|
</div>
|
|
137
138
|
{operation && <OpenApiOperation operation={operation} />}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
<PageActions
|
|
140
|
+
key={page.url}
|
|
141
|
+
page={page}
|
|
142
|
+
frontmatter={doc.frontmatter}
|
|
143
|
+
site={site}
|
|
144
|
+
lastUpdated={
|
|
145
|
+
lastModified ? (
|
|
146
|
+
<>
|
|
147
|
+
{site.labels.lastUpdated}{' '}
|
|
148
|
+
<time dateTime={lastModified}>{dateFormat.format(new Date(lastModified))}</time>
|
|
149
|
+
</>
|
|
150
|
+
) : undefined
|
|
151
|
+
}
|
|
152
|
+
/>
|
|
144
153
|
{related.length > 0 && (
|
|
145
154
|
<nav className="mt-8" aria-label={site.labels.relatedTopics} data-pagefind-ignore>
|
|
146
155
|
<div className="text-sm text-muted-foreground">{site.labels.relatedTopics}</div>
|
package/src/components/Docs.tsx
CHANGED
|
@@ -60,7 +60,9 @@ export function Docs({ page, doc, site }: DocsProps) {
|
|
|
60
60
|
|
|
61
61
|
// API reference pages append their generated section anchors to the TOC.
|
|
62
62
|
const operation = getOperation(doc.frontmatter?.openapi);
|
|
63
|
-
const toc = operation
|
|
63
|
+
const toc = operation
|
|
64
|
+
? [...(doc.toc || []), ...operationSections(operation, site.labels)]
|
|
65
|
+
: doc.toc;
|
|
64
66
|
|
|
65
67
|
return (
|
|
66
68
|
<PanelProvider>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ConfiguredIcon } from '@/components/ConfiguredIcon';
|
|
2
|
+
import { useLabels } from '@/lib/label-context';
|
|
2
3
|
import type { NormalizedFooter } from '@/lib/types';
|
|
3
4
|
|
|
4
5
|
export function Footer({
|
|
@@ -8,6 +9,7 @@ export function Footer({
|
|
|
8
9
|
footer: NormalizedFooter | null;
|
|
9
10
|
className?: string;
|
|
10
11
|
}) {
|
|
12
|
+
const labels = useLabels();
|
|
11
13
|
if (!footer) {
|
|
12
14
|
return null;
|
|
13
15
|
}
|
|
@@ -48,7 +50,7 @@ export function Footer({
|
|
|
48
50
|
href="https://shiso.umami.is?ref=docs-footer"
|
|
49
51
|
className="text-sm hover:text-foreground"
|
|
50
52
|
>
|
|
51
|
-
|
|
53
|
+
{labels.poweredBy} <span className="font-bold">shiso</span>
|
|
52
54
|
</a>
|
|
53
55
|
) : (
|
|
54
56
|
<span />
|
|
@@ -6,7 +6,7 @@ import { ThemeToggle } from '@/components/ThemeToggle';
|
|
|
6
6
|
import { TopNav } from '@/components/TopNav';
|
|
7
7
|
import { VersionSwitcher } from '@/components/VersionSwitcher';
|
|
8
8
|
import { isExternalHref } from '@/lib/paths';
|
|
9
|
-
import {
|
|
9
|
+
import { getHomeHref, getScopeByPathname } from '@/lib/site-config';
|
|
10
10
|
import type { NormalizedLink, SiteModel } from '@/lib/types';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -62,8 +62,9 @@ export function Header({ site }: { site: SiteModel }) {
|
|
|
62
62
|
const { pathname } = useLocation();
|
|
63
63
|
// The header renders the navigation of whichever scope owns the current page.
|
|
64
64
|
const docs = getScopeByPathname(pathname).docs;
|
|
65
|
-
// The brand links to the standalone home page
|
|
66
|
-
|
|
65
|
+
// The brand links to the standalone home page in the current language when
|
|
66
|
+
// one owns "/", else the docs home.
|
|
67
|
+
const brandHref = logo?.href || getHomeHref(pathname);
|
|
67
68
|
const hasBrand = !!name || !!logo?.light || !!logo?.dark;
|
|
68
69
|
const brandClassName =
|
|
69
70
|
'inline-flex items-center gap-2 text-xl font-bold text-foreground tracking-[-0.03em]';
|
|
@@ -8,19 +8,29 @@ import {
|
|
|
8
8
|
DropdownMenuTrigger,
|
|
9
9
|
} from '@/components/ui/dropdown-menu';
|
|
10
10
|
import { getLanguageScopes } from '@/lib/docs-config';
|
|
11
|
-
import {
|
|
11
|
+
import { getLanguageName, isValidLocale } from '@/lib/locale';
|
|
12
|
+
import {
|
|
13
|
+
docsSite,
|
|
14
|
+
getScopeByPathname,
|
|
15
|
+
getStandaloneCounterpart,
|
|
16
|
+
getStandalonePage,
|
|
17
|
+
} from '@/lib/site-config';
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Language selector for multi-language sites. Each option is a language's
|
|
15
|
-
* landing scope — its default version — so switching languages
|
|
16
|
-
*
|
|
17
|
-
*
|
|
21
|
+
* landing scope — its default version — so switching languages lands on that
|
|
22
|
+
* language's default-version first page. On a standalone page (e.g. a home
|
|
23
|
+
* page) it lands on the same page in the chosen language when one exists.
|
|
24
|
+
* Hidden languages never
|
|
25
|
+
* appear as options. Each language is shown by its native name, e.g. "ja"
|
|
26
|
+
* as "日本語".
|
|
18
27
|
*/
|
|
19
28
|
export function LanguageSwitcher() {
|
|
20
29
|
const { pathname } = useLocation();
|
|
21
30
|
const navigate = useNavigate();
|
|
22
31
|
const current = getScopeByPathname(pathname);
|
|
23
32
|
const options = getLanguageScopes(docsSite);
|
|
33
|
+
const standalone = getStandalonePage(pathname);
|
|
24
34
|
|
|
25
35
|
if (!current.language || (options.length < 2 && !current.hidden)) {
|
|
26
36
|
return null;
|
|
@@ -36,7 +46,7 @@ export function LanguageSwitcher() {
|
|
|
36
46
|
/>
|
|
37
47
|
}
|
|
38
48
|
>
|
|
39
|
-
{current.language}
|
|
49
|
+
{getLanguageName(current.language)}
|
|
40
50
|
<ChevronRight className="size-3.5 rotate-90 text-muted-foreground" />
|
|
41
51
|
</DropdownMenuTrigger>
|
|
42
52
|
<DropdownMenuContent align="start" className="min-w-32">
|
|
@@ -45,11 +55,21 @@ export function LanguageSwitcher() {
|
|
|
45
55
|
key={scope.id}
|
|
46
56
|
onClick={() => {
|
|
47
57
|
if (scope.language !== current.language) {
|
|
48
|
-
|
|
58
|
+
const counterpart =
|
|
59
|
+
standalone && scope.language
|
|
60
|
+
? getStandaloneCounterpart(standalone, scope.language)
|
|
61
|
+
: null;
|
|
62
|
+
|
|
63
|
+
navigate(counterpart?.path || scope.firstPageUrl);
|
|
49
64
|
}
|
|
50
65
|
}}
|
|
51
66
|
>
|
|
52
|
-
<span
|
|
67
|
+
<span
|
|
68
|
+
className="grow"
|
|
69
|
+
lang={isValidLocale(scope.language) ? scope.language : undefined}
|
|
70
|
+
>
|
|
71
|
+
{scope.language ? getLanguageName(scope.language) : null}
|
|
72
|
+
</span>
|
|
53
73
|
{scope.language === current.language ? <Check className="size-3.5" /> : null}
|
|
54
74
|
</DropdownMenuItem>
|
|
55
75
|
))}
|
|
@@ -3,16 +3,18 @@ import { Badge } from '@/components/docs/Badge';
|
|
|
3
3
|
import { CodeGroup } from '@/components/docs/CodeGroup';
|
|
4
4
|
import { Expandable } from '@/components/docs/Expandable';
|
|
5
5
|
import { PropertiesTable } from '@/components/docs/PropertiesTable';
|
|
6
|
+
import { useLabels } from '@/lib/label-context';
|
|
6
7
|
import { operationParameterSections, operationSections, statusColor } from '@/lib/openapi';
|
|
7
|
-
import type { NormalizedOperation, SchemaNode } from '@/lib/types';
|
|
8
|
+
import type { NormalizedOperation, SchemaNode, ThemeLabels } from '@/lib/types';
|
|
8
9
|
|
|
9
10
|
function FieldChildren({ node }: { node: SchemaNode }) {
|
|
11
|
+
const labels = useLabels();
|
|
10
12
|
return (
|
|
11
13
|
<>
|
|
12
14
|
{node.description}
|
|
13
15
|
{node.enum && node.enum.length > 0 && (
|
|
14
16
|
<div className="mt-1">
|
|
15
|
-
|
|
17
|
+
{labels.fieldOptions}{' '}
|
|
16
18
|
{node.enum.map((value, index) => (
|
|
17
19
|
<span key={value}>
|
|
18
20
|
{index > 0 && ', '}
|
|
@@ -25,12 +27,14 @@ function FieldChildren({ node }: { node: SchemaNode }) {
|
|
|
25
27
|
);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
function schemaDetails(node: SchemaNode) {
|
|
30
|
+
function schemaDetails(node: SchemaNode, labels: ThemeLabels) {
|
|
29
31
|
if (!node.children?.length) return undefined;
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
34
|
<Expandable
|
|
33
|
-
title={
|
|
35
|
+
title={
|
|
36
|
+
node.type === 'oneOf' || node.type === 'anyOf' ? labels.allowedTypes : labels.properties
|
|
37
|
+
}
|
|
34
38
|
>
|
|
35
39
|
<SchemaTable nodes={node.children} />
|
|
36
40
|
</Expandable>
|
|
@@ -38,6 +42,7 @@ function schemaDetails(node: SchemaNode) {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
function SchemaTable({ nodes }: { nodes: SchemaNode[] }) {
|
|
45
|
+
const labels = useLabels();
|
|
41
46
|
return (
|
|
42
47
|
<PropertiesTable>
|
|
43
48
|
{nodes.map(node => (
|
|
@@ -48,7 +53,7 @@ function SchemaTable({ nodes }: { nodes: SchemaNode[] }) {
|
|
|
48
53
|
required={node.required}
|
|
49
54
|
deprecated={node.deprecated}
|
|
50
55
|
default={node.default}
|
|
51
|
-
details={schemaDetails(node)}
|
|
56
|
+
details={schemaDetails(node, labels)}
|
|
52
57
|
>
|
|
53
58
|
<FieldChildren node={node} />
|
|
54
59
|
</PropertiesTable.Row>
|
|
@@ -105,17 +110,19 @@ export interface OpenApiOperationProps {
|
|
|
105
110
|
|
|
106
111
|
/** The generated reference for one API operation, rendered under the page body. */
|
|
107
112
|
export function OpenApiOperation({ operation }: OpenApiOperationProps) {
|
|
108
|
-
const
|
|
113
|
+
const labels = useLabels();
|
|
114
|
+
const sectionEntries = operationSections(operation);
|
|
115
|
+
const sections = new Map(sectionEntries.map(entry => [entry.name, entry.id]));
|
|
109
116
|
|
|
110
117
|
return (
|
|
111
118
|
<div className="docs-markdown">
|
|
112
|
-
{operationParameterSections(operation).map(({ location, name }) => (
|
|
119
|
+
{operationParameterSections(operation, labels).map(({ location, name }, index) => (
|
|
113
120
|
<section key={location}>
|
|
114
|
-
<h2 id={
|
|
121
|
+
<h2 id={sectionEntries[index].id}>{name}</h2>
|
|
115
122
|
<PropertiesTable>
|
|
116
123
|
{location === 'header' && operation.security.length > 0 && (
|
|
117
124
|
<PropertiesTable.Row name="Authorization" type="string" required>
|
|
118
|
-
|
|
125
|
+
{labels.apiCredentials} <code>Bearer <token></code>.
|
|
119
126
|
</PropertiesTable.Row>
|
|
120
127
|
)}
|
|
121
128
|
{operation.parameters[location].map(parameter => (
|
|
@@ -126,7 +133,7 @@ export function OpenApiOperation({ operation }: OpenApiOperationProps) {
|
|
|
126
133
|
required={parameter.required}
|
|
127
134
|
deprecated={parameter.deprecated}
|
|
128
135
|
default={parameter.default}
|
|
129
|
-
details={schemaDetails(parameter)}
|
|
136
|
+
details={schemaDetails(parameter, labels)}
|
|
130
137
|
>
|
|
131
138
|
<FieldChildren node={parameter} />
|
|
132
139
|
</PropertiesTable.Row>
|
|
@@ -136,12 +143,12 @@ export function OpenApiOperation({ operation }: OpenApiOperationProps) {
|
|
|
136
143
|
))}
|
|
137
144
|
{operation.requestBody && (
|
|
138
145
|
<section>
|
|
139
|
-
<h2 id={sections.get('Request body')}>
|
|
146
|
+
<h2 id={sections.get('Request body')}>{labels.apiRequestBody}</h2>
|
|
140
147
|
<SchemaFields node={operation.requestBody.schema} />
|
|
141
148
|
{operation.requestBody.example && (
|
|
142
149
|
<HighlightedCode
|
|
143
150
|
language="json"
|
|
144
|
-
title=
|
|
151
|
+
title={labels.apiExampleRequest}
|
|
145
152
|
html={operation.requestBody.exampleHtml}
|
|
146
153
|
source={operation.requestBody.example}
|
|
147
154
|
/>
|
|
@@ -150,7 +157,7 @@ export function OpenApiOperation({ operation }: OpenApiOperationProps) {
|
|
|
150
157
|
)}
|
|
151
158
|
{operation.responses.length > 0 && (
|
|
152
159
|
<section>
|
|
153
|
-
<h2 id={sections.get('Responses')}>
|
|
160
|
+
<h2 id={sections.get('Responses')}>{labels.apiResponses}</h2>
|
|
154
161
|
{operation.responses.map(response => (
|
|
155
162
|
<div key={response.status} className="mt-6 first:mt-0">
|
|
156
163
|
<div className="flex items-center gap-2">
|
|
@@ -165,7 +172,7 @@ export function OpenApiOperation({ operation }: OpenApiOperationProps) {
|
|
|
165
172
|
{response.example && (
|
|
166
173
|
<HighlightedCode
|
|
167
174
|
language="json"
|
|
168
|
-
title={
|
|
175
|
+
title={labels.apiExampleResponse.replace('{status}', () => response.status)}
|
|
169
176
|
html={response.exampleHtml}
|
|
170
177
|
source={response.example}
|
|
171
178
|
/>
|
|
@@ -176,7 +183,7 @@ export function OpenApiOperation({ operation }: OpenApiOperationProps) {
|
|
|
176
183
|
)}
|
|
177
184
|
{operation.samples.length > 0 && (
|
|
178
185
|
<section>
|
|
179
|
-
<h2 id={sections.get('Code samples')}>
|
|
186
|
+
<h2 id={sections.get('Code samples')}>{labels.apiCodeSamples}</h2>
|
|
180
187
|
<CodeGroup>
|
|
181
188
|
{operation.samples.map(sample => (
|
|
182
189
|
<HighlightedCode
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { ExternalLink } from '@/components/icons';
|
|
3
|
+
import { toHref } from '@/lib/paths';
|
|
4
|
+
import type { DocFrontmatter, NormalizedDocsPage, SiteModel } from '@/lib/types';
|
|
5
|
+
|
|
6
|
+
function editHref(template: string | undefined, filePath: string): string | undefined {
|
|
7
|
+
if (!template) return undefined;
|
|
8
|
+
const file = filePath.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/');
|
|
9
|
+
const href = template.replaceAll('$file', file);
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(href);
|
|
12
|
+
if (url.protocol === 'https:' || url.protocol === 'http:') return url.href;
|
|
13
|
+
} catch {
|
|
14
|
+
// Invalid frontmatter URLs should never become executable links.
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function PageActions({
|
|
20
|
+
page,
|
|
21
|
+
frontmatter,
|
|
22
|
+
site,
|
|
23
|
+
lastUpdated,
|
|
24
|
+
}: {
|
|
25
|
+
page: NormalizedDocsPage;
|
|
26
|
+
frontmatter?: DocFrontmatter;
|
|
27
|
+
site: SiteModel;
|
|
28
|
+
lastUpdated?: ReactNode;
|
|
29
|
+
}) {
|
|
30
|
+
const [status, setStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');
|
|
31
|
+
const [rating, setRating] = useState<boolean>();
|
|
32
|
+
const request = useRef<AbortController | null>(null);
|
|
33
|
+
useEffect(() => () => request.current?.abort(), []);
|
|
34
|
+
|
|
35
|
+
const feedback = frontmatter?.feedback === false ? null : site.feedback;
|
|
36
|
+
const href =
|
|
37
|
+
frontmatter?.editLink === false
|
|
38
|
+
? undefined
|
|
39
|
+
: editHref(
|
|
40
|
+
typeof frontmatter?.editLink === 'string' ? frontmatter.editLink : site.editLink?.url,
|
|
41
|
+
page.filePath,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
async function submit(helpful: boolean) {
|
|
45
|
+
if (!feedback || request.current || status === 'success') return;
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
request.current = controller;
|
|
48
|
+
setRating(helpful);
|
|
49
|
+
setStatus('pending');
|
|
50
|
+
const timeout = window.setTimeout(() => controller.abort(), 15000);
|
|
51
|
+
try {
|
|
52
|
+
const endpoint = new URL(toHref(feedback.endpoint), window.location.origin);
|
|
53
|
+
if (!['https:', 'http:'].includes(endpoint.protocol)) throw new Error('Invalid endpoint');
|
|
54
|
+
const response = await fetch(endpoint.href, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
helpful,
|
|
59
|
+
path: toHref(page.url),
|
|
60
|
+
title: frontmatter?.title || page.label,
|
|
61
|
+
language: page.language,
|
|
62
|
+
version: page.version,
|
|
63
|
+
}),
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) throw new Error('Feedback submission failed');
|
|
67
|
+
setStatus('success');
|
|
68
|
+
} catch {
|
|
69
|
+
setStatus('error');
|
|
70
|
+
} finally {
|
|
71
|
+
window.clearTimeout(timeout);
|
|
72
|
+
request.current = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!href && !feedback && !lastUpdated) return null;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
{(lastUpdated || href) && (
|
|
81
|
+
<div
|
|
82
|
+
className="mt-8 flex items-baseline gap-4 text-sm text-muted-foreground"
|
|
83
|
+
data-pagefind-ignore
|
|
84
|
+
>
|
|
85
|
+
{lastUpdated && <div className="min-w-0">{lastUpdated}</div>}
|
|
86
|
+
{href && (
|
|
87
|
+
<a
|
|
88
|
+
href={href}
|
|
89
|
+
target="_blank"
|
|
90
|
+
rel="noopener noreferrer"
|
|
91
|
+
className="ml-auto inline-flex shrink-0 items-center gap-1.5 text-right no-underline hover:text-primary"
|
|
92
|
+
>
|
|
93
|
+
{site.editLink?.label || site.labels.editPage}
|
|
94
|
+
<ExternalLink size={14} aria-hidden="true" />
|
|
95
|
+
</a>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
{feedback && (
|
|
100
|
+
<div
|
|
101
|
+
className="mt-8 flex flex-col gap-2 border-t border-border pt-6 text-sm"
|
|
102
|
+
aria-busy={status === 'pending'}
|
|
103
|
+
data-pagefind-ignore
|
|
104
|
+
>
|
|
105
|
+
<fieldset
|
|
106
|
+
aria-label={feedback.prompt || site.labels.feedbackPrompt}
|
|
107
|
+
className="flex flex-wrap items-center gap-2"
|
|
108
|
+
>
|
|
109
|
+
<span className="mr-2 text-muted-foreground">
|
|
110
|
+
{feedback.prompt || site.labels.feedbackPrompt}
|
|
111
|
+
</span>
|
|
112
|
+
{[true, false].map(helpful => (
|
|
113
|
+
<button
|
|
114
|
+
key={String(helpful)}
|
|
115
|
+
type="button"
|
|
116
|
+
aria-pressed={rating === helpful}
|
|
117
|
+
disabled={status === 'pending' || status === 'success'}
|
|
118
|
+
onClick={() => void submit(helpful)}
|
|
119
|
+
className="rounded-md border border-border px-3 py-1.5 text-foreground hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-default disabled:opacity-60 aria-pressed:border-primary aria-pressed:text-primary"
|
|
120
|
+
>
|
|
121
|
+
{helpful
|
|
122
|
+
? feedback.helpfulLabel || site.labels.feedbackYes
|
|
123
|
+
: feedback.unhelpfulLabel || site.labels.feedbackNo}
|
|
124
|
+
</button>
|
|
125
|
+
))}
|
|
126
|
+
</fieldset>
|
|
127
|
+
<div role="status" aria-live="polite" className="text-muted-foreground">
|
|
128
|
+
{status === 'success' && (feedback.successMessage || site.labels.feedbackSuccess)}
|
|
129
|
+
{status === 'error' && (feedback.errorMessage || site.labels.feedbackError)}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</>
|
|
134
|
+
);
|
|
135
|
+
}
|