@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,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
|
+
}
|