@umami/shiso 1.12.0 → 1.13.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.
Files changed (121) hide show
  1. package/dist/chunks/App.js +76 -49
  2. package/dist/chunks/abnfDiagram-VCTEODGH.js +109 -0
  3. package/dist/chunks/arc.js +130 -0
  4. package/dist/chunks/architecture-7GRP2DOG.js +4 -0
  5. package/dist/chunks/architectureDiagram-5GKGNRK7.js +7466 -0
  6. package/dist/chunks/array.js +8 -0
  7. package/dist/chunks/blockDiagram-I7D4REHJ.js +2546 -0
  8. package/dist/chunks/c4Diagram-7LVT6UL2.js +3654 -0
  9. package/dist/chunks/channel.js +9 -0
  10. package/dist/chunks/chunk-2Q5K7J3B.js +21 -0
  11. package/dist/chunks/chunk-4HAMMTFA.js +6106 -0
  12. package/dist/chunks/chunk-5VM5RSS4.js +21 -0
  13. package/dist/chunks/chunk-75Z2AOVW.js +2185 -0
  14. package/dist/chunks/chunk-DU6HZSFF.js +8729 -0
  15. package/dist/chunks/chunk-F27PBJKO.js +95 -0
  16. package/dist/chunks/chunk-FOHPRMQF.js +27162 -0
  17. package/dist/chunks/chunk-GMAD6QVW.js +2369 -0
  18. package/dist/chunks/chunk-GVQU2GXP.js +58 -0
  19. package/dist/chunks/chunk-IMKFNOWR.js +2593 -0
  20. package/dist/chunks/chunk-JWPE2WC7.js +12 -0
  21. package/dist/chunks/chunk-L3NEJ4N5.js +420 -0
  22. package/dist/chunks/chunk-OSK3NFVY.js +1029 -0
  23. package/dist/chunks/chunk-P2QGCYS3.js +95 -0
  24. package/dist/chunks/chunk-POPQ4Y6H.js +33 -0
  25. package/dist/chunks/chunk-PWAF6VOD.js +467 -0
  26. package/dist/chunks/chunk-SHT3W25Y.js +4813 -0
  27. package/dist/chunks/chunk-SVP7TREG.js +686 -0
  28. package/dist/chunks/chunk-TICWLB2K.js +3800 -0
  29. package/dist/chunks/chunk-XXDRQBXY.js +12 -0
  30. package/dist/chunks/chunk-Y2CYZVJY.js +15 -0
  31. package/dist/chunks/classDiagram-ZZMXUADV.js +33 -0
  32. package/dist/chunks/classDiagram-v2-VYDZK3BY.js +33 -0
  33. package/dist/chunks/cose-bilkent-JH36ORCC.js +4268 -0
  34. package/dist/chunks/cynefin-OW5HDTMX.js +4 -0
  35. package/dist/chunks/cynefinDiagram-5FMLGOSQ.js +482 -0
  36. package/dist/chunks/cytoscape.esm.js +28282 -0
  37. package/dist/chunks/dagre-GXQ25YYZ.js +583 -0
  38. package/dist/chunks/dagre.js +4235 -0
  39. package/dist/chunks/defaultLocale.js +246 -0
  40. package/dist/chunks/diagram-S7CK7UJ4.js +402 -0
  41. package/dist/chunks/diagram-UQ7AKVKN.js +291 -0
  42. package/dist/chunks/diagram-VSXAHHWV.js +553 -0
  43. package/dist/chunks/diagram-VX7I27RA.js +798 -0
  44. package/dist/chunks/diagram-Z3DM3KII.js +186 -0
  45. package/dist/chunks/dist.js +92 -0
  46. package/dist/chunks/docs.js +987 -91
  47. package/dist/chunks/ebnfDiagram-PWID7BFC.js +123 -0
  48. package/dist/chunks/erDiagram-RLTQ6QDP.js +2295 -0
  49. package/dist/chunks/eventmodeling-NTZA5JFV.js +4 -0
  50. package/dist/chunks/flowDiagram-HODETNUW.js +17 -0
  51. package/dist/chunks/ganttDiagram-EL5Y4UJY.js +3928 -0
  52. package/dist/chunks/gitGraph-4MIJSDKK.js +4 -0
  53. package/dist/chunks/gitGraphDiagram-WWUBYQGX.js +1153 -0
  54. package/dist/chunks/graphlib.js +4194 -0
  55. package/dist/chunks/info-A6RAGUB7.js +4 -0
  56. package/dist/chunks/infoDiagram-27XIBGKW.js +25 -0
  57. package/dist/chunks/init.js +14 -0
  58. package/dist/chunks/ishikawaDiagram-5VMMS53U.js +971 -0
  59. package/dist/chunks/journeyDiagram-3NMN7TZE.js +1226 -0
  60. package/dist/chunks/kanban-definition-UXKFOSKX.js +1241 -0
  61. package/dist/chunks/katex.js +26463 -0
  62. package/dist/chunks/line.js +48 -0
  63. package/dist/chunks/linear.js +391 -0
  64. package/dist/chunks/mermaid-parser.core.js +754 -0
  65. package/dist/chunks/mermaid.core.js +4898 -0
  66. package/dist/chunks/mindmap-definition-YA3MSWOX.js +1309 -0
  67. package/dist/chunks/ordinal.js +84 -0
  68. package/dist/chunks/packet-AYTQ26CC.js +4 -0
  69. package/dist/chunks/path.js +106 -0
  70. package/dist/chunks/pegDiagram-XKGWAZYB.js +115 -0
  71. package/dist/chunks/pie-WAS4IAKB.js +4 -0
  72. package/dist/chunks/pieDiagram-E7YTZNPT.js +297 -0
  73. package/dist/chunks/quadrantDiagram-AXDQQJYC.js +2234 -0
  74. package/dist/chunks/radar-RG4KPBEZ.js +4 -0
  75. package/dist/chunks/railroad-74A4TZTK.js +4 -0
  76. package/dist/chunks/railroad-abnf-HS5TGJTU.js +4 -0
  77. package/dist/chunks/railroad-ebnf-LZEXJU2U.js +4 -0
  78. package/dist/chunks/railroad-peg-WCYAUIDC.js +4 -0
  79. package/dist/chunks/railroadDiagram-O6MQD6OU.js +89 -0
  80. package/dist/chunks/requirementDiagram-BXWQKSXE.js +2464 -0
  81. package/dist/chunks/rough.esm.js +1396 -0
  82. package/dist/chunks/sankeyDiagram-P5KCCOFB.js +1308 -0
  83. package/dist/chunks/sequenceDiagram-WJ2MYXX4.js +5445 -0
  84. package/dist/chunks/sizeCapture-INFHLROL.js +56 -0
  85. package/dist/chunks/src.js +2653 -0
  86. package/dist/chunks/stateDiagram-D77RDMKH.js +368 -0
  87. package/dist/chunks/stateDiagram-v2-MP3YSRHH.js +33 -0
  88. package/dist/chunks/swimlanes-42K2YHIH.js +6925 -0
  89. package/dist/chunks/swimlanesDiagram-VR7AAH4N.js +32 -0
  90. package/dist/chunks/timeline-definition-24CTP7MA.js +1507 -0
  91. package/dist/chunks/treeView-Q6P3EWNA.js +4 -0
  92. package/dist/chunks/treemap-WGGIJYW6.js +4 -0
  93. package/dist/chunks/vennDiagram-4TSXK5OY.js +2803 -0
  94. package/dist/chunks/wardley-WFR3VGLG.js +4 -0
  95. package/dist/chunks/wardleyDiagram-VM6X3IG4.js +877 -0
  96. package/dist/chunks/xychartDiagram-S5SC5T6Z.js +2698 -0
  97. package/dist/components.js +2 -2
  98. package/dist/entry-client.js +1 -1
  99. package/dist/entry-server.js +1 -1
  100. package/docs.schema.json +1 -1
  101. package/package.json +2 -1
  102. package/scripts/expand-navigation-globs.mjs +77 -8
  103. package/src/components/CodeBlock.tsx +36 -0
  104. package/src/components/Docs.tsx +44 -21
  105. package/src/components/OpenApiOperation.tsx +28 -20
  106. package/src/components/SideNav.tsx +8 -3
  107. package/src/components/docs/Changelog.tsx +163 -0
  108. package/src/components/docs/Mermaid.tsx +164 -0
  109. package/src/components/docs/Panel.tsx +23 -0
  110. package/src/components/docs/ParamField.tsx +17 -5
  111. package/src/components/docs/PropertiesTable.tsx +2 -2
  112. package/src/components/docs/ResponseField.tsx +17 -5
  113. package/src/components/docs/Tiles.tsx +59 -0
  114. package/src/components/docs/Tree.tsx +359 -0
  115. package/src/components/docs/Update.tsx +72 -0
  116. package/src/components/docs/index.ts +7 -0
  117. package/src/components/docs/panel-context.tsx +25 -0
  118. package/src/components/docs/styles.ts +49 -0
  119. package/src/components/icons/index.ts +4 -0
  120. package/src/lib/rehype-shiki.ts +18 -1
  121. package/types/components.d.ts +79 -0
