@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.
Files changed (125) hide show
  1. package/bin/shiso.mjs +0 -0
  2. package/dist/chunks/App.js +166 -139
  3. package/dist/chunks/abnfDiagram-VCTEODGH.js +109 -0
  4. package/dist/chunks/arc.js +130 -0
  5. package/dist/chunks/architecture-7GRP2DOG.js +4 -0
  6. package/dist/chunks/architectureDiagram-5GKGNRK7.js +7466 -0
  7. package/dist/chunks/array.js +8 -0
  8. package/dist/chunks/blockDiagram-I7D4REHJ.js +2546 -0
  9. package/dist/chunks/c4Diagram-7LVT6UL2.js +3654 -0
  10. package/dist/chunks/channel.js +9 -0
  11. package/dist/chunks/chunk-2Q5K7J3B.js +21 -0
  12. package/dist/chunks/chunk-4HAMMTFA.js +6106 -0
  13. package/dist/chunks/chunk-5VM5RSS4.js +21 -0
  14. package/dist/chunks/chunk-75Z2AOVW.js +2185 -0
  15. package/dist/chunks/chunk-DU6HZSFF.js +8729 -0
  16. package/dist/chunks/chunk-F27PBJKO.js +95 -0
  17. package/dist/chunks/chunk-FOHPRMQF.js +27162 -0
  18. package/dist/chunks/chunk-GMAD6QVW.js +2369 -0
  19. package/dist/chunks/chunk-GVQU2GXP.js +58 -0
  20. package/dist/chunks/chunk-IMKFNOWR.js +2593 -0
  21. package/dist/chunks/chunk-JWPE2WC7.js +12 -0
  22. package/dist/chunks/chunk-L3NEJ4N5.js +420 -0
  23. package/dist/chunks/chunk-OSK3NFVY.js +1029 -0
  24. package/dist/chunks/chunk-P2QGCYS3.js +95 -0
  25. package/dist/chunks/chunk-POPQ4Y6H.js +33 -0
  26. package/dist/chunks/chunk-PWAF6VOD.js +467 -0
  27. package/dist/chunks/chunk-SHT3W25Y.js +4813 -0
  28. package/dist/chunks/chunk-SVP7TREG.js +686 -0
  29. package/dist/chunks/chunk-TICWLB2K.js +3800 -0
  30. package/dist/chunks/chunk-XXDRQBXY.js +12 -0
  31. package/dist/chunks/chunk-Y2CYZVJY.js +15 -0
  32. package/dist/chunks/classDiagram-ZZMXUADV.js +33 -0
  33. package/dist/chunks/classDiagram-v2-VYDZK3BY.js +33 -0
  34. package/dist/chunks/cose-bilkent-JH36ORCC.js +4268 -0
  35. package/dist/chunks/cynefin-OW5HDTMX.js +4 -0
  36. package/dist/chunks/cynefinDiagram-5FMLGOSQ.js +482 -0
  37. package/dist/chunks/cytoscape.esm.js +28282 -0
  38. package/dist/chunks/dagre-GXQ25YYZ.js +583 -0
  39. package/dist/chunks/dagre.js +4235 -0
  40. package/dist/chunks/defaultLocale.js +246 -0
  41. package/dist/chunks/diagram-S7CK7UJ4.js +402 -0
  42. package/dist/chunks/diagram-UQ7AKVKN.js +291 -0
  43. package/dist/chunks/diagram-VSXAHHWV.js +553 -0
  44. package/dist/chunks/diagram-VX7I27RA.js +798 -0
  45. package/dist/chunks/diagram-Z3DM3KII.js +186 -0
  46. package/dist/chunks/dist.js +92 -0
  47. package/dist/chunks/docs.js +1163 -347
  48. package/dist/chunks/ebnfDiagram-PWID7BFC.js +123 -0
  49. package/dist/chunks/erDiagram-RLTQ6QDP.js +2295 -0
  50. package/dist/chunks/eventmodeling-NTZA5JFV.js +4 -0
  51. package/dist/chunks/flowDiagram-HODETNUW.js +17 -0
  52. package/dist/chunks/ganttDiagram-EL5Y4UJY.js +3928 -0
  53. package/dist/chunks/gitGraph-4MIJSDKK.js +4 -0
  54. package/dist/chunks/gitGraphDiagram-WWUBYQGX.js +1153 -0
  55. package/dist/chunks/graphlib.js +4194 -0
  56. package/dist/chunks/info-A6RAGUB7.js +4 -0
  57. package/dist/chunks/infoDiagram-27XIBGKW.js +25 -0
  58. package/dist/chunks/init.js +14 -0
  59. package/dist/chunks/ishikawaDiagram-5VMMS53U.js +971 -0
  60. package/dist/chunks/journeyDiagram-3NMN7TZE.js +1226 -0
  61. package/dist/chunks/kanban-definition-UXKFOSKX.js +1241 -0
  62. package/dist/chunks/katex.js +26463 -0
  63. package/dist/chunks/line.js +48 -0
  64. package/dist/chunks/linear.js +391 -0
  65. package/dist/chunks/mermaid-parser.core.js +754 -0
  66. package/dist/chunks/mermaid.core.js +4898 -0
  67. package/dist/chunks/mindmap-definition-YA3MSWOX.js +1309 -0
  68. package/dist/chunks/ordinal.js +84 -0
  69. package/dist/chunks/packet-AYTQ26CC.js +4 -0
  70. package/dist/chunks/path.js +106 -0
  71. package/dist/chunks/pegDiagram-XKGWAZYB.js +115 -0
  72. package/dist/chunks/pie-WAS4IAKB.js +4 -0
  73. package/dist/chunks/pieDiagram-E7YTZNPT.js +297 -0
  74. package/dist/chunks/quadrantDiagram-AXDQQJYC.js +2234 -0
  75. package/dist/chunks/radar-RG4KPBEZ.js +4 -0
  76. package/dist/chunks/railroad-74A4TZTK.js +4 -0
  77. package/dist/chunks/railroad-abnf-HS5TGJTU.js +4 -0
  78. package/dist/chunks/railroad-ebnf-LZEXJU2U.js +4 -0
  79. package/dist/chunks/railroad-peg-WCYAUIDC.js +4 -0
  80. package/dist/chunks/railroadDiagram-O6MQD6OU.js +89 -0
  81. package/dist/chunks/requirementDiagram-BXWQKSXE.js +2464 -0
  82. package/dist/chunks/rough.esm.js +1396 -0
  83. package/dist/chunks/sankeyDiagram-P5KCCOFB.js +1308 -0
  84. package/dist/chunks/sequenceDiagram-WJ2MYXX4.js +5445 -0
  85. package/dist/chunks/sizeCapture-INFHLROL.js +56 -0
  86. package/dist/chunks/src.js +2653 -0
  87. package/dist/chunks/stateDiagram-D77RDMKH.js +368 -0
  88. package/dist/chunks/stateDiagram-v2-MP3YSRHH.js +33 -0
  89. package/dist/chunks/swimlanes-42K2YHIH.js +6925 -0
  90. package/dist/chunks/swimlanesDiagram-VR7AAH4N.js +32 -0
  91. package/dist/chunks/timeline-definition-24CTP7MA.js +1507 -0
  92. package/dist/chunks/treeView-Q6P3EWNA.js +4 -0
  93. package/dist/chunks/treemap-WGGIJYW6.js +4 -0
  94. package/dist/chunks/vennDiagram-4TSXK5OY.js +2803 -0
  95. package/dist/chunks/wardley-WFR3VGLG.js +4 -0
  96. package/dist/chunks/wardleyDiagram-VM6X3IG4.js +877 -0
  97. package/dist/chunks/xychartDiagram-S5SC5T6Z.js +2698 -0
  98. package/dist/components.js +2 -2
  99. package/dist/entry-client.js +1 -1
  100. package/dist/entry-server.js +2 -2
  101. package/docs.schema.json +1 -1
  102. package/package.json +2 -1
  103. package/scripts/expand-navigation-globs.mjs +77 -8
  104. package/scripts/lib/openapi.mjs +24 -13
  105. package/src/components/CodeBlock.tsx +36 -0
  106. package/src/components/Docs.tsx +44 -21
  107. package/src/components/OpenApiOperation.tsx +39 -52
  108. package/src/components/SideNav.tsx +8 -3
  109. package/src/components/docs/Changelog.tsx +163 -0
  110. package/src/components/docs/Mermaid.tsx +164 -0
  111. package/src/components/docs/Panel.tsx +23 -0
  112. package/src/components/docs/Param.tsx +6 -0
  113. package/src/components/docs/PropertiesTable.tsx +52 -36
  114. package/src/components/docs/Tiles.tsx +59 -0
  115. package/src/components/docs/Tree.tsx +359 -0
  116. package/src/components/docs/Update.tsx +72 -0
  117. package/src/components/docs/index.ts +8 -2
  118. package/src/components/docs/panel-context.tsx +25 -0
  119. package/src/components/docs/styles.ts +47 -6
  120. package/src/components/icons/index.ts +4 -0
  121. package/src/lib/openapi.ts +17 -1
  122. package/src/lib/rehype-shiki.ts +18 -1
  123. package/types/components.d.ts +83 -14
  124. package/src/components/docs/ParamField.tsx +0 -45
  125. 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
+ }
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from 'react';
2
+ import { styles } from './styles';
3
+
4
+ export function Param({ children }: { children?: ReactNode }) {
5
+ return <code className={styles.code}>{children}</code>;
6
+ }
@@ -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: ResponseFieldProps['type']): string | undefined {
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<ResponseFieldProps>(children);
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-1/5">Name</th>
33
- <th className="w-1/4">Type</th>
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
+ }