@umami/shiso 1.12.0 → 1.14.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 +166 -139
- package/dist/chunks/abnfDiagram-VCTEODGH.js +109 -0
- package/dist/chunks/arc.js +130 -0
- package/dist/chunks/architecture-7GRP2DOG.js +4 -0
- package/dist/chunks/architectureDiagram-5GKGNRK7.js +7466 -0
- package/dist/chunks/array.js +8 -0
- package/dist/chunks/blockDiagram-I7D4REHJ.js +2546 -0
- package/dist/chunks/c4Diagram-7LVT6UL2.js +3654 -0
- package/dist/chunks/channel.js +9 -0
- package/dist/chunks/chunk-2Q5K7J3B.js +21 -0
- package/dist/chunks/chunk-4HAMMTFA.js +6106 -0
- package/dist/chunks/chunk-5VM5RSS4.js +21 -0
- package/dist/chunks/chunk-75Z2AOVW.js +2185 -0
- package/dist/chunks/chunk-DU6HZSFF.js +8729 -0
- package/dist/chunks/chunk-F27PBJKO.js +95 -0
- package/dist/chunks/chunk-FOHPRMQF.js +27162 -0
- package/dist/chunks/chunk-GMAD6QVW.js +2369 -0
- package/dist/chunks/chunk-GVQU2GXP.js +58 -0
- package/dist/chunks/chunk-IMKFNOWR.js +2593 -0
- package/dist/chunks/chunk-JWPE2WC7.js +12 -0
- package/dist/chunks/chunk-L3NEJ4N5.js +420 -0
- package/dist/chunks/chunk-OSK3NFVY.js +1029 -0
- package/dist/chunks/chunk-P2QGCYS3.js +95 -0
- package/dist/chunks/chunk-POPQ4Y6H.js +33 -0
- package/dist/chunks/chunk-PWAF6VOD.js +467 -0
- package/dist/chunks/chunk-SHT3W25Y.js +4813 -0
- package/dist/chunks/chunk-SVP7TREG.js +686 -0
- package/dist/chunks/chunk-TICWLB2K.js +3800 -0
- package/dist/chunks/chunk-XXDRQBXY.js +12 -0
- package/dist/chunks/chunk-Y2CYZVJY.js +15 -0
- package/dist/chunks/classDiagram-ZZMXUADV.js +33 -0
- package/dist/chunks/classDiagram-v2-VYDZK3BY.js +33 -0
- package/dist/chunks/cose-bilkent-JH36ORCC.js +4268 -0
- package/dist/chunks/cynefin-OW5HDTMX.js +4 -0
- package/dist/chunks/cynefinDiagram-5FMLGOSQ.js +482 -0
- package/dist/chunks/cytoscape.esm.js +28282 -0
- package/dist/chunks/dagre-GXQ25YYZ.js +583 -0
- package/dist/chunks/dagre.js +4235 -0
- package/dist/chunks/defaultLocale.js +246 -0
- package/dist/chunks/diagram-S7CK7UJ4.js +402 -0
- package/dist/chunks/diagram-UQ7AKVKN.js +291 -0
- package/dist/chunks/diagram-VSXAHHWV.js +553 -0
- package/dist/chunks/diagram-VX7I27RA.js +798 -0
- package/dist/chunks/diagram-Z3DM3KII.js +186 -0
- package/dist/chunks/dist.js +92 -0
- package/dist/chunks/docs.js +1163 -347
- package/dist/chunks/ebnfDiagram-PWID7BFC.js +123 -0
- package/dist/chunks/erDiagram-RLTQ6QDP.js +2295 -0
- package/dist/chunks/eventmodeling-NTZA5JFV.js +4 -0
- package/dist/chunks/flowDiagram-HODETNUW.js +17 -0
- package/dist/chunks/ganttDiagram-EL5Y4UJY.js +3928 -0
- package/dist/chunks/gitGraph-4MIJSDKK.js +4 -0
- package/dist/chunks/gitGraphDiagram-WWUBYQGX.js +1153 -0
- package/dist/chunks/graphlib.js +4194 -0
- package/dist/chunks/info-A6RAGUB7.js +4 -0
- package/dist/chunks/infoDiagram-27XIBGKW.js +25 -0
- package/dist/chunks/init.js +14 -0
- package/dist/chunks/ishikawaDiagram-5VMMS53U.js +971 -0
- package/dist/chunks/journeyDiagram-3NMN7TZE.js +1226 -0
- package/dist/chunks/kanban-definition-UXKFOSKX.js +1241 -0
- package/dist/chunks/katex.js +26463 -0
- package/dist/chunks/line.js +48 -0
- package/dist/chunks/linear.js +391 -0
- package/dist/chunks/mermaid-parser.core.js +754 -0
- package/dist/chunks/mermaid.core.js +4898 -0
- package/dist/chunks/mindmap-definition-YA3MSWOX.js +1309 -0
- package/dist/chunks/ordinal.js +84 -0
- package/dist/chunks/packet-AYTQ26CC.js +4 -0
- package/dist/chunks/path.js +106 -0
- package/dist/chunks/pegDiagram-XKGWAZYB.js +115 -0
- package/dist/chunks/pie-WAS4IAKB.js +4 -0
- package/dist/chunks/pieDiagram-E7YTZNPT.js +297 -0
- package/dist/chunks/quadrantDiagram-AXDQQJYC.js +2234 -0
- package/dist/chunks/radar-RG4KPBEZ.js +4 -0
- package/dist/chunks/railroad-74A4TZTK.js +4 -0
- package/dist/chunks/railroad-abnf-HS5TGJTU.js +4 -0
- package/dist/chunks/railroad-ebnf-LZEXJU2U.js +4 -0
- package/dist/chunks/railroad-peg-WCYAUIDC.js +4 -0
- package/dist/chunks/railroadDiagram-O6MQD6OU.js +89 -0
- package/dist/chunks/requirementDiagram-BXWQKSXE.js +2464 -0
- package/dist/chunks/rough.esm.js +1396 -0
- package/dist/chunks/sankeyDiagram-P5KCCOFB.js +1308 -0
- package/dist/chunks/sequenceDiagram-WJ2MYXX4.js +5445 -0
- package/dist/chunks/sizeCapture-INFHLROL.js +56 -0
- package/dist/chunks/src.js +2653 -0
- package/dist/chunks/stateDiagram-D77RDMKH.js +368 -0
- package/dist/chunks/stateDiagram-v2-MP3YSRHH.js +33 -0
- package/dist/chunks/swimlanes-42K2YHIH.js +6925 -0
- package/dist/chunks/swimlanesDiagram-VR7AAH4N.js +32 -0
- package/dist/chunks/timeline-definition-24CTP7MA.js +1507 -0
- package/dist/chunks/treeView-Q6P3EWNA.js +4 -0
- package/dist/chunks/treemap-WGGIJYW6.js +4 -0
- package/dist/chunks/vennDiagram-4TSXK5OY.js +2803 -0
- package/dist/chunks/wardley-WFR3VGLG.js +4 -0
- package/dist/chunks/wardleyDiagram-VM6X3IG4.js +877 -0
- package/dist/chunks/xychartDiagram-S5SC5T6Z.js +2698 -0
- package/dist/components.js +2 -2
- package/dist/entry-client.js +1 -1
- package/dist/entry-server.js +2 -2
- package/docs.schema.json +1 -1
- package/package.json +2 -1
- package/scripts/expand-navigation-globs.mjs +77 -8
- package/scripts/lib/openapi.mjs +24 -13
- package/src/components/CodeBlock.tsx +36 -0
- package/src/components/Docs.tsx +44 -21
- package/src/components/OpenApiOperation.tsx +39 -52
- package/src/components/SideNav.tsx +8 -3
- package/src/components/docs/Changelog.tsx +163 -0
- package/src/components/docs/Mermaid.tsx +164 -0
- package/src/components/docs/Panel.tsx +23 -0
- package/src/components/docs/Param.tsx +6 -0
- package/src/components/docs/PropertiesTable.tsx +52 -36
- package/src/components/docs/Tiles.tsx +59 -0
- package/src/components/docs/Tree.tsx +359 -0
- package/src/components/docs/Update.tsx +72 -0
- package/src/components/docs/index.ts +8 -2
- package/src/components/docs/panel-context.tsx +25 -0
- package/src/components/docs/styles.ts +47 -6
- package/src/components/icons/index.ts +4 -0
- package/src/lib/openapi.ts +17 -1
- package/src/lib/rehype-shiki.ts +18 -1
- package/types/components.d.ts +83 -14
- package/src/components/docs/ParamField.tsx +0 -45
- package/src/components/docs/ResponseField.tsx +0 -36
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import { Badge } from './Badge';
|
|
3
|
-
import type { ResponseFieldProps } from './ResponseField';
|
|
4
3
|
import { decodeHtmlEntities, toElementArray } from './utils';
|
|
5
4
|
|
|
5
|
+
type FieldValue = string | number | boolean | null | undefined;
|
|
6
|
+
|
|
7
|
+
export interface PropertiesTableRowProps {
|
|
8
|
+
name: string;
|
|
9
|
+
type?: FieldValue;
|
|
10
|
+
default?: FieldValue;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
deprecated?: boolean;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
export interface PropertiesTableProps {
|
|
7
17
|
children?: ReactNode;
|
|
8
18
|
}
|
|
9
19
|
|
|
10
|
-
function displayValue(value:
|
|
20
|
+
function displayValue(value: PropertiesTableRowProps['type']): string | undefined {
|
|
11
21
|
if (typeof value === 'string') {
|
|
12
22
|
return decodeHtmlEntities(value);
|
|
13
23
|
}
|
|
@@ -16,7 +26,7 @@ function displayValue(value: ResponseFieldProps['type']): string | undefined {
|
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
export function PropertiesTable({ children }: PropertiesTableProps) {
|
|
19
|
-
const rows = toElementArray<
|
|
29
|
+
const rows = toElementArray<PropertiesTableRowProps>(children);
|
|
20
30
|
|
|
21
31
|
if (!rows.length) {
|
|
22
32
|
return null;
|
|
@@ -29,43 +39,49 @@ export function PropertiesTable({ children }: PropertiesTableProps) {
|
|
|
29
39
|
<table className="m-0 min-w-[40rem] table-fixed">
|
|
30
40
|
<thead>
|
|
31
41
|
<tr>
|
|
32
|
-
<th className="w-
|
|
33
|
-
<th className="w-
|
|
42
|
+
<th className="w-[30%]">Name</th>
|
|
43
|
+
<th className="w-[15%]">Type</th>
|
|
34
44
|
<th>Description</th>
|
|
35
45
|
</tr>
|
|
36
46
|
</thead>
|
|
37
|
-
<tbody>
|
|
38
|
-
{rows.map(row => {
|
|
39
|
-
const { name, type, required, deprecated, children: description } = row.props;
|
|
40
|
-
const defaultValue = displayValue(row.props.default);
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<tr key={row.key ?? name}>
|
|
44
|
-
<td>
|
|
45
|
-
<span className="inline-flex flex-wrap items-center gap-2">
|
|
46
|
-
{name}
|
|
47
|
-
{required ? (
|
|
48
|
-
<Badge size="xs" tone="primary">
|
|
49
|
-
required
|
|
50
|
-
</Badge>
|
|
51
|
-
) : null}
|
|
52
|
-
{deprecated ? <Badge size="xs">deprecated</Badge> : null}
|
|
53
|
-
</span>
|
|
54
|
-
</td>
|
|
55
|
-
<td>{displayValue(type)}</td>
|
|
56
|
-
<td className="[&_p]:m-0">
|
|
57
|
-
{description}
|
|
58
|
-
{defaultValue ? (
|
|
59
|
-
<span className="mt-1 block text-muted-foreground">
|
|
60
|
-
Default: {defaultValue}
|
|
61
|
-
</span>
|
|
62
|
-
) : null}
|
|
63
|
-
</td>
|
|
64
|
-
</tr>
|
|
65
|
-
);
|
|
66
|
-
})}
|
|
67
|
-
</tbody>
|
|
47
|
+
<tbody>{rows}</tbody>
|
|
68
48
|
</table>
|
|
69
49
|
</div>
|
|
70
50
|
);
|
|
71
51
|
}
|
|
52
|
+
|
|
53
|
+
function PropertiesTableRow({
|
|
54
|
+
name,
|
|
55
|
+
type,
|
|
56
|
+
required,
|
|
57
|
+
deprecated,
|
|
58
|
+
default: value,
|
|
59
|
+
children,
|
|
60
|
+
}: PropertiesTableRowProps) {
|
|
61
|
+
const defaultValue = displayValue(value);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<tr>
|
|
65
|
+
<td>
|
|
66
|
+
<span className="inline-flex flex-wrap items-center gap-2">
|
|
67
|
+
{name}
|
|
68
|
+
{required ? (
|
|
69
|
+
<Badge size="sm" tone="primary">
|
|
70
|
+
required
|
|
71
|
+
</Badge>
|
|
72
|
+
) : null}
|
|
73
|
+
{deprecated ? <Badge size="sm">deprecated</Badge> : null}
|
|
74
|
+
</span>
|
|
75
|
+
</td>
|
|
76
|
+
<td>{displayValue(type)}</td>
|
|
77
|
+
<td className="[&_p]:m-0">
|
|
78
|
+
{children}
|
|
79
|
+
{defaultValue ? (
|
|
80
|
+
<span className="mt-1 block text-muted-foreground">Default: {defaultValue}</span>
|
|
81
|
+
) : null}
|
|
82
|
+
</td>
|
|
83
|
+
</tr>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
PropertiesTable.Row = PropertiesTableRow;
|
|
@@ -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
|
+
}
|