@@ -1,3 +1,3 @@
1
- import { At as Danger, Bt as AccordionGroup, Dt as CardGroup, Et as Card, Ft as WarningBanner, It as Button, Lt as Badge, Mt as Note, Nt as Tip, Ot as Callout, Pt as Warning, St as CodeGroup, _ as Tooltip, at as Tabs, bt as Column, ct as ResponseField, dt as ParamField, ft as Link, gt as Expandable, it as Tab, jt as Info, kt as Check, lt as PropertiesTable, mt as Frame, n as ZoomableImage, ot as Step, pt as Icon, st as Steps, ut as Param, xt as Columns, zt as Accordion } from "./chunks/docs.js";
1
+ import { $t as Accordion, Bt as Card, C as Tooltip, Ct as useSetPanelContent, Et as Frame, Gt as Info, Ht as Callout, Jt as Warning, Kt as Note, Mt as Column, Nt as Columns, Ot as Expandable, Pt as CodeGroup, Rt as Changelog, S as TreeFolder, Sn as mermaidSource, St as usePanelContent, Tt as Icon, Ut as Check, Vt as CardGroup, Wt as Danger, Xt as Button, Yt as WarningBanner, Zt as Badge, _ as FileTree, _t as PropertiesTable, b as Tree, bt as Panel, dt as Tiles, en as AccordionGroup, ft as Tab, gt as ResponseField, ht as Steps, mt as Step, n as ZoomableImage, pt as Tabs, qt as Tip, ut as Tile, v as FileTreeFile, vt as Param, wt as Link, x as TreeFile, xn as Mermaid, xt as PanelProvider, y as FileTreeFolder, yt as ParamField, zt as Update } from "./chunks/docs.js";
2
2
 
