@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
@@ -0,0 +1,164 @@
1
+ import { type ReactNode, useEffect, useId, useRef, useState } from 'react';
2
+ import { styles } from './styles';
3
+
4
+ export type MermaidPlacement = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
5
+
6
+ export interface MermaidProps {
7
+ /** Diagram definition. Falls back to string children when omitted. */
8
+ chart?: string;
9
+ title?: ReactNode;
10
+ /**
11
+ * Show zoom/pan/reset controls. Defaults to true when the diagram exceeds
12
+ * 120px in height (Mintlify behavior). Pass false to hide.
13
+ */
14
+ actions?: boolean;
15
+ /** Corner for the controls. Defaults to "bottom-right". */
16
+ placement?: MermaidPlacement;
17
+ children?: ReactNode;
18
+ }
19
+
20
+ function childrenToText(node: ReactNode): string {
21
+ if (typeof node === 'string' || typeof node === 'number') {
22
+ return String(node);
23
+ }
24
+ if (Array.isArray(node)) {
25
+ return node.map(childrenToText).join('');
26
+ }
27
+ if (node && typeof node === 'object' && 'props' in (node as object)) {
28
+ const props = (node as { props?: { children?: ReactNode } }).props;
29
+ return childrenToText(props?.children);
30
+ }
31
+ return '';
32
+ }
33
+
34
+ /** Extracts raw diagram source from MDX children (string or <code> element). */
35
+ export function mermaidSource(chart: string | undefined, children: ReactNode): string {
36
+ const raw = typeof chart === 'string' && chart.trim() ? chart : childrenToText(children);
37
+ return raw.replace(/^\n+|\n+$/g, '').replace(/\\n$/, '');
38
+ }
39
+
40
+ function isDarkMode(): boolean {
41
+ if (typeof document === 'undefined') {
42
+ return false;
43
+ }
44
+ return document.documentElement.classList.contains('dark');
45
+ }
46
+
47
+ const PLACEMENT_CLASS: Record<MermaidPlacement, string> = {
48
+ 'top-left': 'top-2 left-2',
49
+ 'top-right': 'top-2 right-2',
50
+ 'bottom-left': 'bottom-2 left-2',
51
+ 'bottom-right': 'bottom-2 right-2',
52
+ };
53
+
54
+ /**
55
+ * Renders a Mermaid diagram. Server/prerender output is the raw definition in
56
+ * a <pre> so search indexing and no-JS still see the content; the client
57
+ * replaces it with SVG via a lazy `mermaid` import (kept out of the SSR bundle).
58
+ */
59
+ export function Mermaid({
60
+ chart,
61
+ title,
62
+ actions,
63
+ placement = 'bottom-right',
64
+ children,
65
+ }: MermaidProps) {
66
+ const source = mermaidSource(chart, children);
67
+ const containerRef = useRef<HTMLDivElement>(null);
68
+ const svgHostRef = useRef<HTMLDivElement>(null);
69
+ const diagramId = useId().replace(/[^a-zA-Z0-9]/g, '');
70
+ const [svg, setSvg] = useState<string | null>(null);
71
+ const [error, setError] = useState(false);
72
+ const [zoom, setZoom] = useState(1);
73
+ const [dark, setDark] = useState(false);
74
+
75
+ useEffect(() => {
76
+ setDark(isDarkMode());
77
+ const observer = new MutationObserver(() => setDark(isDarkMode()));
78
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
79
+ return () => observer.disconnect();
80
+ }, []);
81
+
82
+ useEffect(() => {
83
+ if (!source.trim() || typeof window === 'undefined') {
84
+ return;
85
+ }
86
+
87
+ let cancelled = false;
88
+ setError(false);
89
+ setSvg(null);
90
+ (async () => {
91
+ try {
92
+ const { default: mermaid } = await import('mermaid');
93
+ if (cancelled) {
94
+ return;
95
+ }
96
+ mermaid.initialize({
97
+ startOnLoad: false,
98
+ theme: dark ? 'dark' : 'neutral',
99
+ securityLevel: 'strict',
100
+ });
101
+ const { svg: rendered } = await mermaid.render(`shiso-mermaid-${diagramId}`, source);
102
+ if (!cancelled) {
103
+ setSvg(rendered);
104
+ }
105
+ } catch {
106
+ if (!cancelled) {
107
+ setError(true);
108
+ }
109
+ }
110
+ })();
111
+
112
+ return () => {
113
+ cancelled = true;
114
+ };
115
+ }, [source, diagramId, dark]);
116
+
117
+ // Pan is native scroll; zoom controls scale the SVG host.
118
+ const showControls = actions ?? true;
119
+
120
+ return (
121
+ <figure className={styles.mermaid} data-dark={dark ? '' : undefined}>
122
+ {title ? <figcaption className={styles.mermaidTitle}>{title}</figcaption> : null}
123
+ <div ref={containerRef} className={styles.mermaidViewport}>
124
+ {svg && !error ? (
125
+ <div
126
+ ref={svgHostRef}
127
+ className={styles.mermaidSvg}
128
+ style={{ transform: `scale(${zoom})` }}
129
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: mermaid generates this SVG from the page's own diagram source.
130
+ dangerouslySetInnerHTML={{ __html: svg }}
131
+ role="img"
132
+ aria-label={title ? String(title) : 'Diagram'}
133
+ />
134
+ ) : (
135
+ <pre className={styles.mermaidFallback} title="Diagram source">
136
+ {source}
137
+ </pre>
138
+ )}
139
+ {error ? <p className={styles.mermaidError}>Could not render this diagram.</p> : null}
140
+ {showControls && svg && !error ? (
141
+ <div className={`${styles.mermaidControls} ${PLACEMENT_CLASS[placement]}`}>
142
+ <button
143
+ type="button"
144
+ aria-label="Zoom out"
145
+ onClick={() => setZoom(z => Math.max(0.5, +(z - 0.25).toFixed(2)))}
146
+ >
147
+ −
148
+ </button>
149
+ <button type="button" aria-label="Reset view" onClick={() => setZoom(1)}>
150
+ ⟳
151
+ </button>
152
+ <button
153
+ type="button"
154
+ aria-label="Zoom in"
155
+ onClick={() => setZoom(z => Math.min(2.5, +(z + 0.25).toFixed(2)))}
156
+ >
157
+ +
158
+ </button>
159
+ </div>
160
+ ) : null}
161
+ </div>
162
+ </figure>
163
+ );
164
+ }
@@ -0,0 +1,23 @@
1
+ import { type ReactNode, useEffect } from 'react';
2
+ import { useSetPanelContent } from './panel-context';
3
+ import { styles } from './styles';
4
+
5
+ export interface PanelProps {
6
+ children?: ReactNode;
7
+ }
8
+
9
+ /**
10
+ * Pins supplementary content to the right rail (replacing the table of
11
+ * contents on desktop). On small screens the content renders inline where it
12
+ * is written. Only one Panel per page is supported; the last one wins.
13
+ */
14
+ export function Panel({ children }: PanelProps) {
15
+ const setContent = useSetPanelContent();
16
+
17
+ useEffect(() => {
18
+ setContent(children ? <div className={styles.panel}>{children}</div> : null);
19
+ return () => setContent(null);
20
+ }, [children, setContent]);
21
+
22
+ return <div className={styles.panelInline}>{children}</div>;
23
+ }
@@ -28,14 +28,26 @@ export function ParamField({
28
28
  const location = query ? 'query' : path ? 'path' : header ? 'header' : body ? 'body' : undefined;
29
29
 
30
30
  return (
31
- <div className={styles.paramField}>
31
+ <div className={styles.paramField} data-slot="field-group-item">
32
32
  <div className={styles.fieldHeader}>
33
33
  <span className={styles.fieldName}>{label}</span>
34
- {location ? <Badge>{location}</Badge> : null}
35
- {type ? <span className={styles.fieldType}>{decodeHtmlEntities(type)}</span> : null}
36
- {required ? <Badge tone="primary">required</Badge> : null}
34
+ {location ? <Badge size="sm">{location}</Badge> : null}
35
+ {type ? (
36
+ <span className={styles.fieldType} data-slot="field-group-type">
37
+ {decodeHtmlEntities(type)}
38
+ </span>
39
+ ) : null}
40
+ {required ? (
41
+ <Badge size="sm" tone="primary">
42
+ required
43
+ </Badge>
44
+ ) : null}
37
45
  </div>
38
- {children ? <div className={styles.fieldBody}>{children}</div> : null}
46
+ {children ? (
47
+ <div className={styles.fieldBody} data-slot="field-group-body">
48
+ {children}
49
+ </div>
50
+ ) : null}
39
51
  </div>
40
52
  );
41
53
  }
@@ -45,11 +45,11 @@ export function PropertiesTable({ children }: PropertiesTableProps) {
45
45
  <span className="inline-flex flex-wrap items-center gap-2">
46
46
  {name}
47
47
  {required ? (
48
- <Badge size="xs" tone="primary">
48
+ <Badge size="sm" tone="primary">
49
49
  required
50
50
  </Badge>
51
51
  ) : null}
52
- {deprecated ? <Badge size="xs">deprecated</Badge> : null}
52
+ {deprecated ? <Badge size="sm">deprecated</Badge> : null}
53
53
  </span>
54
54
  </td>
55
55
  <td>{displayValue(type)}</td>
@@ -23,14 +23,26 @@ export function ResponseField({ name, type, required, deprecated, children }: Re
23
23
  : String(type);
24
24
 
25
25
  return (
26
- <div className={styles.field}>
26
+ <div className={styles.field} data-slot="field-group-item">
27
27
  <div className={styles.fieldHeader}>
28
28
  <span className={styles.fieldName}>{name}</span>
29
- {normalizedType ? <span className={styles.fieldType}>{normalizedType}</span> : null}
30
- {required ? <Badge tone="primary">required</Badge> : null}
31
- {deprecated ? <Badge>deprecated</Badge> : null}
29
+ {normalizedType ? (
30
+ <span className={styles.fieldType} data-slot="field-group-type">
31
+ {normalizedType}
32
+ </span>
33
+ ) : null}
34
+ {required ? (
35
+ <Badge size="sm" tone="primary">
36
+ required
37
+ </Badge>
38
+ ) : null}
39
+ {deprecated ? <Badge size="sm">deprecated</Badge> : null}
32
40
  </div>
33
- {children ? <div className={styles.fieldBody}>{children}</div> : null}
41
+ {children ? (
42
+ <div className={styles.fieldBody} data-slot="field-group-body">
43
+ {children}
44
+ </div>
45
+ ) : null}
34
46
  </div>
35
47
  );
36
48
  }
@@ -0,0 +1,59 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Link } from 'react-router';
3
+ import { styles } from './styles';
4
+ import { gridColsClass } from './utils';
5
+
6
+ export interface TileProps {
7
+ /** URL to navigate to when the tile is clicked. */
8
+ href: string;
9
+ title?: ReactNode;
10
+ description?: ReactNode;
11
+ /** Preview content, typically light/dark image variants. */
12
+ children: ReactNode;
13
+ target?: '_self' | '_blank';
14
+ }
15
+
16
+ export function Tile({ href, title, description, children, target }: TileProps) {
17
+ const external = /^https?:\/\//i.test(href);
18
+ const resolvedTarget = target ?? (external ? '_blank' : undefined);
19
+
20
+ const content = (
21
+ <>
22
+ <div className={styles.tilePreview} data-slot="tile-preview">
23
+ {children}
24
+ </div>
25
+ {title || description ? (
26
+ <div className={styles.tileBody} data-slot="tile-body">
27
+ {title ? <div className={styles.tileTitle}>{title}</div> : null}
28
+ {description ? <div className={styles.tileDescription}>{description}</div> : null}
29
+ </div>
30
+ ) : null}
31
+ </>
32
+ );
33
+
34
+ const className = `${styles.tile} no-underline hover:no-underline active:no-underline`;
35
+
36
+ if (external) {
37
+ return (
38
+ <a href={href} className={className} target={resolvedTarget} rel="noreferrer">
39
+ {content}
40
+ </a>
41
+ );
42
+ }
43
+
44
+ return (
45
+ <Link to={href} className={className} target={resolvedTarget}>
46
+ {content}
47
+ </Link>
48
+ );
49
+ }
50
+
51
+ export interface TilesProps {
52
+ children?: ReactNode;
53
+ cols?: 1 | 2 | 3 | 4;
54
+ }
55
+
56
+ /** Grid wrapper for <Tile> when <Columns> is not used directly. */
57
+ export function Tiles({ children, cols = 2 }: TilesProps) {
58
+ return <div className={`${styles.grid} ${styles[gridColsClass(cols)]}`}>{children}</div>;
59
+ }
@@ -0,0 +1,359 @@
1
+ import {
2
+ Children,
3
+ type CSSProperties,
4
+ isValidElement,
5
+ type KeyboardEvent,
6
+ type ReactElement,
7
+ type ReactNode,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
11
+ import {
12
+ ChevronRight,
13
+ File as FileIcon,
14
+ Folder as FolderIcon,
15
+ FolderOpen,
16
+ } from '@/components/icons';
17
+ import { styles } from './styles';
18
+
19
+ export interface TreeFolderProps {
20
+ name: string;
21
+ defaultOpen?: boolean;
22
+ /** When false the folder renders statically (no toggle). Defaults to true. */
23
+ openable?: boolean;
24
+ highlight?: boolean;
25
+ children?: ReactNode;
26
+ }
27
+
28
+ export interface TreeFileProps {
29
+ name: string;
30
+ highlight?: boolean;
31
+ }
32
+
33
+ export interface TreeProps {
34
+ children?: ReactNode;
35
+ style?: CSSProperties;
36
+ className?: string;
37
+ }
38
+
39
+ function elementText(node: ReactNode): string {
40
+ if (typeof node === 'string' || typeof node === 'number') {
41
+ return String(node);
42
+ }
43
+ if (Array.isArray(node)) {
44
+ return node.map(elementText).join('');
45
+ }
46
+ if (isValidElement<{ children?: ReactNode }>(node)) {
47
+ return elementText(node.props.children);
48
+ }
49
+ return '';
50
+ }
51
+
52
+ function isListElement(node: ReactNode): node is ReactElement<{ children?: ReactNode }> {
53
+ return isValidElement(node) && (node.type === 'ul' || node.type === 'ol');
54
+ }
55
+
56
+ /** Stable key for MDX-generated list wrappers (static content, never reordered). */
57
+ function listFingerprint(node: ReactNode): string {
58
+ const text = elementText(node).replace(/\s+/g, ' ').trim().slice(0, 64);
59
+ let hash = 0;
60
+ for (let i = 0; i < text.length; i += 1) {
61
+ hash = (hash * 31 + text.charCodeAt(i)) | 0;
62
+ }
63
+ return `mdx-list-${text.length}-${hash}`;
64
+ }
65
+
66
+ /** Split an MDX <li> into its label (inline content) and an optional nested list. */
67
+ function splitListItem(li: ReactElement<{ children?: ReactNode }>): {
68
+ label: string;
69
+ nested: ReactNode;
70
+ } {
71
+ let nested: ReactNode = null;
72
+ const labelParts: ReactNode[] = [];
73
+
74
+ Children.forEach(li.props.children, child => {
75
+ if (isListElement(child)) {
76
+ nested = child;
77
+ } else {
78
+ labelParts.push(child);
79
+ }
80
+ });
81
+
82
+ return { label: elementText(labelParts).trim(), nested };
83
+ }
84
+
85
+ function ListItemsAsNodes({ items }: { items: ReactNode }): ReactNode {
86
+ const nodes: ReactNode[] = [];
87
+ let index = 0;
88
+
89
+ Children.forEach(items, li => {
90
+ if (!isValidElement<{ children?: ReactNode }>(li) || li.type !== 'li') {
91
+ return;
92
+ }
93
+ const { label, nested } = splitListItem(li);
94
+ if (!label && !nested) {
95
+ return;
96
+ }
97
+ const name = label || 'untitled';
98
+ const isFolder = name.endsWith('/') || Boolean(nested);
99
+ index += 1;
100
+
101
+ if (isFolder) {
102
+ nodes.push(
103
+ <TreeFolder key={`${name}-${index}`} name={name} defaultOpen={Boolean(nested)}>
104
+ {nested ? (
105
+ <ListItemsAsNodes
106
+ items={(nested as ReactElement<{ children?: ReactNode }>).props.children}
107
+ />
108
+ ) : null}
109
+ </TreeFolder>,
110
+ );
111
+ } else {
112
+ nodes.push(<TreeFile key={`${name}-${index}`} name={name} />);
113
+ }
114
+ });
115
+
116
+ return <>{nodes}</>;
117
+ }
118
+
119
+ function containsList(children: ReactNode): boolean {
120
+ let found = false;
121
+ Children.forEach(children, child => {
122
+ if (isListElement(child)) {
123
+ found = true;
124
+ }
125
+ });
126
+ return found;
127
+ }
128
+
129
+ export function TreeFolder({
130
+ name,
131
+ defaultOpen = false,
132
+ openable = true,
133
+ highlight = false,
134
+ children,
135
+ }: TreeFolderProps) {
136
+ const [open, setOpen] = useState(defaultOpen);
137
+ const expanded = openable ? open : true;
138
+ const Icon = expanded ? FolderOpen : FolderIcon;
139
+
140
+ return (
141
+ // biome-ignore lint/a11y/useFocusableInteractive: keyboard focus is delegated to the row button inside this treeitem
142
+ <li
143
+ role="treeitem"
144
+ aria-expanded={openable ? expanded : undefined}
145
+ data-highlight={highlight ? '' : undefined}
146
+ className={styles.treeItem}
147
+ >
148
+ {openable ? (
149
+ <button
150
+ type="button"
151
+ data-tree-focus
152
+ onClick={() => setOpen(value => !value)}
153
+ className={`${styles.treeRow} ${highlight ? styles.treeHighlight : ''}`}
154
+ >
155
+ <ChevronRight
156
+ size={14}
157
+ className={styles.treeChevron}
158
+ data-open={expanded ? '' : undefined}
159
+ aria-hidden={true}
160
+ />
161
+ <Icon size={15} className={styles.treeIcon} aria-hidden={true} />
162
+ <span className={styles.treeName}>{name}</span>
163
+ </button>
164
+ ) : (
165
+ <span
166
+ data-tree-focus
167
+ tabIndex={-1}
168
+ className={`${styles.treeRow} ${highlight ? styles.treeHighlight : ''}`}
169
+ >
170
+ <Icon size={15} className={styles.treeIcon} aria-hidden={true} />
171
+ <span className={styles.treeName}>{name}</span>
172
+ </span>
173
+ )}
174
+ {children ? (
175
+ // biome-ignore lint/a11y/useSemanticElements: group is the required ARIA role for nested tree levels
176
+ <ul
177
+ role="group"
178
+ hidden={openable && !expanded ? true : undefined}
179
+ className={styles.treeGroup}
180
+ >
181
+ {children}
182
+ </ul>
183
+ ) : null}
184
+ </li>
185
+ );
186
+ }
187
+
188
+ export function TreeFile({ name, highlight = false }: TreeFileProps) {
189
+ return (
190
+ // biome-ignore lint/a11y/useFocusableInteractive: file rows are arrow-key targets in the tree widget
191
+ <li role="treeitem" data-highlight={highlight ? '' : undefined} className={styles.treeItem}>
192
+ <span
193
+ data-tree-focus
194
+ tabIndex={-1}
195
+ className={`${styles.treeRow} ${highlight ? styles.treeHighlight : ''}`}
196
+ >
197
+ <FileIcon size={15} className={styles.treeIcon} aria-hidden={true} />
198
+ <span className={styles.treeName}>{name}</span>
199
+ </span>
200
+ </li>
201
+ );
202
+ }
203
+
204
+ function visibleFocusables(root: HTMLElement): HTMLElement[] {
205
+ return [...root.querySelectorAll<HTMLElement>('[data-tree-focus]')].filter(
206
+ element => !element.closest('[hidden]'),
207
+ );
208
+ }
209
+
210
+ function TreeRoot({ children, style, className }: TreeProps) {
211
+ const rootRef = useRef<HTMLDivElement>(null);
212
+ const searchRef = useRef({ text: '', timer: 0 });
213
+
214
+ const onKeyDown = (event: KeyboardEvent) => {
215
+ const root = rootRef.current;
216
+ const target = event.target as HTMLElement;
217
+ if (!root || !target.closest('[data-tree-focus]')) {
218
+ return;
219
+ }
220
+
221
+ const items = visibleFocusables(root);
222
+ const index = items.indexOf(target as HTMLElement);
223
+
224
+ const focusAt = (next: number) => {
225
+ event.preventDefault();
226
+ items[(next + items.length) % items.length]?.focus();
227
+ };
228
+
229
+ switch (event.key) {
230
+ case 'ArrowDown':
231
+ focusAt(index + 1);
232
+ break;
233
+ case 'ArrowUp':
234
+ focusAt(index - 1);
235
+ break;
236
+ case 'Home':
237
+ focusAt(0);
238
+ break;
239
+ case 'End':
240
+ focusAt(items.length - 1);
241
+ break;
242
+ case 'ArrowRight': {
243
+ const row = target.closest('[role="treeitem"]');
244
+ const toggle = row?.querySelector<HTMLElement>(':scope > [data-tree-focus]');
245
+ if (
246
+ toggle &&
247
+ toggle.tagName === 'BUTTON' &&
248
+ toggle.getAttribute('aria-expanded') === 'false'
249
+ ) {
250
+ event.preventDefault();
251
+ toggle.click();
252
+ } else {
253
+ const firstChild = row?.querySelector<HTMLElement>(':scope > ul [data-tree-focus]');
254
+ if (firstChild) {
255
+ event.preventDefault();
256
+ firstChild.focus();
257
+ }
258
+ }
259
+ break;
260
+ }
261
+ case 'ArrowLeft': {
262
+ const row = target.closest('[role="treeitem"]');
263
+ const toggle = row?.querySelector<HTMLElement>(':scope > [data-tree-focus]');
264
+ if (
265
+ toggle &&
266
+ toggle.tagName === 'BUTTON' &&
267
+ toggle.getAttribute('aria-expanded') === 'true'
268
+ ) {
269
+ event.preventDefault();
270
+ toggle.click();
271
+ } else {
272
+ const parent = row?.parentElement?.closest('[role="treeitem"]');
273
+ const parentFocus = parent?.querySelector<HTMLElement>(':scope > [data-tree-focus]');
274
+ if (parentFocus) {
275
+ event.preventDefault();
276
+ parentFocus.focus();
277
+ }
278
+ }
279
+ break;
280
+ }
281
+ case '*': {
282
+ // Expand all sibling folders at the current level.
283
+ const group = target.closest('ul');
284
+ group
285
+ ?.querySelectorAll<HTMLElement>(
286
+ ':scope > [role="treeitem"] > button[aria-expanded="false"]',
287
+ )
288
+ .forEach(button => {
289
+ button.click();
290
+ });
291
+ break;
292
+ }
293
+ default: {
294
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
295
+ const state = searchRef.current;
296
+ window.clearTimeout(state.timer);
297
+ state.text += event.key.toLowerCase();
298
+ state.timer = window.setTimeout(() => {
299
+ state.text = '';
300
+ }, 500);
301
+ const match = items.find(item =>
302
+ (item.textContent || '').trim().toLowerCase().startsWith(state.text),
303
+ );
304
+ if (match) {
305
+ event.preventDefault();
306
+ (match as HTMLElement).focus();
307
+ }
308
+ }
309
+ }
310
+ }
311
+ };
312
+
313
+ const content = containsList(children)
314
+ ? Children.map(children, child =>
315
+ isListElement(child) ? (
316
+ <ListItemsAsNodes
317
+ key={listFingerprint(child)}
318
+ items={(child as ReactElement<{ children?: ReactNode }>).props.children}
319
+ />
320
+ ) : (
321
+ child
322
+ ),
323
+ )
324
+ : children;
325
+
326
+ // Buttons (folders) stay in the tab order natively; file rows are reached
327
+ // with arrow keys.
328
+ return (
329
+ // biome-ignore lint/a11y/noStaticElementInteractions: key handling for the tree widget lives on its root
330
+ <div
331
+ ref={rootRef}
332
+ style={style}
333
+ className={`${styles.tree} ${className || ''}`}
334
+ onKeyDown={onKeyDown}
335
+ >
336
+ {/* biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: tree is the required ARIA role for this widget */}
337
+ <ul role="tree" aria-label="File tree" className={styles.treeList}>
338
+ {content}
339
+ </ul>
340
+ </div>
341
+ );
342
+ }
343
+
344
+ export function Tree(props: TreeProps) {
345
+ return <TreeRoot {...props} />;
346
+ }
347
+
348
+ /** Alias: <Tree> and <FileTree> are interchangeable, like Mintlify. */
349
+ export function FileTree(props: TreeProps) {
350
+ return <TreeRoot {...props} />;
351
+ }
352
+
353
+ Tree.Folder = TreeFolder;
354
+ Tree.File = TreeFile;
355
+ FileTree.Folder = TreeFolder;
356
+ FileTree.File = TreeFile;
357
+
358
+ export const FileTreeFolder = TreeFolder;
359
+ export const FileTreeFile = TreeFile;