@umami/shiso 0.54.0 → 0.61.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/CHANGELOG.md +12 -0
- package/README.md +9 -49
- package/bin/shiso.mjs +132 -0
- package/docs.schema.json +835 -0
- package/mdx.config.ts +143 -0
- package/package.json +73 -83
- package/scripts/check-package.mjs +18 -0
- package/scripts/generate-icon-registry.mjs +196 -0
- package/scripts/generate-last-modified.mjs +128 -0
- package/scripts/generate-search-index.mjs +252 -0
- package/scripts/load-docs-config.mjs +244 -0
- package/scripts/prerender.mjs +187 -0
- package/scripts/validate-config.mjs +104 -0
- package/scripts/vite-docs-config.mjs +60 -0
- package/src/App.tsx +39 -0
- package/src/components/Banner.tsx +69 -0
- package/src/components/CodeBlock.tsx +46 -0
- package/src/components/ConfiguredIcon.tsx +15 -0
- package/src/components/ContextualMenu.tsx +93 -0
- package/src/components/DocContent.tsx +100 -0
- package/src/components/Docs.tsx +126 -0
- package/src/components/Footer.tsx +84 -0
- package/src/components/Header.tsx +72 -0
- package/src/components/Layout.tsx +16 -0
- package/src/components/PageLinks.tsx +71 -0
- package/src/components/Search.tsx +208 -0
- package/src/components/SideNav.tsx +347 -0
- package/src/components/SocialIcon.tsx +88 -0
- package/src/components/ThemeToggle.tsx +34 -0
- package/src/components/TopNav.tsx +127 -0
- package/src/components/docs/Accordion.tsx +68 -0
- package/src/components/docs/Badge.tsx +171 -0
- package/src/components/docs/Callout.tsx +73 -0
- package/src/components/docs/Card.tsx +158 -0
- package/src/components/docs/CodeGroup.tsx +73 -0
- package/src/components/docs/Columns.tsx +20 -0
- package/src/components/docs/Expandable.tsx +28 -0
- package/src/components/docs/Frame.tsx +56 -0
- package/src/components/docs/Icon.tsx +30 -0
- package/src/components/docs/ParamField.tsx +45 -0
- package/src/components/docs/PropertiesTable.tsx +84 -0
- package/src/components/docs/ResponseField.tsx +36 -0
- package/src/components/docs/Steps.tsx +47 -0
- package/src/components/docs/Tabs.tsx +116 -0
- package/src/components/docs/Tooltip.tsx +21 -0
- package/src/components/docs/index.ts +15 -0
- package/src/components/docs/styles.ts +82 -0
- package/src/components/docs/utils.ts +118 -0
- package/src/components/icons/index.ts +17 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert.tsx +69 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +88 -0
- package/src/components/ui/collapsible.tsx +15 -0
- package/src/components/ui/command.tsx +173 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/scroll-area.tsx +71 -0
- package/src/components/ui/sheet.tsx +124 -0
- package/src/components/ui/tabs.tsx +73 -0
- package/src/components/ui/tooltip.tsx +52 -0
- package/src/declarations.d.ts +9 -0
- package/src/entry-client.tsx +17 -0
- package/src/entry-server.tsx +77 -0
- package/src/generated/last-modified.ts +2 -0
- package/src/lib/content.ts +44 -0
- package/src/lib/docs-config.ts +682 -0
- package/src/lib/head.ts +220 -0
- package/src/lib/icon-registry.generated.ts +4 -0
- package/src/lib/icons.ts +29 -0
- package/src/lib/inline-markdown.tsx +86 -0
- package/src/lib/mdast.ts +56 -0
- package/src/lib/paths.ts +86 -0
- package/src/lib/remark-toc.ts +71 -0
- package/src/lib/search/config.ts +43 -0
- package/src/lib/search/provider.ts +80 -0
- package/src/lib/search/providers/local.ts +15 -0
- package/src/lib/search-index.generated.ts +4 -0
- package/src/lib/search.ts +100 -0
- package/src/lib/site-config.ts +104 -0
- package/src/lib/site-model.ts +221 -0
- package/src/lib/slug.ts +38 -0
- package/src/lib/types.ts +478 -0
- package/src/lib/utils.ts +6 -0
- package/src/pages/DocPage.tsx +33 -0
- package/src/styles/global.css +268 -0
- package/src/styles/tokens.css +114 -0
- package/types/client.d.ts +3 -0
- package/types/search.d.ts +17 -0
- package/vite.config.ts +342 -0
- package/LICENSE +0 -21
- package/dist/index.css +0 -189
- package/dist/index.d.ts +0 -57
- package/dist/index.js +0 -464
- package/dist/index.mjs +0 -437
- package/server/index.d.ts +0 -30
- package/server/index.js +0 -189
- package/styles.css +0 -4766
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { type ReactNode, useEffect, useState } from 'react';
|
|
3
|
+
import { Link, useLocation, useNavigate } from 'react-router';
|
|
4
|
+
import { resolveIcon } from '@/components/docs/utils';
|
|
5
|
+
import { ChevronRight, ExternalLink } from '@/components/icons';
|
|
6
|
+
import { Badge } from '@/components/ui/badge';
|
|
7
|
+
import { Button } from '@/components/ui/button';
|
|
8
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
|
9
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
10
|
+
import { flattenNav, isNodeHidden } from '@/lib/docs-config';
|
|
11
|
+
import type { DocsTab, NavGroupNode, NavNode } from '@/lib/types';
|
|
12
|
+
|
|
13
|
+
const sectionLabelClass =
|
|
14
|
+
'flex min-w-0 flex-1 items-center gap-[0.4rem] pb-2 pr-1 font-bold text-inherit';
|
|
15
|
+
const groupLabelClass =
|
|
16
|
+
'flex min-w-0 flex-1 items-center gap-[0.4rem] px-3 py-2 font-medium text-inherit';
|
|
17
|
+
const selectedClass =
|
|
18
|
+
'border-l-sidebar-primary font-medium text-sidebar-primary hover:text-sidebar-primary';
|
|
19
|
+
|
|
20
|
+
export interface SideNavProps {
|
|
21
|
+
tabs: DocsTab[];
|
|
22
|
+
navigation: Record<string, NavNode[]>;
|
|
23
|
+
anchors: NavNode[];
|
|
24
|
+
isSticky?: boolean;
|
|
25
|
+
drilldown?: boolean;
|
|
26
|
+
navigationLabel: string;
|
|
27
|
+
expandLabel: string;
|
|
28
|
+
collapseLabel: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True when the group's root or any descendant page is the current page. */
|
|
32
|
+
function containsPage(node: NavGroupNode, pathname: string): boolean {
|
|
33
|
+
if (node.root?.page.url === pathname) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return flattenNav(node.children).some(page => page.url === pathname);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Group section. Groups with `collapsible: false` stay open; otherwise,
|
|
42
|
+
* top-level groups start expanded and nested groups start collapsed unless
|
|
43
|
+
* `expanded: true` or the current page is inside.
|
|
44
|
+
*
|
|
45
|
+
* Click behavior follows `interaction.drilldown`:
|
|
46
|
+
* - true: expanding also navigates to the group's root or first page
|
|
47
|
+
* - false: the header only expands/collapses
|
|
48
|
+
* - unset: headers with a root page navigate, others toggle
|
|
49
|
+
*/
|
|
50
|
+
function CollapsibleGroup({
|
|
51
|
+
node,
|
|
52
|
+
pathname,
|
|
53
|
+
depth,
|
|
54
|
+
drilldown,
|
|
55
|
+
expandLabel,
|
|
56
|
+
collapseLabel,
|
|
57
|
+
}: {
|
|
58
|
+
node: NavGroupNode;
|
|
59
|
+
pathname: string;
|
|
60
|
+
depth: number;
|
|
61
|
+
drilldown?: boolean;
|
|
62
|
+
expandLabel: string;
|
|
63
|
+
collapseLabel: string;
|
|
64
|
+
}) {
|
|
65
|
+
const navigate = useNavigate();
|
|
66
|
+
const active = containsPage(node, pathname);
|
|
67
|
+
const collapsible = node.collapsible !== false;
|
|
68
|
+
// Top-level sections stay open by default so the sidebar is usable on first load.
|
|
69
|
+
const [expanded, setExpanded] = useState(
|
|
70
|
+
!collapsible || depth === 0 || !!node.expanded || active,
|
|
71
|
+
);
|
|
72
|
+
const isExpanded = !collapsible || expanded;
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!collapsible || active) {
|
|
76
|
+
setExpanded(true);
|
|
77
|
+
}
|
|
78
|
+
}, [active, collapsible]);
|
|
79
|
+
|
|
80
|
+
const firstPage = node.root?.page || flattenNav(node.children).find(page => !page.hidden);
|
|
81
|
+
|
|
82
|
+
const handleOpenChange = (next: boolean) => {
|
|
83
|
+
setExpanded(next);
|
|
84
|
+
|
|
85
|
+
if (next && drilldown === true && firstPage) {
|
|
86
|
+
navigate(firstPage.url);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const chevron = (
|
|
91
|
+
<ChevronRight className="size-3.5 shrink-0 origin-center text-muted-foreground transition-[color,transform] duration-150 group-hover/section:text-sidebar-accent-foreground group-aria-expanded/collapsible-trigger:rotate-90" />
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const isTopLevel = depth === 0;
|
|
95
|
+
const rootSelected = node.root?.page.url === pathname;
|
|
96
|
+
const headerClass = isTopLevel
|
|
97
|
+
? 'flex min-w-0 items-center text-sidebar-foreground'
|
|
98
|
+
: classNames(
|
|
99
|
+
'flex items-center border-sidebar-border border-l text-muted-foreground hover:text-sidebar-accent-foreground',
|
|
100
|
+
{ 'pl-6': depth > 1 },
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const fixedHeader = (
|
|
104
|
+
<div
|
|
105
|
+
className={classNames(headerClass, {
|
|
106
|
+
[selectedClass]: rootSelected && !isTopLevel,
|
|
107
|
+
'text-sidebar-primary': rootSelected && isTopLevel,
|
|
108
|
+
})}
|
|
109
|
+
>
|
|
110
|
+
{node.root ? (
|
|
111
|
+
<Link to={node.root.page.url} className={isTopLevel ? sectionLabelClass : groupLabelClass}>
|
|
112
|
+
{resolveIcon(node.icon)}
|
|
113
|
+
{node.label}
|
|
114
|
+
</Link>
|
|
115
|
+
) : (
|
|
116
|
+
<span className={isTopLevel ? sectionLabelClass : groupLabelClass}>
|
|
117
|
+
{resolveIcon(node.icon)}
|
|
118
|
+
{node.label}
|
|
119
|
+
</span>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Navigating collapsible headers keep a separate chevron button so collapse stays reachable.
|
|
125
|
+
const header = !collapsible ? (
|
|
126
|
+
fixedHeader
|
|
127
|
+
) : drilldown !== false && node.root ? (
|
|
128
|
+
<div
|
|
129
|
+
className={classNames(headerClass, 'group/section flex items-center', {
|
|
130
|
+
[selectedClass]: rootSelected && !isTopLevel,
|
|
131
|
+
'text-sidebar-primary': rootSelected && isTopLevel,
|
|
132
|
+
})}
|
|
133
|
+
>
|
|
134
|
+
<Link to={node.root.page.url} className={isTopLevel ? sectionLabelClass : groupLabelClass}>
|
|
135
|
+
{resolveIcon(node.icon)}
|
|
136
|
+
{node.label}
|
|
137
|
+
</Link>
|
|
138
|
+
<CollapsibleTrigger
|
|
139
|
+
render={
|
|
140
|
+
<Button
|
|
141
|
+
type="button"
|
|
142
|
+
variant="ghost"
|
|
143
|
+
size="icon-xs"
|
|
144
|
+
className={classNames(
|
|
145
|
+
'group/collapsible-trigger inline-flex size-6 items-center justify-center rounded-sm text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
|
146
|
+
{ 'mr-[0.35rem]': !isTopLevel },
|
|
147
|
+
)}
|
|
148
|
+
aria-label={`${isExpanded ? collapseLabel : expandLabel} ${node.label}`}
|
|
149
|
+
/>
|
|
150
|
+
}
|
|
151
|
+
>
|
|
152
|
+
{chevron}
|
|
153
|
+
</CollapsibleTrigger>
|
|
154
|
+
</div>
|
|
155
|
+
) : (
|
|
156
|
+
<CollapsibleTrigger
|
|
157
|
+
render={
|
|
158
|
+
<Button
|
|
159
|
+
type="button"
|
|
160
|
+
variant="ghost"
|
|
161
|
+
className={classNames(
|
|
162
|
+
headerClass,
|
|
163
|
+
'group/section group/collapsible-trigger flex h-auto w-full items-center justify-start gap-[0.4rem] whitespace-normal rounded-none px-0 py-0 text-left hover:bg-transparent aria-expanded:bg-transparent dark:hover:bg-transparent [&>svg:last-child]:ml-auto',
|
|
164
|
+
{
|
|
165
|
+
'pb-2 font-bold text-sidebar-foreground': isTopLevel,
|
|
166
|
+
'border-sidebar-border border-l px-3 py-2 font-medium text-muted-foreground hover:text-sidebar-accent-foreground':
|
|
167
|
+
!isTopLevel,
|
|
168
|
+
},
|
|
169
|
+
)}
|
|
170
|
+
/>
|
|
171
|
+
}
|
|
172
|
+
>
|
|
173
|
+
{resolveIcon(node.icon)}
|
|
174
|
+
{node.label}
|
|
175
|
+
{chevron}
|
|
176
|
+
</CollapsibleTrigger>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (!collapsible) {
|
|
180
|
+
return (
|
|
181
|
+
<div className="flex flex-col">
|
|
182
|
+
{fixedHeader}
|
|
183
|
+
<div className="flex flex-col">
|
|
184
|
+
<NavNodes
|
|
185
|
+
nodes={node.children}
|
|
186
|
+
pathname={pathname}
|
|
187
|
+
depth={depth + 1}
|
|
188
|
+
drilldown={drilldown}
|
|
189
|
+
expandLabel={expandLabel}
|
|
190
|
+
collapseLabel={collapseLabel}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<Collapsible open={expanded} onOpenChange={handleOpenChange} className="flex flex-col">
|
|
199
|
+
{header}
|
|
200
|
+
<CollapsibleContent>
|
|
201
|
+
<div className="flex flex-col">
|
|
202
|
+
<NavNodes
|
|
203
|
+
nodes={node.children}
|
|
204
|
+
pathname={pathname}
|
|
205
|
+
depth={depth + 1}
|
|
206
|
+
drilldown={drilldown}
|
|
207
|
+
expandLabel={expandLabel}
|
|
208
|
+
collapseLabel={collapseLabel}
|
|
209
|
+
/>
|
|
210
|
+
</div>
|
|
211
|
+
</CollapsibleContent>
|
|
212
|
+
</Collapsible>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function NavNodes({
|
|
217
|
+
nodes,
|
|
218
|
+
pathname,
|
|
219
|
+
depth,
|
|
220
|
+
drilldown,
|
|
221
|
+
expandLabel,
|
|
222
|
+
collapseLabel,
|
|
223
|
+
}: {
|
|
224
|
+
nodes: NavNode[];
|
|
225
|
+
pathname: string;
|
|
226
|
+
depth: number;
|
|
227
|
+
drilldown?: boolean;
|
|
228
|
+
expandLabel: string;
|
|
229
|
+
collapseLabel: string;
|
|
230
|
+
}) {
|
|
231
|
+
const rendered: ReactNode[] = [];
|
|
232
|
+
|
|
233
|
+
nodes.forEach(node => {
|
|
234
|
+
if (isNodeHidden(node)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (node.kind === 'link') {
|
|
239
|
+
rendered.push(
|
|
240
|
+
<a
|
|
241
|
+
key={`link-${node.href}`}
|
|
242
|
+
href={node.href}
|
|
243
|
+
target={node.target}
|
|
244
|
+
rel={node.target === '_blank' ? 'noreferrer' : undefined}
|
|
245
|
+
className={classNames(
|
|
246
|
+
'flex min-w-0 items-center gap-[0.4rem] border-sidebar-border border-l px-3 py-[0.55rem] text-muted-foreground hover:text-sidebar-accent-foreground [overflow-wrap:anywhere]',
|
|
247
|
+
{ 'pl-6': depth > 1 },
|
|
248
|
+
)}
|
|
249
|
+
>
|
|
250
|
+
{resolveIcon(node.icon)}
|
|
251
|
+
{node.label}
|
|
252
|
+
<ExternalLink size={12} className="ml-auto opacity-60" />
|
|
253
|
+
</a>,
|
|
254
|
+
);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (node.kind === 'page') {
|
|
259
|
+
const { url, label, icon, tag } = node.page;
|
|
260
|
+
const isSelected = url === pathname;
|
|
261
|
+
|
|
262
|
+
rendered.push(
|
|
263
|
+
<Link
|
|
264
|
+
key={url}
|
|
265
|
+
to={url}
|
|
266
|
+
className={classNames(
|
|
267
|
+
'flex min-w-0 items-center gap-[0.4rem] border-sidebar-border border-l px-3 py-[0.55rem] [overflow-wrap:anywhere]',
|
|
268
|
+
{
|
|
269
|
+
'pl-6': depth > 1,
|
|
270
|
+
[selectedClass]: isSelected,
|
|
271
|
+
'text-muted-foreground hover:text-sidebar-accent-foreground': !isSelected,
|
|
272
|
+
},
|
|
273
|
+
)}
|
|
274
|
+
>
|
|
275
|
+
{resolveIcon(icon)}
|
|
276
|
+
{label}
|
|
277
|
+
{tag ? (
|
|
278
|
+
<Badge
|
|
279
|
+
variant="secondary"
|
|
280
|
+
className="ml-auto h-auto rounded-sm px-[0.35rem] py-[0.05rem] text-[0.7rem] text-muted-foreground uppercase"
|
|
281
|
+
>
|
|
282
|
+
{tag}
|
|
283
|
+
</Badge>
|
|
284
|
+
) : null}
|
|
285
|
+
</Link>,
|
|
286
|
+
);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
rendered.push(
|
|
291
|
+
<CollapsibleGroup
|
|
292
|
+
key={`group-${node.label}`}
|
|
293
|
+
node={node}
|
|
294
|
+
pathname={pathname}
|
|
295
|
+
depth={depth}
|
|
296
|
+
drilldown={drilldown}
|
|
297
|
+
expandLabel={expandLabel}
|
|
298
|
+
collapseLabel={collapseLabel}
|
|
299
|
+
/>,
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return <>{rendered}</>;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function SideNav({
|
|
307
|
+
tabs,
|
|
308
|
+
navigation,
|
|
309
|
+
anchors,
|
|
310
|
+
isSticky,
|
|
311
|
+
drilldown,
|
|
312
|
+
navigationLabel,
|
|
313
|
+
expandLabel,
|
|
314
|
+
collapseLabel,
|
|
315
|
+
}: SideNavProps) {
|
|
316
|
+
const { pathname } = useLocation();
|
|
317
|
+
|
|
318
|
+
const tab = [...tabs]
|
|
319
|
+
.sort((a, b) => b.url.length - a.url.length)
|
|
320
|
+
.find(item => pathname === item.url || pathname.startsWith(`${item.url}/`));
|
|
321
|
+
const nodes = navigation[tab?.id || tabs?.[0]?.id] || [];
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<ScrollArea className={classNames('w-full max-w-full', { 'h-full': isSticky })}>
|
|
325
|
+
<nav className="flex w-full flex-col gap-6 pr-4 text-sm" aria-label={navigationLabel}>
|
|
326
|
+
{anchors.length ? (
|
|
327
|
+
<NavNodes
|
|
328
|
+
nodes={anchors}
|
|
329
|
+
pathname={pathname}
|
|
330
|
+
depth={0}
|
|
331
|
+
drilldown={drilldown}
|
|
332
|
+
expandLabel={expandLabel}
|
|
333
|
+
collapseLabel={collapseLabel}
|
|
334
|
+
/>
|
|
335
|
+
) : null}
|
|
336
|
+
<NavNodes
|
|
337
|
+
nodes={nodes}
|
|
338
|
+
pathname={pathname}
|
|
339
|
+
depth={0}
|
|
340
|
+
drilldown={drilldown}
|
|
341
|
+
expandLabel={expandLabel}
|
|
342
|
+
collapseLabel={collapseLabel}
|
|
343
|
+
/>
|
|
344
|
+
</nav>
|
|
345
|
+
</ScrollArea>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Globe, Podcast } from 'lucide-react';
|
|
2
|
+
import {
|
|
3
|
+
siBluesky,
|
|
4
|
+
siDiscord,
|
|
5
|
+
siFacebook,
|
|
6
|
+
siGithub,
|
|
7
|
+
siInstagram,
|
|
8
|
+
siMedium,
|
|
9
|
+
siReddit,
|
|
10
|
+
siTelegram,
|
|
11
|
+
siThreads,
|
|
12
|
+
siX,
|
|
13
|
+
siYcombinator,
|
|
14
|
+
siYoutube,
|
|
15
|
+
} from 'simple-icons';
|
|
16
|
+
|
|
17
|
+
interface BrandIcon {
|
|
18
|
+
path: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Slack and LinkedIn were removed from both simple-icons and lucide for
|
|
23
|
+
* trademark reasons, so their (widely mirrored) 24x24 paths live here.
|
|
24
|
+
*/
|
|
25
|
+
const LOCAL_BRANDS: Record<string, BrandIcon> = {
|
|
26
|
+
slack: {
|
|
27
|
+
path: 'M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z',
|
|
28
|
+
},
|
|
29
|
+
linkedin: {
|
|
30
|
+
path: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Icons for every platform name the footer `socials` config accepts. Brand
|
|
36
|
+
* marks come from simple-icons; generic ones (website, podcast) fall back
|
|
37
|
+
* to lucide.
|
|
38
|
+
*/
|
|
39
|
+
const BRAND_ICONS: Record<string, BrandIcon> = {
|
|
40
|
+
x: siX,
|
|
41
|
+
twitter: siX,
|
|
42
|
+
'x-twitter': siX,
|
|
43
|
+
github: siGithub,
|
|
44
|
+
facebook: siFacebook,
|
|
45
|
+
youtube: siYoutube,
|
|
46
|
+
discord: siDiscord,
|
|
47
|
+
instagram: siInstagram,
|
|
48
|
+
'hacker-news': siYcombinator,
|
|
49
|
+
medium: siMedium,
|
|
50
|
+
telegram: siTelegram,
|
|
51
|
+
bluesky: siBluesky,
|
|
52
|
+
threads: siThreads,
|
|
53
|
+
reddit: siReddit,
|
|
54
|
+
...LOCAL_BRANDS,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const LUCIDE_ICONS = {
|
|
58
|
+
website: Globe,
|
|
59
|
+
'earth-americas': Globe,
|
|
60
|
+
podcast: Podcast,
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
export function isKnownPlatform(platform: string): boolean {
|
|
64
|
+
return platform in BRAND_ICONS || platform in LUCIDE_ICONS;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function SocialIcon({ platform, size = 18 }: { platform: string; size?: number }) {
|
|
68
|
+
const brand = BRAND_ICONS[platform];
|
|
69
|
+
|
|
70
|
+
if (brand) {
|
|
71
|
+
return (
|
|
72
|
+
<svg
|
|
73
|
+
role="img"
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
viewBox="0 0 24 24"
|
|
76
|
+
width={size}
|
|
77
|
+
height={size}
|
|
78
|
+
fill="currentColor"
|
|
79
|
+
>
|
|
80
|
+
<path d={brand.path} />
|
|
81
|
+
</svg>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const Lucide = LUCIDE_ICONS[platform as keyof typeof LUCIDE_ICONS] || Globe;
|
|
86
|
+
|
|
87
|
+
return <Lucide size={size} aria-hidden="true" />;
|
|
88
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Moon, Sun } from '@/components/icons';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Both icons are always rendered and toggled via CSS on [data-theme], so the
|
|
6
|
+
* server render matches the client regardless of the user's stored theme.
|
|
7
|
+
*/
|
|
8
|
+
export function ThemeToggle({ label }: { label: string }) {
|
|
9
|
+
const handleClick = () => {
|
|
10
|
+
const current = document.documentElement.getAttribute('data-theme');
|
|
11
|
+
const next = current === 'dark' ? 'light' : 'dark';
|
|
12
|
+
document.documentElement.setAttribute('data-theme', next);
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
localStorage.setItem('shiso-theme', next);
|
|
16
|
+
} catch {
|
|
17
|
+
// Ignore storage failures (private mode, etc).
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Button
|
|
23
|
+
type="button"
|
|
24
|
+
variant="ghost"
|
|
25
|
+
size="icon"
|
|
26
|
+
className="inline-flex size-8 items-center justify-center rounded-md text-foreground hover:bg-accent"
|
|
27
|
+
onClick={handleClick}
|
|
28
|
+
aria-label={label}
|
|
29
|
+
>
|
|
30
|
+
<Sun className="size-3.5 dark:hidden" />
|
|
31
|
+
<Moon className="hidden size-3.5 dark:block" />
|
|
32
|
+
</Button>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { Link, useLocation, useNavigate } from 'react-router';
|
|
3
|
+
import { ConfiguredIcon } from '@/components/ConfiguredIcon';
|
|
4
|
+
import { ChevronRight } from '@/components/icons';
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from '@/components/ui/dropdown-menu';
|
|
11
|
+
import type { DocsTab, LinkTarget, NavNode, NormalizedDocsConfig } from '@/lib/types';
|
|
12
|
+
|
|
13
|
+
interface MenuLink {
|
|
14
|
+
label: string;
|
|
15
|
+
href: string;
|
|
16
|
+
target: LinkTarget;
|
|
17
|
+
routed: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function collectMenuLinks(nodes: NavNode[]): MenuLink[] {
|
|
21
|
+
const links: MenuLink[] = [];
|
|
22
|
+
|
|
23
|
+
for (const node of nodes) {
|
|
24
|
+
if (node.kind === 'page') {
|
|
25
|
+
if (!node.page.hidden) {
|
|
26
|
+
links.push({ label: node.page.label, href: node.page.url, target: '_self', routed: true });
|
|
27
|
+
}
|
|
28
|
+
} else if (node.kind === 'link') {
|
|
29
|
+
if (!node.hidden) {
|
|
30
|
+
links.push({ label: node.label, href: node.href, target: node.target, routed: false });
|
|
31
|
+
}
|
|
32
|
+
} else if (!node.hidden) {
|
|
33
|
+
if (node.root && !node.root.page.hidden) {
|
|
34
|
+
links.push({
|
|
35
|
+
label: node.root.page.label,
|
|
36
|
+
href: node.root.page.url,
|
|
37
|
+
target: '_self',
|
|
38
|
+
routed: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
links.push(...collectMenuLinks(node.children));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return links;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function TopNav({ docs, label }: { docs: NormalizedDocsConfig; label: string }) {
|
|
49
|
+
const { pathname } = useLocation();
|
|
50
|
+
const navigate = useNavigate();
|
|
51
|
+
const tabs = docs.tabs.filter(tab => !tab.hidden);
|
|
52
|
+
|
|
53
|
+
if (!tabs.length) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const page = docs.pages.find(item => item.url === pathname);
|
|
58
|
+
const selected =
|
|
59
|
+
page?.tabId ||
|
|
60
|
+
[...tabs]
|
|
61
|
+
.sort((a, b) => b.url.length - a.url.length)
|
|
62
|
+
.find(tab => pathname === tab.url || pathname.startsWith(`${tab.url}/`))?.id ||
|
|
63
|
+
tabs[0]?.id;
|
|
64
|
+
const tabClass = (tab: DocsTab) =>
|
|
65
|
+
classNames(
|
|
66
|
+
'flex h-full items-center gap-1 whitespace-nowrap border-transparent border-b-2 font-medium',
|
|
67
|
+
{
|
|
68
|
+
'border-b-primary text-foreground': tab.id === selected,
|
|
69
|
+
'text-muted-foreground hover:text-foreground': tab.id !== selected,
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<nav
|
|
75
|
+
className="hidden h-[calc(100%+1px)] max-w-screen self-start items-center gap-7 overflow-x-auto text-sm [scrollbar-width:none] lg:flex [&::-webkit-scrollbar]:hidden"
|
|
76
|
+
aria-label={label}
|
|
77
|
+
>
|
|
78
|
+
{tabs.map(tab => {
|
|
79
|
+
if (tab.presentation === 'dropdown') {
|
|
80
|
+
const menuLinks = collectMenuLinks(docs.navigation[tab.id] || []);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<DropdownMenu key={tab.id}>
|
|
84
|
+
<DropdownMenuTrigger render={<button type="button" className={tabClass(tab)} />}>
|
|
85
|
+
<ConfiguredIcon icon={tab.icon} />
|
|
86
|
+
{tab.label}
|
|
87
|
+
<ChevronRight className="size-3.5 rotate-90" />
|
|
88
|
+
</DropdownMenuTrigger>
|
|
89
|
+
<DropdownMenuContent align="start" className="min-w-48">
|
|
90
|
+
{menuLinks.map(item => (
|
|
91
|
+
<DropdownMenuItem
|
|
92
|
+
key={item.href}
|
|
93
|
+
onClick={() => {
|
|
94
|
+
if (item.routed) {
|
|
95
|
+
navigate(item.href);
|
|
96
|
+
} else {
|
|
97
|
+
window.open(
|
|
98
|
+
item.href,
|
|
99
|
+
item.target,
|
|
100
|
+
item.target === '_blank' ? 'noreferrer' : undefined,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{item.label}
|
|
106
|
+
</DropdownMenuItem>
|
|
107
|
+
))}
|
|
108
|
+
</DropdownMenuContent>
|
|
109
|
+
</DropdownMenu>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Link
|
|
115
|
+
key={tab.id}
|
|
116
|
+
to={tab.url}
|
|
117
|
+
className={tabClass(tab)}
|
|
118
|
+
aria-current={tab.id === selected ? 'page' : undefined}
|
|
119
|
+
>
|
|
120
|
+
<ConfiguredIcon icon={tab.icon} />
|
|
121
|
+
{tab.label}
|
|
122
|
+
</Link>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</nav>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
AccordionContent,
|
|
4
|
+
AccordionItem,
|
|
5
|
+
Accordion as AccordionPrimitive,
|
|
6
|
+
AccordionTrigger,
|
|
7
|
+
} from '@/components/ui/accordion';
|
|
8
|
+
import { styles } from './styles';
|
|
9
|
+
import { resolveIcon, toElementArray } from './utils';
|
|
10
|
+
|
|
11
|
+
export interface AccordionProps {
|
|
12
|
+
title?: ReactNode;
|
|
13
|
+
icon?: ReactNode | string;
|
|
14
|
+
defaultOpen?: boolean;
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function AccordionSection({ value, title, icon, children }: AccordionProps & { value: string }) {
|
|
19
|
+
return (
|
|
20
|
+
<AccordionItem value={value} className={styles.accordionItem}>
|
|
21
|
+
<AccordionTrigger className={styles.accordionTrigger}>
|
|
22
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
23
|
+
{resolveIcon(icon)}
|
|
24
|
+
{title}
|
|
25
|
+
</span>
|
|
26
|
+
</AccordionTrigger>
|
|
27
|
+
<AccordionContent className={styles.accordionContent}>{children}</AccordionContent>
|
|
28
|
+
</AccordionItem>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function Accordion({ title, icon, defaultOpen, children }: AccordionProps) {
|
|
33
|
+
const value = 'accordion-item';
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<AccordionPrimitive defaultValue={defaultOpen ? [value] : []} className={styles.accordion}>
|
|
37
|
+
<AccordionSection value={value} title={title} icon={icon}>
|
|
38
|
+
{children}
|
|
39
|
+
</AccordionSection>
|
|
40
|
+
</AccordionPrimitive>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AccordionGroupProps {
|
|
45
|
+
children?: ReactNode;
|
|
46
|
+
defaultOpen?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function AccordionGroup({ children, defaultOpen = false }: AccordionGroupProps) {
|
|
50
|
+
const items = toElementArray<AccordionProps>(children);
|
|
51
|
+
|
|
52
|
+
if (!items.length) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const values = items.map((_, index) => `accordion-${index + 1}`);
|
|
57
|
+
const defaultValue = values.filter((_, index) => {
|
|
58
|
+
return items[index].props.defaultOpen ?? (defaultOpen && index === 0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<AccordionPrimitive multiple defaultValue={defaultValue} className={styles.accordionGroup}>
|
|
63
|
+
{items.map((child, index) => (
|
|
64
|
+
<AccordionSection key={child.key ?? values[index]} value={values[index]} {...child.props} />
|
|
65
|
+
))}
|
|
66
|
+
</AccordionPrimitive>
|
|
67
|
+
);
|
|
68
|
+
}
|