3
- export { Accordion, AccordionGroup, Badge, Button, Callout, Card, CardGroup, Check, CodeGroup, Column, Columns, Danger, Expandable, Frame, Icon, Info, Link, Note, Param, ParamField, PropertiesTable, ResponseField, Step, Steps, Tab, Tabs, Tip, Tooltip, Warning, WarningBanner, ZoomableImage };
3
+ export { Accordion, AccordionGroup, Badge, Button, Callout, Card, CardGroup, Changelog, Check, CodeGroup, Column, Columns, Danger, Expandable, FileTree, FileTreeFile, FileTreeFolder, Frame, Icon, Info, Link, Mermaid, Note, Panel, PanelProvider, Param, ParamField, PropertiesTable, ResponseField, Step, Steps, Tab, Tabs, Tile, Tiles, Tip, Tooltip, Tree, TreeFile, TreeFolder, Update, Warning, WarningBanner, ZoomableImage, mermaidSource, usePanelContent, useSetPanelContent };
@@ -1,4 +1,4 @@
1
- import { ar as BrowserRouter } from "./chunks/docs.js";
1
+ import { Sr as BrowserRouter } from "./chunks/docs.js";
2
2
  import { h as BASE_URL, t as App } from "./chunks/App.js";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
 
@@ -1,4 +1,4 @@
1
- import { hr as ABSOLUTE_URL_REGEX, lr as Router, mr as parsePath, pr as createPath } from "./chunks/docs.js";
1
+ import { Ar as createPath, Er as Router, Mr as ABSOLUTE_URL_REGEX, jr as parsePath } from "./chunks/docs.js";
2
2
  import { a as docsSite, c as getSeo, d as standalonePages, f as getDocModule, g as toAbsoluteUrl, h as BASE_URL, i as docsHomeUrl, l as siteConfig, m as getScopeForPage, n as buildHead, o as getLocaleByPathname, p as getLastModified, r as renderHeadToString, s as getRedirects, t as App, u as siteName } from "./chunks/App.js";
3
3
  import * as React$1 from "react";
4
4
  import { jsx } from "react/jsx-runtime";
