@umami/shiso 0.61.0 → 1.0.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.
@@ -3,12 +3,14 @@ import { useLocation, useNavigate } from 'react-router';
3
3
  import { DocContent } from '@/components/DocContent';
4
4
  import { Footer } from '@/components/Footer';
5
5
  import { Menu } from '@/components/icons';
6
+ import { LanguageSwitcher } from '@/components/LanguageSwitcher';
6
7
  import { PageLinks } from '@/components/PageLinks';
7
8
  import { SideNav } from '@/components/SideNav';
8
9
  import { Button } from '@/components/ui/button';
9
10
  import { Sheet, SheetContent, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
11
+ import { VersionSwitcher } from '@/components/VersionSwitcher';
10
12
  import { renderInlineMarkdown } from '@/lib/inline-markdown';
11
- import { DOCS_PREFIX } from '@/lib/paths';
13
+ import { docsHomeUrl, getScopeByPathname } from '@/lib/site-config';
12
14
  import type { DocModule, NormalizedDocsPage, SiteModel } from '@/lib/types';
13
15
 
14
16
  /**
@@ -23,7 +25,7 @@ function NotFound({ site }: { site: SiteModel }) {
23
25
 
24
26
  useEffect(() => {
25
27
  if (redirect) {
26
- navigate(DOCS_PREFIX || '/', { replace: true });
28
+ navigate(docsHomeUrl, { replace: true });
27
29
  }
28
30
  }, [redirect, navigate]);
29
31
 
@@ -44,7 +46,9 @@ export interface DocsProps {
44
46
  export function Docs({ page, doc, site }: DocsProps) {
45
47
  const { pathname } = useLocation();
46
48
  const [menuOpen, setMenuOpen] = useState(false);
47
- const { tabs, navigation } = site.docs;
49
+ // Navigation follows the scope (version/language) that owns the current page.
50
+ const scopeDocs = getScopeByPathname(pathname).docs;
51
+ const { tabs, navigation } = scopeDocs;
48
52
 
49
53
  // Close the mobile menu and start each newly loaded page at the top. Hash
50
54
  // links keep their native section-scrolling behavior.
@@ -82,10 +86,14 @@ export function Docs({ page, doc, site }: DocsProps) {
82
86
  >
83
87
  <SheetTitle className="sr-only">{site.labels.documentationNavigation}</SheetTitle>
84
88
  <div className="pt-8">
89
+ <div className="flex items-center gap-2 pb-4 lg:hidden">
90
+ <VersionSwitcher />
91
+ <LanguageSwitcher />
92
+ </div>
85
93
  <SideNav
86
94
  tabs={tabs}
87
95
  navigation={navigation}
88
- anchors={site.docs.anchors}
96
+ anchors={scopeDocs.anchors}
89
97
  drilldown={site.drilldown}
90
98
  navigationLabel={site.labels.documentationNavigation}
91
99
  expandLabel={site.labels.expand}
@@ -99,7 +107,7 @@ export function Docs({ page, doc, site }: DocsProps) {
99
107
  <SideNav
100
108
  tabs={tabs}
101
109
  navigation={navigation}
102
- anchors={site.docs.anchors}
110
+ anchors={scopeDocs.anchors}
103
111
  isSticky
104
112
  drilldown={site.drilldown}
105
113
  navigationLabel={site.labels.documentationNavigation}
@@ -44,15 +44,12 @@ export function Footer({
44
44
  {attribution || socials.length > 0 ? (
45
45
  <div className="flex items-center justify-between gap-4">
46
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>
47
+ <a
48
+ href="https://shiso.umami.is?ref=docs-footer"
49
+ className="text-sm hover:text-foreground"
50
+ >
51
+ Powered by <span className="font-bold">shiso</span>
52
+ </a>
56
53
  ) : (
57
54
  <span />
58
55
  )}
@@ -1,8 +1,11 @@
1
+ import { useLocation } from 'react-router';
1
2
  import { ConfiguredIcon } from '@/components/ConfiguredIcon';
3
+ import { LanguageSwitcher } from '@/components/LanguageSwitcher';
2
4
  import { Search } from '@/components/Search';
3
5
  import { ThemeToggle } from '@/components/ThemeToggle';
4
6
  import { TopNav } from '@/components/TopNav';
5
- import { DOCS_PREFIX } from '@/lib/paths';
7
+ import { VersionSwitcher } from '@/components/VersionSwitcher';
8
+ import { docsHomeUrl, getScopeByPathname } from '@/lib/site-config';
6
9
  import type { NormalizedLink, SiteModel } from '@/lib/types';
7
10
 
8
11
  function NavbarLinkItem({ link, primary = false }: { link: NormalizedLink; primary?: boolean }) {
@@ -28,8 +31,11 @@ function NavbarLinkItem({ link, primary = false }: { link: NormalizedLink; prima
28
31
  }
29
32
 
30
33
  export function Header({ site }: { site: SiteModel }) {
31
- const { logo, navbar, name, appearance, docs, labels, search } = site;
32
- const brandHref = logo?.href || DOCS_PREFIX || '/';
34
+ const { logo, navbar, name, appearance, labels, search } = site;
35
+ const { pathname } = useLocation();
36
+ // The header renders the navigation of whichever scope owns the current page.
37
+ const docs = getScopeByPathname(pathname).docs;
38
+ const brandHref = logo?.href || docsHomeUrl;
33
39
  const hasBrand = !!name || !!logo?.light || !!logo?.dark;
34
40
 
35
41
  return (
@@ -56,6 +62,10 @@ export function Header({ site }: { site: SiteModel }) {
56
62
  {name ? <span>{name}</span> : null}
57
63
  </a>
58
64
  ) : null}
65
+ <div className="hidden items-center gap-2 lg:flex">
66
+ <VersionSwitcher />
67
+ <LanguageSwitcher />
68
+ </div>
59
69
  </div>
60
70
  {docs.showTabs ? <TopNav docs={docs} label={labels.sections} /> : null}
61
71
  <div className="flex min-w-0 items-center gap-2 justify-self-end">
@@ -0,0 +1,59 @@
1
+ import { useLocation, useNavigate } from 'react-router';
2
+ import { Check, ChevronRight } from '@/components/icons';
3
+ import { Button } from '@/components/ui/button';
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from '@/components/ui/dropdown-menu';
10
+ import { getLanguageScopes } from '@/lib/docs-config';
11
+ import { docsSite, getScopeByPathname } from '@/lib/site-config';
12
+
13
+ /**
14
+ * Language selector for multi-language sites. Each option is a language's
15
+ * landing scope — its default version — so switching languages always lands
16
+ * on that language's default-version first page. Hidden languages never
17
+ * appear as options.
18
+ */
19
+ export function LanguageSwitcher() {
20
+ const { pathname } = useLocation();
21
+ const navigate = useNavigate();
22
+ const current = getScopeByPathname(pathname);
23
+ const options = getLanguageScopes(docsSite);
24
+
25
+ if (!current.language || (options.length < 2 && !current.hidden)) {
26
+ return null;
27
+ }
28
+
29
+ return (
30
+ <DropdownMenu>
31
+ <DropdownMenuTrigger
32
+ render={
33
+ <Button
34
+ variant="outline"
35
+ className="h-auto gap-1.5 rounded-md bg-card px-2.5 py-1.5 text-sm font-medium text-foreground"
36
+ />
37
+ }
38
+ >
39
+ {current.language}
40
+ <ChevronRight className="size-3.5 rotate-90 text-muted-foreground" />
41
+ </DropdownMenuTrigger>
42
+ <DropdownMenuContent align="start" className="min-w-32">
43
+ {options.map(scope => (
44
+ <DropdownMenuItem
45
+ key={scope.id}
46
+ onClick={() => {
47
+ if (scope.language !== current.language) {
48
+ navigate(scope.firstPageUrl);
49
+ }
50
+ }}
51
+ >
52
+ <span className="grow">{scope.language}</span>
53
+ {scope.language === current.language ? <Check className="size-3.5" /> : null}
54
+ </DropdownMenuItem>
55
+ ))}
56
+ </DropdownMenuContent>
57
+ </DropdownMenu>
58
+ );
59
+ }
@@ -1,9 +1,17 @@
1
1
  import type { ReactNode } from 'react';
2
+ import { useLocation } from 'react-router';
2
3
  import { Banner } from '@/components/Banner';
3
4
  import { Header } from '@/components/Header';
5
+ import { useHead } from '@/lib/head';
4
6
  import type { SiteModel } from '@/lib/types';
5
7
 
6
8
  export function Layout({ children, site }: { children: ReactNode; site: SiteModel }) {
9
+ const { pathname } = useLocation();
10
+
11
+ // Keep metadata, document language, and text direction synchronized after
12
+ // client-side navigation. The prerendered values are retained on hydration.
13
+ useHead(pathname);
14
+
7
15
  return (
8
16
  <div className="flex min-h-dvh flex-col">
9
17
  <Banner banner={site.banner} dismissLabel={site.labels.dismissBanner} />
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
- import { useNavigate } from 'react-router';
2
+ import { useLocation, useNavigate } from 'react-router';
3
3
  import { Search as SearchIcon } from '@/components/icons';
4
4
  import { Button } from '@/components/ui/button';
5
5
  import {
@@ -14,6 +14,7 @@ import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '@/components/
14
14
  import type { SearchResult } from '@/lib/search';
15
15
  import type { ResolvedSearchConfig } from '@/lib/search/config';
16
16
  import { resolveSearchProvider, type SearchProvider } from '@/lib/search/provider';
17
+ import { getScopeByPathname } from '@/lib/site-config';
17
18
  import type { ThemeLabels } from '@/lib/types';
18
19
 
19
20
  /**
@@ -22,6 +23,7 @@ import type { ThemeLabels } from '@/lib/types';
22
23
  */
23
24
  export function Search({ config, labels }: { config: ResolvedSearchConfig; labels: ThemeLabels }) {
24
25
  const navigate = useNavigate();
26
+ const { pathname } = useLocation();
25
27
  const [open, setOpen] = useState(false);
26
28
  const [query, setQuery] = useState('');
27
29
  const [results, setResults] = useState<SearchResult[]>([]);
@@ -66,7 +68,14 @@ export function Search({ config, labels }: { config: ResolvedSearchConfig; label
66
68
 
67
69
  try {
68
70
  const activeProvider = await loadProvider();
69
- const nextResults = await activeProvider.search(value);
71
+ // Search stays inside the version/language scope being browsed.
72
+ // Providers that predate the context argument simply ignore it.
73
+ const scope = getScopeByPathname(pathname);
74
+ const nextResults = await activeProvider.search(value, undefined, {
75
+ scopeId: scope.id,
76
+ language: scope.language,
77
+ version: scope.version,
78
+ });
70
79
 
71
80
  if (currentRequest === requestId.current) {
72
81
  setResults(nextResults);
@@ -80,7 +89,7 @@ export function Search({ config, labels }: { config: ResolvedSearchConfig; label
80
89
  }
81
90
  }
82
91
  },
83
- [loadProvider],
92
+ [loadProvider, pathname],
84
93
  );
85
94
 
86
95
  const openDialog = useCallback(() => {
@@ -0,0 +1,60 @@
1
+ import { useLocation, useNavigate } from 'react-router';
2
+ import { Check, ChevronRight } from '@/components/icons';
3
+ import { Button } from '@/components/ui/button';
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from '@/components/ui/dropdown-menu';
10
+ import { docsSite, getScopeByPathname } from '@/lib/site-config';
11
+
12
+ /**
13
+ * Version selector for multi-version sites. Options come from the visible
14
+ * version scopes of the active language; selecting one navigates to that
15
+ * scope's first visible page. Hidden versions never appear as options, but a
16
+ * hidden version still shows as the current value while it is being viewed.
17
+ */
18
+ export function VersionSwitcher() {
19
+ const { pathname } = useLocation();
20
+ const navigate = useNavigate();
21
+ const current = getScopeByPathname(pathname);
22
+ const options = docsSite.scopes.filter(
23
+ scope => scope.version && !scope.hidden && scope.language === current.language,
24
+ );
25
+
26
+ if (!current.version || (options.length < 2 && !current.hidden)) {
27
+ return null;
28
+ }
29
+
30
+ return (
31
+ <DropdownMenu>
32
+ <DropdownMenuTrigger
33
+ render={
34
+ <Button
35
+ variant="outline"
36
+ className="h-auto gap-1.5 rounded-md bg-card px-2.5 py-1.5 text-sm font-medium text-foreground"
37
+ />
38
+ }
39
+ >
40
+ {current.version}
41
+ <ChevronRight className="size-3.5 rotate-90 text-muted-foreground" />
42
+ </DropdownMenuTrigger>
43
+ <DropdownMenuContent align="start" className="min-w-32">
44
+ {options.map(scope => (
45
+ <DropdownMenuItem
46
+ key={scope.id}
47
+ onClick={() => {
48
+ if (scope.id !== current.id) {
49
+ navigate(scope.firstPageUrl);
50
+ }
51
+ }}
52
+ >
53
+ <span className="grow">{scope.version}</span>
54
+ {scope.id === current.id ? <Check className="size-3.5" /> : null}
55
+ </DropdownMenuItem>
56
+ ))}
57
+ </DropdownMenuContent>
58
+ </DropdownMenu>
59
+ );
60
+ }
@@ -23,17 +23,17 @@ export function PropertiesTable({ children }: PropertiesTableProps) {
23
23
  }
24
24
 
25
25
  return (
26
- <div className="my-4 overflow-x-auto rounded-lg border border-border p-6">
26
+ <div className="my-4 overflow-x-auto rounded-lg border border-border px-6 py-2">
27
27
  <table className="m-0 min-w-[40rem] table-fixed border-collapse">
28
28
  <thead>
29
29
  <tr>
30
- <th className="w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-3 pr-6 text-left text-sm font-semibold text-foreground">
30
+ <th className="w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 pr-6 text-left text-sm font-semibold text-foreground">
31
31
  Name
32
32
  </th>
33
- <th className="w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-3 pr-6 text-left text-sm font-semibold text-foreground">
33
+ <th className="w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 pr-6 text-left text-sm font-semibold text-foreground">
34
34
  Type
35
35
  </th>
36
- <th className="w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-3 text-left text-sm font-semibold text-foreground">
36
+ <th className="w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 text-left text-sm font-semibold text-foreground">
37
37
  Description
38
38
  </th>
39
39
  </tr>
@@ -47,7 +47,7 @@ export function PropertiesTable({ children }: PropertiesTableProps) {
47
47
  return (
48
48
  <tr key={row.key ?? name}>
49
49
  <td
50
- className={`${borderClass} border-x-0 border-t-0 border-border px-0 py-3 pr-6 align-top text-sm text-foreground`}
50
+ className={`${borderClass} border-x-0 border-t-0 border-border px-0 py-2 pr-6 align-top text-sm text-foreground`}
51
51
  >
52
52
  <span className="inline-flex flex-wrap items-center gap-2">
53
53
  {name}
@@ -60,12 +60,12 @@ export function PropertiesTable({ children }: PropertiesTableProps) {
60
60
  </span>
61
61
  </td>
62
62
  <td
63
- className={`${borderClass} border-x-0 border-t-0 border-border px-0 py-3 pr-6 align-top text-sm text-foreground`}
63
+ className={`${borderClass} border-x-0 border-t-0 border-border px-0 py-2 pr-6 align-top text-sm text-foreground`}
64
64
  >
65
65
  {displayValue(type)}
66
66
  </td>
67
67
  <td
68
- className={`${borderClass} border-x-0 border-t-0 border-border px-0 py-3 align-top text-sm leading-6 text-foreground [&_p]:m-0`}
68
+ className={`${borderClass} border-x-0 border-t-0 border-border px-0 py-2 align-top text-sm leading-6 text-foreground [&_p]:m-0`}
69
69
  >
70
70
  {description}
71
71
  {defaultValue ? (
@@ -2,13 +2,23 @@ import { renderToString } from 'react-dom/server';
2
2
  import { StaticRouter } from 'react-router';
3
3
  import { App } from '@/App';
4
4
  import { getDocModule, getLastModified } from '@/lib/content';
5
+ import { getScopeForPage } from '@/lib/docs-config';
5
6
  import { buildHead, renderHeadToString } from '@/lib/head';
6
7
  import { BASE_URL, toAbsoluteUrl } from '@/lib/paths';
7
- import { docsConfig, getRedirects, getSeo, siteName } from '@/lib/site-config';
8
+ import {
9
+ docsHomeUrl,
10
+ docsSite,
11
+ getLocaleByPathname,
12
+ getRedirects,
13
+ getSeo,
14
+ siteName,
15
+ } from '@/lib/site-config';
8
16
 
9
17
  export interface RenderResult {
10
18
  html: string;
11
19
  head: string;
20
+ /** Attributes for the document element: language and text direction. */
21
+ htmlAttrs: { lang: string; dir: 'ltr' | 'rtl' };
12
22
  }
13
23
 
14
24
  export interface SitemapEntry {
@@ -16,9 +26,9 @@ export interface SitemapEntry {
16
26
  lastmod?: string;
17
27
  }
18
28
 
19
- /** Base-relative routes. The prerenderer prepends the deploy base itself. */
29
+ /** Base-relative routes for every scope. The prerenderer prepends the deploy base itself. */
20
30
  export function getRoutes(): string[] {
21
- return docsConfig.pages.map(page => page.url);
31
+ return docsSite.pages.map(page => page.url);
22
32
  }
23
33
 
24
34
  /** Redirect rules with exact-match sources, for static redirect pages. */
@@ -31,7 +41,7 @@ export { getRedirects };
31
41
  * "/content/docs/index.mdx", resolved against the project root.
32
42
  */
33
43
  export function getMarkdownPages(): { route: string; filePath: string }[] {
34
- return docsConfig.pages.map(page => ({ route: page.url, filePath: page.filePath }));
44
+ return docsSite.pages.map(page => ({ route: page.url, filePath: page.filePath }));
35
45
  }
36
46
 
37
47
  /**
@@ -43,8 +53,10 @@ export function getSitemapEntries(): SitemapEntry[] {
43
53
  const { indexing } = getSeo();
44
54
  const entries: SitemapEntry[] = [];
45
55
 
46
- for (const page of docsConfig.pages) {
47
- if (page.hidden && indexing !== 'all') {
56
+ for (const page of docsSite.pages) {
57
+ // Hidden pages and pages of hidden scopes (versions/languages) stay out
58
+ // of the sitemap unless `seo.indexing: "all"` opts them in.
59
+ if ((page.hidden || getScopeForPage(docsSite, page).hidden) && indexing !== 'all') {
48
60
  continue;
49
61
  }
50
62
 
@@ -71,7 +83,7 @@ export function render(url: string): RenderResult {
71
83
  </StaticRouter>,
72
84
  );
73
85
 
74
- return { html, head: renderHeadToString(buildHead(url)) };
86
+ return { html, head: renderHeadToString(buildHead(url)), htmlAttrs: getLocaleByPathname(url) };
75
87
  }
76
88
 
77
- export { siteName };
89
+ export { docsHomeUrl, siteName };