@umami/shiso 0.55.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +9 -49
  3. package/bin/shiso.mjs +132 -0
  4. package/docs.schema.json +835 -0
  5. package/mdx.config.ts +143 -0
  6. package/package.json +73 -83
  7. package/scripts/check-package.mjs +18 -0
  8. package/scripts/generate-icon-registry.mjs +196 -0
  9. package/scripts/generate-last-modified.mjs +128 -0
  10. package/scripts/generate-search-index.mjs +252 -0
  11. package/scripts/load-docs-config.mjs +244 -0
  12. package/scripts/prerender.mjs +187 -0
  13. package/scripts/validate-config.mjs +104 -0
  14. package/scripts/vite-docs-config.mjs +60 -0
  15. package/src/App.tsx +39 -0
  16. package/src/components/Banner.tsx +69 -0
  17. package/src/components/CodeBlock.tsx +46 -0
  18. package/src/components/ConfiguredIcon.tsx +15 -0
  19. package/src/components/ContextualMenu.tsx +93 -0
  20. package/src/components/DocContent.tsx +100 -0
  21. package/src/components/Docs.tsx +126 -0
  22. package/src/components/Footer.tsx +84 -0
  23. package/src/components/Header.tsx +72 -0
  24. package/src/components/Layout.tsx +16 -0
  25. package/src/components/PageLinks.tsx +71 -0
  26. package/src/components/Search.tsx +208 -0
  27. package/src/components/SideNav.tsx +347 -0
  28. package/src/components/SocialIcon.tsx +88 -0
  29. package/src/components/ThemeToggle.tsx +34 -0
  30. package/src/components/TopNav.tsx +127 -0
  31. package/src/components/docs/Accordion.tsx +68 -0
  32. package/src/components/docs/Badge.tsx +171 -0
  33. package/src/components/docs/Callout.tsx +73 -0
  34. package/src/components/docs/Card.tsx +158 -0
  35. package/src/components/docs/CodeGroup.tsx +73 -0
  36. package/src/components/docs/Columns.tsx +20 -0
  37. package/src/components/docs/Expandable.tsx +28 -0
  38. package/src/components/docs/Frame.tsx +56 -0
  39. package/src/components/docs/Icon.tsx +30 -0
  40. package/src/components/docs/ParamField.tsx +45 -0
  41. package/src/components/docs/PropertiesTable.tsx +84 -0
  42. package/src/components/docs/ResponseField.tsx +36 -0
  43. package/src/components/docs/Steps.tsx +47 -0
  44. package/src/components/docs/Tabs.tsx +116 -0
  45. package/src/components/docs/Tooltip.tsx +21 -0
  46. package/src/components/docs/index.ts +15 -0
  47. package/src/components/docs/styles.ts +82 -0
  48. package/src/components/docs/utils.ts +118 -0
  49. package/src/components/icons/index.ts +17 -0
  50. package/src/components/ui/accordion.tsx +69 -0
  51. package/src/components/ui/alert.tsx +69 -0
  52. package/src/components/ui/badge.tsx +49 -0
  53. package/src/components/ui/button.tsx +58 -0
  54. package/src/components/ui/card.tsx +88 -0
  55. package/src/components/ui/collapsible.tsx +15 -0
  56. package/src/components/ui/command.tsx +173 -0
  57. package/src/components/ui/dialog.tsx +137 -0
  58. package/src/components/ui/dropdown-menu.tsx +257 -0
  59. package/src/components/ui/scroll-area.tsx +71 -0
  60. package/src/components/ui/sheet.tsx +124 -0
  61. package/src/components/ui/tabs.tsx +73 -0
  62. package/src/components/ui/tooltip.tsx +52 -0
  63. package/src/declarations.d.ts +9 -0
  64. package/src/entry-client.tsx +17 -0
  65. package/src/entry-server.tsx +77 -0
  66. package/src/generated/last-modified.ts +2 -0
  67. package/src/lib/content.ts +44 -0
  68. package/src/lib/docs-config.ts +682 -0
  69. package/src/lib/head.ts +220 -0
  70. package/src/lib/icon-registry.generated.ts +4 -0
  71. package/src/lib/icons.ts +29 -0
  72. package/src/lib/inline-markdown.tsx +86 -0
  73. package/src/lib/mdast.ts +56 -0
  74. package/src/lib/paths.ts +86 -0
  75. package/src/lib/remark-toc.ts +71 -0
  76. package/src/lib/search/config.ts +43 -0
  77. package/src/lib/search/provider.ts +80 -0
  78. package/src/lib/search/providers/local.ts +15 -0
  79. package/src/lib/search-index.generated.ts +4 -0
  80. package/src/lib/search.ts +100 -0
  81. package/src/lib/site-config.ts +104 -0
  82. package/src/lib/site-model.ts +221 -0
  83. package/src/lib/slug.ts +38 -0
  84. package/src/lib/types.ts +478 -0
  85. package/src/lib/utils.ts +6 -0
  86. package/src/pages/DocPage.tsx +33 -0
  87. package/src/styles/global.css +268 -0
  88. package/src/styles/tokens.css +114 -0
  89. package/types/client.d.ts +3 -0
  90. package/types/search.d.ts +17 -0
  91. package/vite.config.ts +342 -0
  92. package/LICENSE +0 -21
  93. package/dist/index.css +0 -189
  94. package/dist/index.d.ts +0 -57
  95. package/dist/index.js +0 -464
  96. package/dist/index.mjs +0 -437
  97. package/server/index.d.ts +0 -30
  98. package/server/index.js +0 -189
  99. package/styles.css +0 -4766