package/docs.schema.json CHANGED
@@ -807,7 +807,7 @@
807
807
  "properties": {
808
808
  "glob": {
809
809
  "allOf": [{ "$ref": "#/definitions/nonEmptyStringValue" }],
810
- "description": "Glob relative to contentDir. Matching Markdown and MDX files are expanded into page entries."
810
+ "description": "Glob relative to contentDir. Matching Markdown and MDX files are expanded into page entries; explicit page references take precedence."
811
811
  },
812
812
  "exclude": {
813
813
  "anyOf": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umami/shiso",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Open-source documentation framework for Markdown and MDX sites.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -83,6 +83,7 @@
83
83
  "github-slugger": "^2.0.0",
84
84
  "jiti": "^2.4.0",
85
85
  "lucide-react": "^1.28.0",
86
+ "mermaid": "^11.17.2",
86
87
  "react-router": "^8.3.0",
87
88
  "rehype-autolink-headings": "^7.1.0",
88
89
  "rehype-slug": "^6.0.0",
@@ -111,12 +111,47 @@ function pageCandidate(contentRoot, filePath) {
111
111
  return { filePath, relativeFile, fileSlug };
112
112
  }
113
113
 
114
+ function normalizePageReference(value) {
115
+ return String(value || '')
116
+ .trim()
117
+ .replace(/\\/g, '/')
118
+ .replace(/^\/+/, '')
119
+ .replace(/^docs\//, '')
120
+ .replace(/\.mdx?$/i, '')
121
+ .replace(/\/+$/, '');
122
+ }
123
+
124
+ /** Collects manual page references without crossing a version/language scope boundary. */
125
+ function collectExplicitPages(value, pages = new Set()) {
126
+ if (Array.isArray(value)) {
127
+ for (const item of value) {
128
+ if (typeof item === 'string') {
129
+ pages.add(normalizePageReference(item));
130
+ } else {
131
+ collectExplicitPages(item, pages);
132
+ }
133
+ }
134
+ return pages;
135
+ }
136
+
137
+ if (!value || typeof value !== 'object' || isGlobItem(value)) return pages;
138
+
139
+ if (typeof value.page === 'string') pages.add(normalizePageReference(value.page));
140
+ if (typeof value.root === 'string') pages.add(normalizePageReference(value.root));
141
+
142
+ for (const key of ['pages', 'groups', 'tabs', 'dropdowns']) {
143
+ if (Array.isArray(value[key])) collectExplicitPages(value[key], pages);
144
+ }
145
+
146
+ return pages;
147
+ }
148
+
114
149
  function matches(candidate, pattern) {
115
150
  const target = /\.mdx?$/i.test(pattern) ? candidate.relativeFile : candidate.fileSlug;
116
151
  return globRegex(pattern).test(target);
117
152
  }
118
153
 
119
- async function expandGlob(item, candidates, projectRoot) {
154
+ async function expandGlob(item, candidates, projectRoot, explicitPages) {
120
155
  assertGlobItem(item);
121
156
  const pattern = globPattern(item.glob);
122
157
  const exclusions = (item.exclude || []).map(globPattern);
@@ -129,8 +164,12 @@ async function expandGlob(item, candidates, projectRoot) {
129
164
  throw new Error(`Navigation glob "${pattern}" matched no Markdown or MDX files.`);
130
165
  }
131
166
 
167
+ // Manual entries own their page's placement and presentation, regardless of
168
+ // whether they appear before or after the glob that would otherwise include it.
169
+ const discovered = matched.filter(candidate => !explicitPages.has(candidate.fileSlug));
170
+
132
171
  const entries = await Promise.all(
133
- matched.map(async candidate => {
172
+ discovered.map(async candidate => {
134
173
  const source = await fs.readFile(candidate.filePath, 'utf8');
135
174
  const sourcePath = path.relative(projectRoot, candidate.filePath).replace(/\\/g, '/');
136
175
  const frontmatter = navigationFrontmatter(source, sourcePath);
@@ -181,8 +220,10 @@ export async function expandNavigationGlobs(config, { root, contentDir }) {
181
220
  );
182
221
  }
183
222
 
184
- async function expandObject(value) {
185
- if (Array.isArray(value)) return Promise.all(value.map(expandObject));
223
+ async function expandObject(value, explicitPages) {
224
+ if (Array.isArray(value)) {
225
+ return Promise.all(value.map(item => expandObject(item, explicitPages)));
226
+ }
186
227
  if (!value || typeof value !== 'object') return value;
187
228
 
188
229
  const entries = await Promise.all(
@@ -191,18 +232,46 @@ export async function expandNavigationGlobs(config, { root, contentDir }) {
191
232
  const expanded = [];
192
233
  for (const item of child) {
193
234
  if (isGlobItem(item)) {
194
- expanded.push(...(await expandGlob(item, candidates, projectRoot)));
235
+ expanded.push(...(await expandGlob(item, candidates, projectRoot, explicitPages)));
195
236
  } else {
196
- expanded.push(await expandObject(item));
237
+ expanded.push(await expandObject(item, explicitPages));
197
238
  }
198
239
  }
199
240
  return [key, expanded];
200
241
  }
201
- return [key, await expandObject(child)];
242
+ return [key, await expandObject(child, explicitPages)];
202
243
  }),
203
244
  );
204
245
  return Object.fromEntries(entries);
205
246
  }
206
247
 
207
- return { ...config, navigation: await expandObject(config.navigation) };
248
+ async function expandScope(scope) {
249
+ return expandObject(scope, collectExplicitPages(scope));
250
+ }
251
+
252
+ async function expandNavigation(navigation) {
253
+ if (Array.isArray(navigation.languages)) {
254
+ const languages = await Promise.all(
255
+ navigation.languages.map(async language => {
256
+ if (!Array.isArray(language.versions)) return expandScope(language);
257
+ return {
258
+ ...language,
259
+ versions: await Promise.all(language.versions.map(expandScope)),
260
+ };
261
+ }),
262
+ );
263
+ return { ...navigation, languages };
264
+ }
265
+
266
+ if (Array.isArray(navigation.versions)) {
267
+ return {
268
+ ...navigation,
269
+ versions: await Promise.all(navigation.versions.map(expandScope)),
270
+ };
271
+ }
272
+
273
+ return expandScope(navigation);
274
+ }
275
+
276
+ return { ...config, navigation: await expandNavigation(config.navigation) };
208
277
  }
@@ -1,8 +1,10 @@
1
+ import type { ReactNode } from 'react';
1
2
  import { type ComponentProps, type CSSProperties, useRef, useState } from 'react';
2
3
  import { CheckIcon, Copy } from '@/components/icons';
3
4
  import { Button } from '@/components/ui/button';
4
5
  import { ScrollArea } from '@/components/ui/scroll-area';
5
6
  import { cn } from '@/lib/utils';
7
+ import { Mermaid, type MermaidPlacement } from './docs/Mermaid';
6
8
 
7
9
  /**
8
10
  * Renders a fenced code block. The `data-*` props are produced at build time by
@@ -15,6 +17,25 @@ export interface CodeBlockProps extends ComponentProps<'pre'> {
15
17
  'data-line-start'?: string;
16
18
  'data-line-count'?: string;
17
19
  'data-diff-markers'?: string;
20
+ 'data-placement'?: MermaidPlacement;
21
+ 'data-actions'?: string;
22
+ }
23
+
24
+ function reactChildrenToText(node: ReactNode): string {
25
+ if (typeof node === 'string' || typeof node === 'number') {
26
+ return String(node);
27
+ }
28
+ if (Array.isArray(node)) {
29
+ return node.map(reactChildrenToText).join('');
30
+ }
31
+ if (node && typeof node === 'object' && 'props' in (node as object)) {
32
+ const props = (node as { props?: { children?: ReactNode; value?: unknown } }).props;
33
+ if (typeof props?.value === 'string') {
34
+ return props.value;
35
+ }
36
+ return reactChildrenToText(props?.children);
37
+ }
38
+ return '';
18
39
  }
19
40
 
20
41
  /**
@@ -44,11 +65,26 @@ export function CodeBlock({ children, className, style, ...rest }: CodeBlockProp
44
65
  'data-language': language,
45
66
  'data-line-start': lineStart,
46
67
  'data-line-count': lineCount,
68
+ 'data-placement': placement,
69
+ 'data-actions': actions,
47
70
  ...preProps
48
71
  } = rest;
49
72
  const textInput = useRef<HTMLPreElement>(null);
50
73
  const [copied, setCopied] = useState(false);
51
74
 
75
+ // ```mermaid fences render as diagrams; the raw definition stays in the
76
+ // markup for search indexing and no-JS fallbacks.
77
+ if (language === 'mermaid') {
78
+ return (
79
+ <Mermaid
80
+ chart={reactChildrenToText(children).replace(/\n$/, '')}
81
+ title={title}
82
+ placement={placement}
83
+ actions={actions === undefined ? undefined : actions !== 'false'}
84
+ />
85
+ );
86
+ }
87
+
52
88
  const start = Number(lineStart) || 1;
53
89
  const lastLine = start + Math.max(Number(lineCount) || 1, 1) - 1;
54
90
  const gutter = `${String(lastLine).length}ch`;
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useLocation, useNavigate } from 'react-router';
3
3
  import { DocContent } from '@/components/DocContent';
4
+ import { PanelProvider, usePanelContent } from '@/components/docs/panel-context';
4
5
  import { Footer } from '@/components/Footer';
5
6
  import { Menu } from '@/components/icons';
6
7
  import { LanguageSwitcher } from '@/components/LanguageSwitcher';
@@ -13,7 +14,7 @@ import { VersionSwitcher } from '@/components/VersionSwitcher';
13
14
  import { renderInlineMarkdown } from '@/lib/inline-markdown';
14
15
  import { getOperation, operationSections } from '@/lib/openapi';
15
16
  import { docsHomeUrl, getScopeByPathname } from '@/lib/site-config';
16
- import type { DocModule, NormalizedDocsPage, SiteModel } from '@/lib/types';
17
+ import type { DocModule, NormalizedDocsPage, SiteModel, TocEntry } from '@/lib/types';
17
18
 
18
19
  /**
19
20
  * 404 view driven by the `errors.404` config key. The standard defaults to
@@ -46,11 +47,46 @@ export interface DocsProps {
46
47
  }
47
48
 
48
49
  export function Docs({ page, doc, site }: DocsProps) {
50
+ if (!page || !doc) {
51
+ return (
52
+ <div className="flex min-h-full flex-col">
53
+ <div className="grow">
54
+ <NotFound site={site} />
55
+ </div>
56
+ <Footer footer={site.footer} />
57
+ </div>
58
+ );
59
+ }
60
+
61
+ // API reference pages append their generated section anchors to the TOC.
62
+ const operation = getOperation(doc.frontmatter?.openapi);
63
+ const toc = operation ? [...(doc.toc || []), ...operationSections(operation)] : doc.toc;
64
+
65
+ return (
66
+ <PanelProvider>
67
+ <DocsBody page={page} doc={doc} site={site} toc={toc} />
68
+ </PanelProvider>
69
+ );
70
+ }
71
+
72
+ function DocsBody({
73
+ page,
74
+ doc,
75
+ site,
76
+ toc,
77
+ }: {
78
+ page: NormalizedDocsPage;
79
+ doc: DocModule;
80
+ site: SiteModel;
81
+ toc: TocEntry[] | undefined;
82
+ }) {
49
83
  const { pathname } = useLocation();
50
84
  const [menuOpen, setMenuOpen] = useState(false);
51
85
  // Navigation follows the scope (version/language) that owns the current page.
52
86
  const scopeDocs = getScopeByPathname(pathname).docs;
53
87
  const { tabs, navigation } = scopeDocs;
88
+ // A <Panel> in the page replaces the table of contents in the right rail.
89
+ const panel = usePanelContent();
54
90
 
55
91
  // Close the mobile menu and start each newly loaded page at the top. Hash
56
92
  // links keep their native section-scrolling behavior.
@@ -62,21 +98,6 @@ export function Docs({ page, doc, site }: DocsProps) {
62
98
  }
63
99
  }, [pathname]);
64
100
 
65
- if (!page || !doc) {
66
- return (
67
- <div className="flex min-h-full flex-col">
68
- <div className="grow">
69
- <NotFound site={site} />
70
- </div>
71
- <Footer footer={site.footer} />
72
- </div>
73
- );
74
- }
75
-
76
- // API reference pages append their generated section anchors to the TOC.
77
- const operation = getOperation(doc.frontmatter?.openapi);
78
- const toc = operation ? [...(doc.toc || []), ...operationSections(operation)] : doc.toc;
79
-
80
101
  return (
81
102
  <div className="flex min-h-full flex-col gap-6 lg:gap-0">
82
103
  <Sheet open={menuOpen} onOpenChange={setMenuOpen}>
@@ -128,11 +149,13 @@ export function Docs({ page, doc, site }: DocsProps) {
128
149
  <div className="flex grow items-start gap-12">
129
150
  <DocContent page={page} doc={doc} site={site} />
130
151
  <div className="hidden min-w-0 max-w-60 basis-60 self-start lg:sticky lg:top-[calc(var(--header-height)+1.5rem)] lg:block lg:shrink-0">
131
- <PageLinks
132
- items={toc}
133
- title={site.labels.tableOfContents}
134
- navigationLabel={site.labels.tableOfContentsNavigation}
135
- />
152
+ {panel ?? (
153
+ <PageLinks
154
+ items={toc}
155
+ title={site.labels.tableOfContents}
156
+ navigationLabel={site.labels.tableOfContentsNavigation}
157
+ />
158
+ )}
136
159
  </div>
137
160
  </div>
138
161
  <Footer footer={site.footer} className="lg:mr-72" />
@@ -4,6 +4,7 @@ import { CodeGroup } from '@/components/docs/CodeGroup';
4
4
  import { Expandable } from '@/components/docs/Expandable';
5
5
  import { ParamField } from '@/components/docs/ParamField';
6
6
  import { ResponseField } from '@/components/docs/ResponseField';
7
+ import { styles } from '@/components/docs/styles';
7
8
  import { operationSections, statusColor } from '@/lib/openapi';
8
9
  import type { NormalizedOperation, SchemaNode } from '@/lib/types';
9
10
 
@@ -38,6 +39,8 @@ function FieldChildren({ node }: { node: SchemaNode }) {
38
39
  }
39
40
 
40
41
  function SchemaField({ node }: { node: SchemaNode }) {
42
+ const hasDetails = Boolean(node.description || node.enum?.length || node.children?.length);
43
+
41
44
  return (
42
45
  <ResponseField
43
46
  name={node.name || node.type}
@@ -46,7 +49,7 @@ function SchemaField({ node }: { node: SchemaNode }) {
46
49
  deprecated={node.deprecated}
47
50
  default={node.default}
48
51
  >
49
- <FieldChildren node={node} />
52
+ {hasDetails ? <FieldChildren node={node} /> : null}
50
53
  </ResponseField>
51
54
  );
52
55
  }
@@ -114,25 +117,26 @@ export function OpenApiOperation({ operation }: OpenApiOperationProps) {
114
117
  {showParameters && (
115
118
  <section>
116
119
  <h2 id={sections.get('Parameters')}>Parameters</h2>
117
- {operation.security.length > 0 && (
118
- <ParamField header="Authorization" type="string" required>
119
- Authentication credentials, e.g. <code>Bearer &lt;token&gt;</code> (
120
- {operation.security.join(', ')}).
121
- </ParamField>
122
- )}
123
- {PARAM_LOCATIONS.map(location =>
124
- operation.parameters[location].map(parameter => (
125
- <ParamField
126
- key={`${location}-${parameter.name}`}
127
- {...{ [location]: parameter.name }}
128
- type={parameter.type}
129
- required={parameter.required}
130
- >
131
- <FieldChildren node={{ ...parameter, name: undefined, description: undefined }} />
132
- {parameter.description}
120
+ <div className={styles.fieldGroup}>
121
+ {operation.security.length > 0 && (
122
+ <ParamField header="Authorization" type="string" required>
123
+ Authentication credentials, e.g. <code>Bearer &lt;token&gt;</code>.
133
124
  </ParamField>
134
- )),
135
- )}
125
+ )}
126
+ {PARAM_LOCATIONS.map(location =>
127
+ operation.parameters[location].map(parameter => (
128
+ <ParamField
129
+ key={`${location}-${parameter.name}`}
130
+ {...{ [location]: parameter.name }}
131
+ type={parameter.type}
132
+ required={parameter.required}
133
+ >
134
+ <FieldChildren node={{ ...parameter, name: undefined, description: undefined }} />
135
+ {parameter.description}
136
+ </ParamField>
137
+ )),
138
+ )}
139
+ </div>
136
140
  </section>
137
141
  )}
138
142
  {operation.requestBody && (
@@ -162,7 +166,11 @@ export function OpenApiOperation({ operation }: OpenApiOperationProps) {
162
166
  <span className="text-muted-foreground text-sm">{response.description}</span>
163
167
  )}
164
168
  </div>
165
- {response.schema && <SchemaFields node={response.schema} />}
169
+ {response.schema && (
170
+ <div className={styles.fieldGroup}>
171
+ <SchemaFields node={response.schema} />
172
+ </div>
173
+ )}
166
174
  {response.example && (
167
175
  <HighlightedCode
168
176
  language="json"
@@ -275,12 +275,17 @@ function NavNodes({
275
275
  )}
276
276
  >
277
277
  {resolveIcon(icon)}
278
- {label}
279
278
  {method ? (
280
- <MethodBadge color={methodColor(method)} size="xs" className="ml-auto font-mono">
279
+ <MethodBadge
280
+ color={methodColor(method)}
281
+ size="xs"
282
+ className="w-10 shrink-0 justify-center whitespace-nowrap font-mono"
283
+ >
281
284
  {method === 'DELETE' ? 'DEL' : method}
282
285
  </MethodBadge>
283
- ) : tag ? (
286
+ ) : null}
287
+ {label}
288
+ {!method && tag ? (
284
289
  <Badge
285
290
  variant="secondary"
286
291
  className="ml-auto h-auto rounded-sm px-[0.35rem] py-[0.05rem] text-[0.7rem] text-muted-foreground uppercase"
@@ -0,0 +1,163 @@
1
+ import { Children, Fragment, isValidElement, type ReactElement, useMemo } from 'react';
2
+ import { useSearchParams } from 'react-router';
3
+ import { Badge } from './Badge';
4
+ import { styles } from './styles';
5
+ import { Update } from './Update';
6
+
7
+ export interface ChangelogProps {
8
+ children?: React.ReactNode;
9
+ }
10
+
11
+ interface UpdateChildInfo {
12
+ key: string;
13
+ tags: string[];
14
+ }
15
+
16
+ /** Flattens fragments so <Update> entries are found however MDX nests them. */
17
+ function flatElements(children: React.ReactNode): ReactElement<{ tags?: unknown }>[] {
18
+ const flat: ReactElement<{ tags?: unknown }>[] = [];
19
+
20
+ Children.forEach(children, child => {
21
+ if (!isValidElement<{ tags?: unknown }>(child)) {
22
+ return;
23
+ }
24
+ if (child.type === Fragment) {
25
+ flat.push(...flatElements(child.props.children));
26
+ } else {
27
+ flat.push(child);
28
+ }
29
+ });
30
+
31
+ return flat;
32
+ }
33
+
34
+ /** Reads `tags` off <Update> children without rendering them. */
35
+ function updateInfo(children: React.ReactNode): Map<string, UpdateChildInfo> {
36
+ const info = new Map<string, UpdateChildInfo>();
37
+
38
+ flatElements(children).forEach((child, index) => {
39
+ if (child.type !== Update) {
40
+ return;
41
+ }
42
+
43
+ const key = String(child.key ?? `update-${index}`);
44
+ const raw = child.props.tags;
45
+ const tags = Array.isArray(raw)
46
+ ? [...new Set(raw.filter((tag): tag is string => typeof tag === 'string' && tag.trim()))].map(
47
+ tag => tag.trim(),
48
+ )
49
+ : [];
50
+ info.set(key, { key, tags });
51
+ });
52
+
53
+ return info;
54
+ }
55
+
56
+ /**
57
+ * Groups <Update> entries with Mintlify-style tag filtering.
58
+ * Selected tags sync to `?tags=a,b` so filtered views are shareable.
59
+ * Pages without any tagged updates render as a plain list.
60
+ */
61
+ export function Changelog({ children }: ChangelogProps) {
62
+ const [searchParams, setSearchParams] = useSearchParams();
63
+
64
+ const allTags = useMemo(() => {
65
+ const seen = new Map<string, string>();
66
+ for (const { tags } of updateInfo(children).values()) {
67
+ for (const tag of tags) {
68
+ const key = tag.toLowerCase();
69
+ if (!seen.has(key)) {
70
+ seen.set(key, tag);
71
+ }
72
+ }
73
+ }
74
+ return [...seen.values()];
75
+ }, [children]);
76
+
77
+ const selected = useMemo(() => {
78
+ const param = searchParams.get('tags');
79
+ if (!param) {
80
+ return [];
81
+ }
82
+ const wanted = new Set(
83
+ param
84
+ .split(',')
85
+ .map(tag => tag.trim().toLowerCase())
86
+ .filter(Boolean),
87
+ );
88
+ return allTags.filter(tag => wanted.has(tag.toLowerCase()));
89
+ }, [searchParams, allTags]);
90
+
91
+ const infoByKey = useMemo(() => updateInfo(children), [children]);
92
+
93
+ if (allTags.length === 0) {
94
+ return <div className={styles.changelog}>{children}</div>;
95
+ }
96
+
97
+ const toggle = (tag: string) => {
98
+ const lower = tag.toLowerCase();
99
+ const next = selected.some(item => item.toLowerCase() === lower)
100
+ ? selected.filter(item => item.toLowerCase() !== lower)
101
+ : [...selected, tag];
102
+
103
+ setSearchParams(next.length ? { tags: next.join(',') } : {}, { preventScrollReset: true });
104
+ };
105
+
106
+ const clear = () => {
107
+ setSearchParams({}, { preventScrollReset: true });
108
+ };
109
+
110
+ const selectedSet = new Set(selected.map(tag => tag.toLowerCase()));
111
+
112
+ const filtered = flatElements(children).map((child, index) => {
113
+ if (child.type !== Update) {
114
+ return child;
115
+ }
116
+ const key = String(child.key ?? `update-${index}`);
117
+ const tags = infoByKey.get(key)?.tags ?? [];
118
+ if (selectedSet.size === 0) {
119
+ return child;
120
+ }
121
+ const lower = tags.map(tag => tag.toLowerCase());
122
+ const matches = [...selectedSet].every(tag => lower.includes(tag));
123
+ return matches ? child : null;
124
+ });
125
+
126
+ const visibleUpdates = filtered.filter(
127
+ child => child && (child as React.ReactElement).type === Update,
128
+ ).length;
129
+
130
+ return (
131
+ <div className={styles.changelog}>
132
+ {/* biome-ignore lint/a11y/useSemanticElements: toggle-button group labelled for the filter */}
133
+ <div className={styles.changelogFilters} role="group" aria-label="Filter updates by tag">
134
+ {allTags.map(tag => {
135
+ const active = selectedSet.has(tag.toLowerCase());
136
+ return (
137
+ <button
138
+ key={tag}
139
+ type="button"
140
+ onClick={() => toggle(tag)}
141
+ aria-pressed={active}
142
+ className={styles.changelogFilterButton}
143
+ data-active={active ? '' : undefined}
144
+ >
145
+ <Badge size="sm" color={active ? 'primary' : undefined}>
146
+ {tag}
147
+ </Badge>
148
+ </button>
149
+ );
150
+ })}
151
+ {selected.length > 0 ? (
152
+ <button type="button" onClick={clear} className={styles.changelogClear}>
153
+ Clear
154
+ </button>
155
+ ) : null}
156
+ </div>
157
+ {visibleUpdates === 0 ? (
158
+ <p className={styles.changelogEmpty}>No updates match the selected tags.</p>
159
+ ) : null}
160
+ {filtered}
161
+ </div>
162
+ );
163
+ }