@@ -0,0 +1,84 @@
1
+ import { ConfiguredIcon } from '@/components/ConfiguredIcon';
2
+ import type { NormalizedFooter } from '@/lib/types';
3
+
4
+ export function Footer({
5
+ footer,
6
+ className = '',
7
+ }: {
8
+ footer: NormalizedFooter | null;
9
+ className?: string;
10
+ }) {
11
+ if (!footer) {
12
+ return null;
13
+ }
14
+
15
+ const { socials, links: columns, attribution } = footer;
16
+
17
+ return (
18
+ <footer className={`mt-8 border-border border-t py-8 text-muted-foreground ${className}`}>
19
+ {columns.length > 0 && (
20
+ <div className="mb-8 grid grid-cols-[repeat(auto-fit,minmax(10rem,max-content))] gap-x-16 gap-y-8">
21
+ {columns.map((column, index) => (
22
+ <div key={column.header || index}>
23
+ {column.header && (
24
+ <div className="mb-3 text-sm font-semibold text-foreground">{column.header}</div>
25
+ )}
26
+ <ul className="m-0 flex list-none flex-col gap-2 p-0">
27
+ {column.items.map(item => (
28
+ <li key={item.href}>
29
+ <a
30
+ href={item.href}
31
+ target={item.target}
32
+ rel={item.target === '_blank' ? 'noreferrer' : undefined}
33
+ className="text-sm text-muted-foreground hover:text-foreground"
34
+ >
35
+ {item.label}
36
+ </a>
37
+ </li>
38
+ ))}
39
+ </ul>
40
+ </div>
41
+ ))}
42
+ </div>
43
+ )}
44
+ {attribution || socials.length > 0 ? (
45
+ <div className="flex items-center justify-between gap-4">
46
+ {attribution ? (
47
+ <span className="text-sm">
48
+ Powered by{' '}
49
+ <a
50
+ href="https://shiso.umami.is?ref=docs-footer"
51
+ className="font-bold hover:text-foreground"
52
+ >
53
+ shiso
54
+ </a>
55
+ </span>
56
+ ) : (
57
+ <span />
58
+ )}
59
+ {socials.length > 0 && (
60
+ <div className="flex items-center gap-1">
61
+ {socials.map(link => {
62
+ const iconOnly = !link.label;
63
+
64
+ return (
65
+ <a
66
+ key={link.href}
67
+ href={link.href}
68
+ target={link.target}
69
+ rel={link.target === '_blank' ? 'noreferrer' : undefined}
70
+ className={`inline-flex min-h-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground ${iconOnly ? 'size-8' : 'gap-1.5 px-2'}`}
71
+ aria-label={iconOnly ? link.ariaLabel || link.icon || link.href : undefined}
72
+ >
73
+ <ConfiguredIcon icon={link.icon} />
74
+ {link.label}
75
+ </a>
76
+ );
77
+ })}
78
+ </div>
79
+ )}
80
+ </div>
81
+ ) : null}
82
+ </footer>
83
+ );
84
+ }
@@ -0,0 +1,72 @@
1
+ import { ConfiguredIcon } from '@/components/ConfiguredIcon';
2
+ import { Search } from '@/components/Search';
3
+ import { ThemeToggle } from '@/components/ThemeToggle';
4
+ import { TopNav } from '@/components/TopNav';
5
+ import { DOCS_PREFIX } from '@/lib/paths';
6
+ import type { NormalizedLink, SiteModel } from '@/lib/types';
7
+
8
+ function NavbarLinkItem({ link, primary = false }: { link: NormalizedLink; primary?: boolean }) {
9
+ const iconOnly = !link.label;
10
+ const accessibleLabel = link.ariaLabel || link.icon || link.href;
11
+
12
+ return (
13
+ <a
14
+ href={link.href}
15
+ className={
16
+ primary
17
+ ? `ml-1 inline-flex items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground hover:opacity-90 ${iconOnly ? 'size-8' : 'gap-1.5 px-3.5 py-1.5'}`
18
+ : `inline-flex items-center rounded-md text-sm font-medium text-foreground hover:bg-accent hover:text-foreground ${iconOnly ? 'size-8 justify-center' : 'gap-1.5 px-2.5 py-1.5'}`
19
+ }
20
+ target={link.target}
21
+ rel={link.target === '_blank' ? 'noreferrer' : undefined}
22
+ aria-label={iconOnly ? accessibleLabel : undefined}
23
+ >
24
+ <ConfiguredIcon icon={link.icon} />
25
+ {link.label}
26
+ </a>
27
+ );
28
+ }
29
+
30
+ export function Header({ site }: { site: SiteModel }) {
31
+ const { logo, navbar, name, appearance, docs, labels, search } = site;
32
+ const brandHref = logo?.href || DOCS_PREFIX || '/';
33
+ const hasBrand = !!name || !!logo?.light || !!logo?.dark;
34
+
35
+ return (
36
+ <header className="sticky top-0 z-50 h-[var(--header-height)] shrink-0 border-border border-b bg-[color-mix(in_srgb,var(--background)_92%,transparent)] backdrop-blur-md">
37
+ <div className="mx-auto grid h-full max-w-[1600px] grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center px-5">
38
+ <div className="flex min-w-0 items-center gap-5 justify-self-start">
39
+ {hasBrand ? (
40
+ <a
41
+ href={brandHref}
42
+ target={logo?.target}
43
+ rel={logo?.target === '_blank' ? 'noreferrer' : undefined}
44
+ className="inline-flex items-center gap-2 text-xl font-bold text-foreground tracking-[-0.03em]"
45
+ >
46
+ {logo?.light ? (
47
+ <img src={logo.light} alt="" className="h-6 w-auto dark:hidden" />
48
+ ) : null}
49
+ {logo?.dark ? (
50
+ <img
51
+ src={logo.dark}
52
+ alt=""
53
+ className={`hidden h-6 w-auto dark:block ${logo.light === logo.dark ? 'dark:invert' : ''}`}
54
+ />
55
+ ) : null}
56
+ {name ? <span>{name}</span> : null}
57
+ </a>
58
+ ) : null}
59
+ </div>
60
+ {docs.showTabs ? <TopNav docs={docs} label={labels.sections} /> : null}
61
+ <div className="flex min-w-0 items-center gap-2 justify-self-end">
62
+ <Search config={search} labels={labels} />
63
+ {navbar?.links.map(link => (
64
+ <NavbarLinkItem key={link.href} link={link} />
65
+ ))}
66
+ {!appearance.strict && <ThemeToggle label={labels.toggleTheme} />}
67
+ {navbar?.primary ? <NavbarLinkItem link={navbar.primary} primary /> : null}
68
+ </div>
69
+ </div>
70
+ </header>
71
+ );
72
+ }
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Banner } from '@/components/Banner';
3
+ import { Header } from '@/components/Header';
4
+ import type { SiteModel } from '@/lib/types';
5
+
6
+ export function Layout({ children, site }: { children: ReactNode; site: SiteModel }) {
7
+ return (
8
+ <div className="flex min-h-dvh flex-col">
9
+ <Banner banner={site.banner} dismissLabel={site.labels.dismissBanner} />
10
+ <Header site={site} />
11
+ <div className="mx-auto flex min-h-0 w-full max-w-[1600px] grow flex-col px-5">
12
+ <main className="min-h-0 grow">{children}</main>
13
+ </div>
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,71 @@
1
+ import classNames from 'classnames';
2
+ import { useEffect, useState } from 'react';
3
+ import type { TocEntry } from '@/lib/types';
4
+
5
+ export interface PageLinksProps {
6
+ items?: TocEntry[];
7
+ title: string;
8
+ navigationLabel: string;
9
+ }
10
+
11
+ export function PageLinks({ items = [], title, navigationLabel }: PageLinksProps) {
12
+ const [hash, setHash] = useState(items?.[0]?.id);
13
+
14
+ useEffect(() => {
15
+ setHash(items?.[0]?.id);
16
+
17
+ const callback = () => {
18
+ const found = [...items].reverse().find(({ id }) => {
19
+ const rect = document.getElementById(id)?.getBoundingClientRect();
20
+ return rect && rect.top <= 0;
21
+ });
22
+
23
+ if (found) {
24
+ setHash(found.id);
25
+ }
26
+ };
27
+
28
+ window.addEventListener('scroll', callback, false);
29
+
30
+ return () => {
31
+ window.removeEventListener('scroll', callback, false);
32
+ };
33
+ }, [items]);
34
+
35
+ if (!items?.length) {
36
+ return null;
37
+ }
38
+
39
+ const indent = (size: number) => {
40
+ if (size <= 2) {
41
+ return '0px';
42
+ }
43
+
44
+ return `${(size - 2) * 10}px`;
45
+ };
46
+
47
+ return (
48
+ <div className="flex h-max min-w-60 flex-col gap-3 text-sm">
49
+ <div className="font-bold text-foreground">{title}</div>
50
+ <nav className="flex flex-col" aria-label={navigationLabel}>
51
+ {items.map(({ name, id, size }) => {
52
+ const isActive = hash === id;
53
+
54
+ return (
55
+ <a
56
+ key={id}
57
+ href={`#${id}`}
58
+ className={classNames('block border-l px-3 py-1.5', {
59
+ 'border-l-primary font-medium text-primary': isActive,
60
+ 'border-border text-muted-foreground hover:text-foreground': !isActive,
61
+ })}
62
+ aria-current={isActive ? 'location' : undefined}
63
+ >
64
+ <span style={{ marginLeft: indent(size), display: 'block' }}>{name}</span>
65
+ </a>
66
+ );
67
+ })}
68
+ </nav>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,208 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { useNavigate } from 'react-router';
3
+ import { Search as SearchIcon } from '@/components/icons';
4
+ import { Button } from '@/components/ui/button';
5
+ import {
6
+ Command,
7
+ CommandEmpty,
8
+ CommandGroup,
9
+ CommandInput,
10
+ CommandItem,
11
+ CommandList,
12
+ } from '@/components/ui/command';
13
+ import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
14
+ import type { SearchResult } from '@/lib/search';
15
+ import type { ResolvedSearchConfig } from '@/lib/search/config';
16
+ import { resolveSearchProvider, type SearchProvider } from '@/lib/search/provider';
17
+ import type { ThemeLabels } from '@/lib/types';
18
+
19
+ /**
20
+ * Provider-neutral search dialog. The selected provider and its index or
21
+ * client are loaded on demand, so search stays out of the initial bundle.
22
+ */
23
+ export function Search({ config, labels }: { config: ResolvedSearchConfig; labels: ThemeLabels }) {
24
+ const navigate = useNavigate();
25
+ const [open, setOpen] = useState(false);
26
+ const [query, setQuery] = useState('');
27
+ const [results, setResults] = useState<SearchResult[]>([]);
28
+ const [status, setStatus] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle');
29
+ const provider = useRef<Promise<SearchProvider> | null>(null);
30
+ const requestId = useRef(0);
31
+
32
+ const loadProvider = useCallback(() => {
33
+ if (!provider.current) {
34
+ provider.current = resolveSearchProvider(config.provider, config.options)
35
+ .then(resolved => {
36
+ if (resolved.fellBack) {
37
+ console.warn(
38
+ `[shiso] Unknown search provider "${config.provider}" — using the built-in local provider.`,
39
+ );
40
+ }
41
+
42
+ return resolved.provider;
43
+ })
44
+ .catch(error => {
45
+ provider.current = null;
46
+ throw error;
47
+ });
48
+ }
49
+
50
+ return provider.current;
51
+ }, [config.options, config.provider]);
52
+
53
+ const runQuery = useCallback(
54
+ async (value: string) => {
55
+ const currentRequest = ++requestId.current;
56
+ setQuery(value);
57
+
58
+ if (!value.trim()) {
59
+ setResults([]);
60
+ setStatus('idle');
61
+ return;
62
+ }
63
+
64
+ setResults([]);
65
+ setStatus('loading');
66
+
67
+ try {
68
+ const activeProvider = await loadProvider();
69
+ const nextResults = await activeProvider.search(value);
70
+
71
+ if (currentRequest === requestId.current) {
72
+ setResults(nextResults);
73
+ setStatus('ready');
74
+ }
75
+ } catch (error) {
76
+ if (currentRequest === requestId.current) {
77
+ console.error('[shiso] Search provider failed:', error);
78
+ setResults([]);
79
+ setStatus('error');
80
+ }
81
+ }
82
+ },
83
+ [loadProvider],
84
+ );
85
+
86
+ const openDialog = useCallback(() => {
87
+ if (!config.enabled) {
88
+ return;
89
+ }
90
+
91
+ setOpen(true);
92
+ void loadProvider().catch(error => {
93
+ console.error('[shiso] Search provider failed to load:', error);
94
+ });
95
+ }, [config.enabled, loadProvider]);
96
+
97
+ const closeDialog = useCallback(() => {
98
+ requestId.current += 1;
99
+ setOpen(false);
100
+ setQuery('');
101
+ setResults([]);
102
+ setStatus('idle');
103
+ }, []);
104
+
105
+ // Global shortcut configured by docs.json.
106
+ useEffect(() => {
107
+ const onKeyDown = (event: KeyboardEvent) => {
108
+ if (
109
+ config.enabled &&
110
+ config.shortcut &&
111
+ (event.metaKey || event.ctrlKey) &&
112
+ event.key.toLowerCase() === config.shortcut
113
+ ) {
114
+ event.preventDefault();
115
+ openDialog();
116
+ }
117
+ };
118
+
119
+ window.addEventListener('keydown', onKeyDown);
120
+ return () => window.removeEventListener('keydown', onKeyDown);
121
+ }, [config.enabled, config.shortcut, openDialog]);
122
+
123
+ const select = (result: SearchResult) => {
124
+ closeDialog();
125
+ navigate(result.url);
126
+ };
127
+
128
+ if (!config.enabled) {
129
+ return null;
130
+ }
131
+
132
+ const hasQuery = !!query.trim();
133
+
134
+ return (
135
+ <Dialog open={open} onOpenChange={nextOpen => (nextOpen ? openDialog() : closeDialog())}>
136
+ <DialogTrigger
137
+ render={
138
+ <Button
139
+ variant="outline"
140
+ className="h-auto gap-2 rounded-md bg-card px-2.5 py-1.5 text-muted-foreground hover:border-input hover:bg-card hover:text-foreground"
141
+ />
142
+ }
143
+ >
144
+ <SearchIcon className="size-3.5" />
145
+ <span className="min-w-24 text-left">{config.prompt}</span>
146
+ {config.shortcut ? (
147
+ <kbd className="rounded-sm border border-border bg-muted px-[0.3rem] py-[0.05rem] text-[0.7rem] font-sans">
148
+ {config.shortcutLabel}
149
+ </kbd>
150
+ ) : null}
151
+ </DialogTrigger>
152
+ <DialogContent
153
+ showCloseButton={false}
154
+ overlayClassName="bg-black/40 supports-backdrop-filter:backdrop-blur-none"
155
+ className="top-[10vh] max-w-[calc(100%-2rem)] -translate-y-0 gap-0 overflow-hidden rounded-lg border border-border bg-background p-0 ring-0 shadow-[0_10px_40px_rgba(0,0,0,0.2)] sm:max-w-[34rem]"
156
+ >
157
+ <DialogTitle className="sr-only">{labels.searchTitle}</DialogTitle>
158
+ <Command shouldFilter={false} className="rounded-none bg-background p-0">
159
+ <CommandInput
160
+ autoFocus
161
+ className="text-base md:text-base"
162
+ placeholder={config.prompt}
163
+ value={query}
164
+ onValueChange={runQuery}
165
+ onKeyDownCapture={event => {
166
+ if (event.key === 'Home' || event.key === 'End') {
167
+ event.stopPropagation();
168
+ }
169
+ }}
170
+ />
171
+ <CommandList className="max-h-[50vh] border-border border-t">
172
+ {hasQuery ? (
173
+ <CommandEmpty>
174
+ {status === 'loading'
175
+ ? labels.searching
176
+ : status === 'error'
177
+ ? labels.searchUnavailable
178
+ : labels.noResults}
179
+ </CommandEmpty>
180
+ ) : null}
181
+ {hasQuery && results.length ? (
182
+ <CommandGroup className="p-2">
183
+ {results.map(result => (
184
+ <CommandItem
185
+ key={result.url}
186
+ value={result.url}
187
+ className="block whitespace-normal rounded-md px-3 py-2 text-left [&>svg:last-child]:hidden"
188
+ onSelect={() => select(result)}
189
+ >
190
+ <div className="text-[0.9rem] font-semibold text-foreground">
191
+ {result.page}
192
+ {result.heading ? ` › ${result.heading}` : ''}
193
+ </div>
194
+ {result.snippet && (
195
+ <div className="mt-[0.15rem] line-clamp-2 text-[0.8rem] text-muted-foreground">
196
+ {result.snippet}
197
+ </div>
198
+ )}
199
+ </CommandItem>
200
+ ))}
201
+ </CommandGroup>
202
+ ) : null}
203
+ </CommandList>
204
+ </Command>
205
+ </DialogContent>
206
+ </Dialog>
207
+ );
208
+ }