@terrafirmagreg-team/kit 1.2.2 → 1.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/kit",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "type": "module",
5
5
  "description": "Fumadocs content engine",
6
6
  "license": "LGPL-3.0-only",
@@ -0,0 +1,192 @@
1
+ 'use client';
2
+ import {
3
+ useMemo,
4
+ type ComponentProps,
5
+ type ComponentType,
6
+ type ReactNode,
7
+ } from 'react';
8
+ import type * as PageTree from 'fumadocs-core/page-tree';
9
+ import { Layout as UiLayout } from '@terrafirmagreg-team/ui/components/layouts/layout';
10
+ import {
11
+ LeftSidebar,
12
+ LeftSidebarActions,
13
+ LeftSidebarLanguage,
14
+ LeftSidebarSearch,
15
+ LeftSidebarSeparator,
16
+ } from '@terrafirmagreg-team/ui/components/layouts/left-sidebar';
17
+ import { Switcher } from '@terrafirmagreg-team/ui/components/blocks/nav/switcher';
18
+ import {
19
+ SiteMenu,
20
+ type SiteMenuItem,
21
+ } from '@terrafirmagreg-team/ui/components/blocks/nav/site-menu';
22
+ import { buttonVariants } from '@terrafirmagreg-team/ui/components/ui/button';
23
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
24
+ import { Globe } from 'lucide-react';
25
+ import {
26
+ SiCurseforge,
27
+ SiDiscord,
28
+ SiGithub,
29
+ SiModrinth,
30
+ } from '@icons-pack/react-simple-icons';
31
+ import { usePathname } from 'next/navigation';
32
+ import { ReadabilityMenu } from '@terrafirmagreg-team/ui/components/blocks/readability/menu';
33
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
34
+ import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
35
+ import { useLocaleRegistry } from '@terrafirmagreg-team/translate/hooks/use-locale-registry';
36
+ import { LangLocalesScope } from '@terrafirmagreg-team/translate/provider';
37
+ import { localeItems } from '@terrafirmagreg-team/translate/create';
38
+ import { siteLinks } from '@terrafirmagreg-team/sites';
39
+ import { useCurrentDataset } from '@terrafirmagreg-team/kit/hooks/use-current-dataset';
40
+ import { useDatasetAccent } from '@terrafirmagreg-team/kit/hooks/use-dataset-accent';
41
+ import { useNavOptions } from '@terrafirmagreg-team/kit/hooks/use-nav-options';
42
+ import { pinTreeDataset } from '@terrafirmagreg-team/kit/lib/pin-tree';
43
+ import { layoutDeps, sectionConfig } from '@terrafirmagreg-team/kit/lib/config';
44
+ import type {
45
+ DatasetCatalog,
46
+ LeftSidebarSlotProps,
47
+ SectionKey,
48
+ SectionTrees,
49
+ } from '@terrafirmagreg-team/kit/types';
50
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
51
+
52
+ export type LayoutProps = {
53
+ section: SectionKey;
54
+ trees: SectionTrees;
55
+ catalog: DatasetCatalog;
56
+
57
+ useTree?: (tree: PageTree.Root) => PageTree.Root;
58
+ sidebar?: Pick<LeftSidebarSlotProps, 'feature'>;
59
+ navTitle?: ComponentType<ComponentProps<'a'>>;
60
+ children?: ReactNode;
61
+ };
62
+
63
+ const NAV_KEYS = new Set(['book', 'cat', 'entry', 'category', 'type']);
64
+
65
+ const LINK_ICONS: Record<string, ReactNode> = {
66
+ github: <SiGithub />,
67
+ curseforge: <SiCurseforge />,
68
+ modrinth: <SiModrinth />,
69
+ discord: <SiDiscord />,
70
+ };
71
+
72
+ const identityTree = (tree: PageTree.Root) => tree;
73
+
74
+ export function Layout({
75
+ section,
76
+ trees,
77
+ catalog,
78
+ useTree = identityTree,
79
+ sidebar,
80
+ navTitle,
81
+ children,
82
+ }: LayoutProps) {
83
+ const t = useTranslations();
84
+ const { defaultLanguage } = useLocaleRegistry();
85
+ const lang: Lang = useResolvedLang();
86
+ const current = useCurrentDataset(catalog[section]);
87
+ const baseId = current ? current.id : undefined;
88
+ const pinned = current ? current.version : undefined;
89
+ const meta = catalog[section].find((item) => item.id === baseId);
90
+ useDatasetAccent(meta?.accent);
91
+ const nav = useNavOptions({ section, catalog });
92
+ const pathname = usePathname();
93
+ const deps = layoutDeps();
94
+ deps.trackView?.(pathname);
95
+ const commentsItem =
96
+ (deps.commentsMenuItem?.() as SiteMenuItem | false | null) ?? false;
97
+ const Feature = sidebar?.feature;
98
+ const scoped = baseId
99
+ ? (trees[lang]?.[baseId] ?? trees[defaultLanguage]?.[baseId])
100
+ : undefined;
101
+ const serverTree = scoped ?? {
102
+ name: section,
103
+ children: [],
104
+ $id: `${section}:empty`,
105
+ };
106
+ const tree = useTree(serverTree);
107
+ const pinnedTree = useMemo(
108
+ () =>
109
+ pinned && baseId
110
+ ? pinTreeDataset(tree, section, baseId, pinned)
111
+ : tree,
112
+ [tree, section, baseId, pinned],
113
+ );
114
+ const locales = meta?.languages ? localeItems(meta.languages) : undefined;
115
+ const links = baseId
116
+ ? sectionConfig().datasetConfig[baseId]?.links
117
+ : undefined;
118
+ const menuItems: (SiteMenuItem | 'separator' | false)[] = [
119
+ commentsItem,
120
+ { label: t('Website', { note: 'ui.website' }), icon: <Globe />, url: siteLinks.website },
121
+ 'separator',
122
+ { label: 'Discord', icon: <SiDiscord />, url: siteLinks.discord, external: true },
123
+ { label: 'GitHub', icon: <SiGithub />, url: siteLinks.github, external: true },
124
+ {
125
+ label: 'CurseForge',
126
+ icon: <SiCurseforge />,
127
+ url: siteLinks.curseforge,
128
+ external: true,
129
+ },
130
+ {
131
+ label: 'Modrinth',
132
+ icon: <SiModrinth />,
133
+ url: siteLinks.modrinth,
134
+ external: true,
135
+ },
136
+ ];
137
+
138
+ const layout = (
139
+ <UiLayout
140
+ tree={pinnedTree}
141
+ navTitle={navTitle}
142
+ sidebar={
143
+ <LeftSidebar
144
+ navKeys={NAV_KEYS}
145
+ menu={<SiteMenu label={t('Site menu', { note: 'ui.site_menu' })} items={menuItems} />}
146
+ footer={
147
+ <>
148
+ <LeftSidebarLanguage />
149
+ <LeftSidebarActions settings={ReadabilityMenu}>
150
+ {links?.map((link) => (
151
+ <a
152
+ key={link.label}
153
+ href={link.url}
154
+ target="_blank"
155
+ rel="noopener noreferrer"
156
+ aria-label={link.label}
157
+ className={cn(
158
+ buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
159
+ 'text-muted-foreground [&_svg]:size-4',
160
+ )}
161
+ >
162
+ {link.icon ? LINK_ICONS[link.icon] : link.label}
163
+ </a>
164
+ ))}
165
+ </LeftSidebarActions>
166
+ </>
167
+ }
168
+ controls={
169
+ <>
170
+ <LeftSidebarSearch />
171
+ <Switcher {...nav.section} />
172
+ <Switcher {...nav.dataset} />
173
+ {Feature ? (
174
+ <>
175
+ <LeftSidebarSeparator />
176
+ <Feature />
177
+ </>
178
+ ) : null}
179
+ </>
180
+ }
181
+ />
182
+ }
183
+ >
184
+ {children}
185
+ </UiLayout>
186
+ );
187
+ return locales ? (
188
+ <LangLocalesScope locales={locales}>{layout}</LangLocalesScope>
189
+ ) : (
190
+ layout
191
+ );
192
+ }
@@ -4,8 +4,9 @@ import {
4
4
  ScrollProvider,
5
5
  type AnchorProviderProps,
6
6
  } from 'fumadocs-core/toc';
7
- import { useRef, type ComponentProps } from 'react';
7
+ import { useMemo, useRef, type ComponentProps } from 'react';
8
8
  import { TOCContext } from '@terrafirmagreg-team/kit/hooks/use-toc';
9
+ import { tocTitle } from '@terrafirmagreg-team/kit/lib/toc-title';
9
10
  import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
10
11
  import { mergeRefs } from '@terrafirmagreg-team/ui/lib/utils/merge-refs';
11
12
 
@@ -32,9 +33,13 @@ export type {
32
33
  export type TOCProviderProps = AnchorProviderProps;
33
34
 
34
35
  export function TOCProvider({ toc, children, ...props }: TOCProviderProps) {
36
+ const items = useMemo(
37
+ () => toc?.map((item) => ({ ...item, title: tocTitle(item) })),
38
+ [toc],
39
+ );
35
40
  return (
36
- <TOCContext value={toc}>
37
- <AnchorProvider toc={toc} {...props}>
41
+ <TOCContext value={items}>
42
+ <AnchorProvider toc={items} {...props}>
38
43
  {children}
39
44
  </AnchorProvider>
40
45
  </TOCContext>
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import type { ReactNode } from 'react';
3
+ import { TOCProvider } from '@terrafirmagreg-team/kit/components/toc/base';
4
+ import type { TOCItemType } from 'fumadocs-core/toc';
5
+
6
+ export function PageToc({
7
+ toc = [],
8
+ single,
9
+ popover,
10
+ outline,
11
+ children,
12
+ }: {
13
+ toc?: TOCItemType[];
14
+ single?: boolean;
15
+ popover?: ReactNode;
16
+ outline?: ReactNode;
17
+ children?: ReactNode;
18
+ }) {
19
+ return (
20
+ <TOCProvider single={single} toc={toc}>
21
+ <div
22
+ data-slot="workspace-outline-popover"
23
+ className="sticky top-[calc(var(--banner-height,0px)+var(--header-height))] z-30 [grid-area:toc-popover] self-start md:hidden"
24
+ >
25
+ {popover}
26
+ <div className="absolute inset-x-2 top-full h-[calc(var(--layout-height)_-_var(--header-height)_-_100%)] rounded-t-2xl border-x border-t outline-8 outline-card [clip-path:inset(0_-8px_-8px)] bg-origin-border bg-linear-to-b from-background to-transparent to-[16px] pointer-events-none" />
27
+ </div>
28
+ {children}
29
+ {outline}
30
+ </TOCProvider>
31
+ );
32
+ }
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import { CheckIcon, ChevronDown } from 'lucide-react';
4
+ import { useRouter } from 'next/navigation';
4
5
  import { useLocationSearchParams } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
5
- import { useState } from 'react';
6
+ import { useEffect, useMemo, useState } from 'react';
6
7
  import {
7
8
  DropdownMenu,
8
9
  DropdownMenuContent,
@@ -12,6 +13,7 @@ import {
12
13
  import { useUiComponents } from '@terrafirmagreg-team/ui/provider';
13
14
  import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
14
15
  import { type DatasetRef, sectionUrl } from '@terrafirmagreg-team/sites/location';
16
+ import { clientDumpLoader } from '@terrafirmagreg-team/kit/lib/config';
15
17
 
16
18
  import type { SectionKey } from '@terrafirmagreg-team/kit/types';
17
19
 
@@ -88,30 +90,48 @@ function VersionMenuItems({
88
90
  }) {
89
91
  const { Link } = useUiComponents();
90
92
  const t = useTranslations();
93
+ const router = useRouter();
91
94
 
92
95
  const current = useLocationSearchParams();
93
- return ordered.map((version) => {
96
+ const hrefs = useMemo(
97
+ () =>
98
+ ordered.map((version) => {
99
+ const params = new URLSearchParams(current);
100
+ if (version === latest) params.delete('version');
101
+ else params.set('version', version);
102
+ const q = params.toString();
103
+ return section
104
+ ? `${sectionUrl(section, datasetId)}${q ? `?${q}` : ''}`
105
+ : undefined;
106
+ }),
107
+ [ordered, current, latest, section, datasetId],
108
+ );
109
+ useEffect(() => {
110
+ if (!section) return;
111
+ for (const href of hrefs) {
112
+ if (href) router.prefetch(href);
113
+ }
114
+ }, [hrefs, section, router]);
115
+ return ordered.map((version, index) => {
94
116
  const isLatest = version === latest;
95
117
  const selected = currentRef.id === datasetId &&
96
118
  currentRef.version === (isLatest ? undefined : version);
97
- const params = new URLSearchParams(current);
98
- if (isLatest) params.delete('version');
99
- else params.set('version', version);
100
- const q = params.toString();
119
+ const href = hrefs[index];
101
120
  return (
102
121
  <DropdownMenuItem
103
122
  key={version}
104
123
  closeOnClick
105
124
  render={
106
- section ? (
107
- <Link
108
- href={`${sectionUrl(section, datasetId)}${q ? `?${q}` : ''}`}
109
- />
110
- ) : undefined
125
+ section && href ? <Link href={href} /> : undefined
111
126
  }
112
127
  onClick={
113
128
  onSelect ? () => onSelect(isLatest ? undefined : version) : undefined
114
129
  }
130
+ onPointerEnter={() =>
131
+ clientDumpLoader().warm?.(
132
+ isLatest ? { id: datasetId } : { id: datasetId, version },
133
+ )
134
+ }
115
135
  className="gap-2 px-2.5 py-1.5 text-xs"
116
136
  >
117
137
  <span className="flex min-w-0 flex-1 flex-col">
@@ -0,0 +1,140 @@
1
+ 'use client';
2
+ import {
3
+ createContext,
4
+ use,
5
+ useEffect,
6
+ useState,
7
+ type ReactNode,
8
+ } from 'react';
9
+ import { PageToc } from '@terrafirmagreg-team/kit/components/toc/page';
10
+ import { Workspace } from '@terrafirmagreg-team/ui/components/layouts/workspace';
11
+ import {
12
+ RightSidebarOutline,
13
+ WorkspaceOutlineDropdown,
14
+ WorkspaceOutlinePopover,
15
+ } from '@terrafirmagreg-team/kit/components/outline';
16
+ import type {
17
+ OutlineProps,
18
+ WorkspaceOutlinePopoverProps,
19
+ } from '@terrafirmagreg-team/kit/types/outline';
20
+ import type { TOCItemType } from 'fumadocs-core/toc';
21
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
22
+ import type { WorkspaceProps } from '@terrafirmagreg-team/ui/components/layouts/workspace';
23
+
24
+ export interface WorkspaceSlots {
25
+ toc?: TOCItemType[];
26
+ outline?: ReactNode;
27
+ breadcrumb?: WorkspaceProps['breadcrumb'];
28
+ pagination?: WorkspaceProps['pagination'];
29
+ full?: boolean;
30
+ }
31
+
32
+ export interface WorkspaceViewProps extends WorkspaceSlots {
33
+ outlineProps?: OutlineProps & { enabled?: boolean; single?: boolean };
34
+ outlinePopover?: WorkspaceOutlinePopoverProps & { enabled?: boolean };
35
+ actions?: ReactNode;
36
+ footer?: ReactNode;
37
+ className?: string;
38
+ }
39
+
40
+ export interface PageFrameProps extends WorkspaceViewProps {
41
+ children?: ReactNode;
42
+ }
43
+
44
+ const WorkspaceSlotsContext = createContext<
45
+ (slots: WorkspaceSlots | null) => void
46
+ >(() => {});
47
+
48
+ export function useWorkspaceSlots(
49
+ slots: WorkspaceSlots | null,
50
+ deps: readonly unknown[],
51
+ ) {
52
+ const set = use(WorkspaceSlotsContext);
53
+ useEffect(() => {
54
+ set(slots);
55
+ return () => set(null);
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, [set, ...deps]);
58
+ }
59
+
60
+ export function PageFrame({
61
+ toc = [],
62
+ outline,
63
+ outlineProps = {},
64
+ outlinePopover = {},
65
+ breadcrumb,
66
+ pagination = {},
67
+ full = false,
68
+ actions,
69
+ footer,
70
+ className,
71
+ children,
72
+ ...props
73
+ }: PageFrameProps) {
74
+ const [slots, setSlots] = useState<WorkspaceSlots | null>(null);
75
+ const effToc = slots?.toc ?? toc;
76
+ const effOutline = slots?.outline ?? outline;
77
+ const effBreadcrumb = slots?.breadcrumb ?? breadcrumb;
78
+ const effPagination = slots?.pagination ?? pagination;
79
+ const effFull = slots?.full ?? full;
80
+ const {
81
+ enabled: outlineEnabledProp,
82
+ single,
83
+ ...oProps
84
+ } = outlineProps;
85
+ const outlineEnabled =
86
+ outlineEnabledProp ?? (!effFull || (slots?.toc?.length ?? 0) > 0);
87
+ const { enabled: popoverEnabledProp, ...pProps } = outlinePopover;
88
+ const popoverEnabled =
89
+ popoverEnabledProp ??
90
+ Boolean(
91
+ effToc.length > 0 || outlinePopover.header || outlinePopover.footer,
92
+ );
93
+ const hasCrumbs = (effBreadcrumb?.items?.length ?? 0) > 0;
94
+
95
+ return (
96
+ <WorkspaceSlotsContext value={setSlots}>
97
+ <PageToc
98
+ single={single}
99
+ toc={outlineEnabled || popoverEnabled ? effToc : []}
100
+ popover={
101
+ popoverEnabled ? (
102
+ <WorkspaceOutlinePopover {...pProps} />
103
+ ) : undefined
104
+ }
105
+ outline={
106
+ effOutline ??
107
+ (outlineEnabled ? <RightSidebarOutline {...oProps} /> : undefined)
108
+ }
109
+ >
110
+ <Workspace
111
+ full={effFull}
112
+ breadcrumb={effBreadcrumb}
113
+ pagination={effPagination}
114
+ actions={actions}
115
+ footer={footer}
116
+ className={className}
117
+ outlineDropdown={
118
+ popoverEnabled ? (
119
+ <div
120
+ className={cn(
121
+ 'flex items-center gap-1.5 min-w-0 transition-[opacity,visibility] duration-300 motion-reduce:transition-none',
122
+ outlineEnabled &&
123
+ 'invisible opacity-0 @max-5xl:visible @max-5xl:opacity-100 @max-5xl:transition-none',
124
+ )}
125
+ >
126
+ {hasCrumbs && (
127
+ <span className="text-sm text-muted-foreground/50">/</span>
128
+ )}
129
+ <WorkspaceOutlineDropdown {...pProps} />
130
+ </div>
131
+ ) : undefined
132
+ }
133
+ {...props}
134
+ >
135
+ {children}
136
+ </Workspace>
137
+ </PageToc>
138
+ </WorkspaceSlotsContext>
139
+ );
140
+ }
@@ -0,0 +1,296 @@
1
+ import type { ComponentProps, ComponentType, ReactNode } from 'react';
2
+ import { notFound } from 'next/navigation';
3
+ import type { Metadata } from 'next';
4
+ import type * as PageTree from 'fumadocs-core/page-tree';
5
+ import { flattenTree } from 'fumadocs-core/page-tree';
6
+ import { LangRedirect } from '@terrafirmagreg-team/ui/components/blocks/lang/redirect';
7
+ import { LangContentSwitch } from '@terrafirmagreg-team/ui/components/blocks/lang/content';
8
+ import {
9
+ renderSectionView,
10
+ renderSectionWorkspace,
11
+ type SectionPageView,
12
+ type SectionPageViewFn,
13
+ } from '@terrafirmagreg-team/kit/components/views/section';
14
+ import { baseOptions } from '@terrafirmagreg-team/kit/lib/layout';
15
+ import {
16
+ datasetCatalog,
17
+ defaultDataset,
18
+ findDataset,
19
+ sectionDatasets,
20
+ } from '@terrafirmagreg-team/kit/lib/datasets';
21
+ import {
22
+ generateDatasetLandingParams,
23
+ getDatasetLanding,
24
+ } from '@terrafirmagreg-team/kit/lib/landing';
25
+ import {
26
+ generateDatasetParams,
27
+ getPage,
28
+ resolvePage,
29
+ sectionMetadata,
30
+ } from '@terrafirmagreg-team/kit/lib/page';
31
+ import {
32
+ localeLoader,
33
+ registerLocaleRenderer,
34
+ } from '@terrafirmagreg-team/kit/lib/locale/renderer';
35
+ import { canonicalUrl } from '@terrafirmagreg-team/kit/lib/urls';
36
+ import { sectionUrl } from '@terrafirmagreg-team/sites/location';
37
+ import { langContent, loadLocaleRegistry } from '@terrafirmagreg-team/translate/server';
38
+ import type {
39
+ DatasetCatalog,
40
+ DatasetLanding,
41
+ SectionDataset,
42
+ SectionKey,
43
+ SectionPage,
44
+ SectionSource,
45
+ SectionTrees,
46
+ } from '@terrafirmagreg-team/kit/types';
47
+ import type { SectionFeatures } from '@terrafirmagreg-team/kit/types/features';
48
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
49
+
50
+ export interface SectionLayoutViewProps {
51
+ section: SectionKey;
52
+ trees: SectionTrees;
53
+ catalog: DatasetCatalog;
54
+ useTree?: (tree: PageTree.Root) => PageTree.Root;
55
+ sidebar?: { feature?: ComponentType };
56
+ navTitle?: ComponentType<ComponentProps<'a'>>;
57
+ children?: ReactNode;
58
+ }
59
+
60
+ export type SectionLandingView = SectionPageView;
61
+
62
+ export interface SectionLandingProps<TSource = unknown> {
63
+ page: DatasetLanding;
64
+ lang: Lang;
65
+ route: string;
66
+ source: TSource;
67
+ trees?: SectionTrees;
68
+ catalog: DatasetCatalog;
69
+ contentDir: string;
70
+ history?: boolean;
71
+ features?: SectionFeatures;
72
+ }
73
+
74
+ export interface SectionRoutesOptions<
75
+ S extends SectionKey,
76
+ TSource = SectionSource,
77
+ > {
78
+ section: S;
79
+ trees?: () => Promise<SectionTrees>;
80
+ source: {
81
+ getSource: () => TSource | Promise<TSource>;
82
+ contentDir?: string;
83
+ history?: boolean;
84
+ };
85
+ features?: SectionFeatures;
86
+ layout: ComponentType<SectionLayoutViewProps>;
87
+ landing?: (
88
+ props: SectionLandingProps<TSource>,
89
+ ) => SectionLandingView | Promise<SectionLandingView>;
90
+ page?: SectionPageViewFn;
91
+ }
92
+
93
+ interface RouteParams {
94
+ params: Promise<{ slug?: string[] }>;
95
+ }
96
+
97
+ type Resolved = { page: unknown; isFallback: boolean } | undefined;
98
+
99
+ export function createSectionRedirect(section: SectionKey) {
100
+ return async function Page() {
101
+ const dataset = await defaultDataset(section);
102
+ if (!dataset) notFound();
103
+ return <LangRedirect to={sectionUrl(section, dataset)} />;
104
+ };
105
+ }
106
+
107
+ export function createSectionRoutes<
108
+ S extends SectionKey,
109
+ TSource = SectionSource,
110
+ >({
111
+ section,
112
+ trees,
113
+ source,
114
+ features,
115
+ layout: LayoutView,
116
+ landing,
117
+ page: PageView,
118
+ }: SectionRoutesOptions<S, TSource>) {
119
+ const resolve =
120
+ (src: TSource, datasets: readonly SectionDataset[]) =>
121
+ (slug: string[] | undefined, lang: Lang): Resolved => {
122
+ if (slug?.length === 1) {
123
+ if (!findDataset(datasets, { id: slug[0] })) return undefined;
124
+ if (landing) {
125
+ const page = getDatasetLanding(section, slug, lang, datasets);
126
+ if (!page) return undefined;
127
+ if (!PageView) return { page, isFallback: false };
128
+ const own = (src as SectionSource)
129
+ .getPages()
130
+ .some(
131
+ (p) =>
132
+ p.slugs[0] === slug[0] &&
133
+ ((p as SectionPage).locale ?? p.slugs[1]) === lang,
134
+ );
135
+ return { page, isFallback: !own };
136
+ }
137
+ }
138
+ return PageView ? resolvePage(src as SectionSource, slug, lang) : undefined;
139
+ };
140
+
141
+ const render = async (
142
+ src: TSource,
143
+ slug: string[] | undefined,
144
+ resolvedTrees: SectionTrees | undefined,
145
+ catalog: DatasetCatalog,
146
+ page: unknown,
147
+ lang: Lang,
148
+ ): Promise<ReactNode> => {
149
+ if (slug?.length === 1 && landing) {
150
+ const view = await landing({
151
+ page: page as DatasetLanding,
152
+ lang,
153
+ route: sectionUrl(section),
154
+ source: src,
155
+ trees: resolvedTrees,
156
+ catalog,
157
+ contentDir: source.contentDir ?? `content/${section}`,
158
+ history: source.history,
159
+ features,
160
+ });
161
+ return renderSectionView(view);
162
+ }
163
+ if (!PageView) return null;
164
+ return renderSectionWorkspace({
165
+ source: src as SectionSource,
166
+ slug,
167
+ contentDir: source.contentDir ?? `content/${section}`,
168
+ trees: resolvedTrees,
169
+ catalog,
170
+ history: source.history,
171
+ features,
172
+ workspace: PageView,
173
+ page: page as SectionPage,
174
+ lang,
175
+ });
176
+ };
177
+
178
+ registerLocaleRenderer(section, {
179
+ resolve: async (slug, lang) =>
180
+ resolve(await source.getSource(), await sectionDatasets(section))(
181
+ slug,
182
+ lang,
183
+ ),
184
+ render: async (page, slug, lang) =>
185
+ render(
186
+ await source.getSource(),
187
+ slug,
188
+ await trees?.(),
189
+ await datasetCatalog(),
190
+ page,
191
+ lang,
192
+ ),
193
+ });
194
+
195
+ async function SectionLayout({ children }: { children?: ReactNode }) {
196
+ const { slots } = baseOptions();
197
+ const [resolvedTrees, catalog] = await Promise.all([
198
+ trees ? trees() : Promise.resolve<SectionTrees>({}),
199
+ datasetCatalog(),
200
+ ]);
201
+ return (
202
+ <LayoutView
203
+ section={section}
204
+ trees={resolvedTrees}
205
+ catalog={catalog}
206
+ navTitle={slots?.navTitle}
207
+ >
208
+ {children}
209
+ </LayoutView>
210
+ );
211
+ }
212
+
213
+ async function Page({ params }: RouteParams) {
214
+ const { slug } = await params;
215
+ const [src, datasets] = await Promise.all([
216
+ source.getSource(),
217
+ sectionDatasets(section),
218
+ ]);
219
+
220
+ if (slug?.length === 1 && !landing && PageView) {
221
+ const { defaultLanguage } = loadLocaleRegistry();
222
+ if (!getPage(src as SectionSource, slug, defaultLanguage)) {
223
+ const resolvedTrees = trees ? await trees() : undefined;
224
+ const first = flattenTree(
225
+ resolvedTrees?.[defaultLanguage]?.[slug[0]]?.children ?? [],
226
+ )[0];
227
+ if (first) return <LangRedirect to={first.url} />;
228
+ }
229
+ }
230
+
231
+ const [resolvedTrees, catalog] = await Promise.all([
232
+ trees?.(),
233
+ datasetCatalog(),
234
+ ]);
235
+ const content = langContent(
236
+ slug,
237
+ resolve(src, datasets),
238
+ (page, lang) =>
239
+ render(src, slug, resolvedTrees, catalog, page, lang) as ReactNode,
240
+ );
241
+ if (!content) notFound();
242
+ return (
243
+ <LangContentSwitch
244
+ key={[section, ...(slug ?? [])].join('/')}
245
+ content={content}
246
+ loadLang={localeLoader()?.bind(null, { section, slug })}
247
+ />
248
+ );
249
+ }
250
+
251
+ async function Redirect() {
252
+ const dataset = await defaultDataset(section);
253
+ if (!dataset) notFound();
254
+ return <LangRedirect to={sectionUrl(section, dataset)} />;
255
+ }
256
+
257
+ function generateStaticParams() {
258
+ if (PageView) {
259
+ return Promise.resolve(source.getSource()).then((src) =>
260
+ generateDatasetParams(section, src as SectionSource),
261
+ );
262
+ }
263
+ return generateDatasetLandingParams(section);
264
+ }
265
+
266
+ async function generateMetadata({ params }: RouteParams): Promise<Metadata> {
267
+ const { slug } = await params;
268
+ if (slug?.length === 1 && landing) {
269
+ const page = getDatasetLanding(
270
+ section,
271
+ slug,
272
+ 'en_us',
273
+ await sectionDatasets(section),
274
+ );
275
+ if (!page) notFound();
276
+ return {
277
+ title: page.title,
278
+ description: page.description,
279
+ alternates: {
280
+ canonical: canonicalUrl(`${sectionUrl(section)}/${slug.join('/')}`),
281
+ },
282
+ };
283
+ }
284
+ if (!PageView) return {};
285
+ const src = (await source.getSource()) as SectionSource | undefined;
286
+ if (src) {
287
+ const { defaultLanguage } = loadLocaleRegistry();
288
+ if (slug?.length === 1 && !landing && !getPage(src, slug, defaultLanguage))
289
+ return {};
290
+ return sectionMetadata(section, src, slug);
291
+ }
292
+ return {};
293
+ }
294
+
295
+ return { SectionLayout, Page, Redirect, generateStaticParams, generateMetadata };
296
+ }
@@ -1,7 +1,5 @@
1
1
  import { createRelativeLink } from '@terrafirmagreg-team/kit/components/relative-link';
2
2
  import type { LoaderConfig, LoaderOutput, Page } from 'fumadocs-core/source';
3
- import { notFound } from 'next/navigation';
4
- import { LangContentSwitch } from '@terrafirmagreg-team/ui/components/blocks/lang/content';
5
3
  import { LinkPreview } from '@terrafirmagreg-team/ui/components/blocks/link/link-preview';
6
4
  import { lookupPagePreview } from '@terrafirmagreg-team/kit/lib/preview/lookup';
7
5
  import {
@@ -9,26 +7,24 @@ import {
9
7
  sectionBreadcrumbPreviews,
10
8
  type SectionCrumb,
11
9
  } from '@terrafirmagreg-team/kit/lib/breadcrumb';
12
- import { langContent } from '@terrafirmagreg-team/translate/server';
13
10
  import { loadLocaleRegistry } from '@terrafirmagreg-team/translate/server';
14
11
  import { readPageSource, translationState } from '@terrafirmagreg-team/translate/server';
15
12
  import { resolvePage, type SectionSource } from '@terrafirmagreg-team/kit/lib/page';
16
- import { localeLoader } from '@terrafirmagreg-team/kit/lib/locale/renderer';
17
13
  import { findNeighbour } from 'fumadocs-core/page-tree';
14
+ import { stripLangFromUrl } from '@terrafirmagreg-team/translate/lib/slug';
15
+ import { sectionConfig } from '@terrafirmagreg-team/kit/lib/config';
18
16
  import type {
19
17
  DatasetCatalog,
20
- SectionKey,
21
18
  SectionPage,
22
19
  SectionTrees,
23
20
  } from '@terrafirmagreg-team/kit/types';
24
- import type { WorkspacePaginationItems } from '@terrafirmagreg-team/kit/components/workspace';
21
+ import { PageFrame } from '@terrafirmagreg-team/kit/components/views/frame';
22
+ import type { WorkspaceViewProps } from '@terrafirmagreg-team/kit/components/views/frame';
23
+ import type { WorkspacePaginationItems } from '@terrafirmagreg-team/ui/components/layouts/workspace';
24
+ import type { SectionFeatures } from '@terrafirmagreg-team/kit/types/features';
25
25
  import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
26
26
  import type { TranslationState } from '@terrafirmagreg-team/translate/server';
27
- import type {
28
- ComponentProps,
29
- ComponentType,
30
- ReactNode,
31
- } from 'react';
27
+ import type { ComponentProps, ReactNode } from 'react';
32
28
  import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
33
29
 
34
30
  export interface SectionWorkspaceProps {
@@ -40,16 +36,31 @@ export interface SectionWorkspaceProps {
40
36
  previews?: Record<string, PagePreviewData>;
41
37
  footer?: WorkspacePaginationItems;
42
38
  history?: boolean;
39
+ features?: SectionFeatures;
40
+ }
41
+
42
+ export interface SectionPageView {
43
+ node: ReactNode;
44
+ workspace?: WorkspaceViewProps;
45
+ }
46
+
47
+ export function renderSectionView(view: SectionPageView) {
48
+ return <PageFrame {...view.workspace}>{view.node}</PageFrame>;
43
49
  }
44
50
 
45
- export function renderSectionWorkspace({
51
+ export type SectionPageViewFn = (
52
+ props: SectionWorkspaceProps,
53
+ ) => SectionPageView | Promise<SectionPageView>;
54
+
55
+ export async function renderSectionWorkspace({
46
56
  source,
47
57
  slug,
48
58
  contentDir,
49
59
  trees,
50
60
  catalog,
51
61
  history,
52
- workspace: Workspace,
62
+ features,
63
+ workspace: pageView,
53
64
  page,
54
65
  lang,
55
66
  }: {
@@ -59,7 +70,8 @@ export function renderSectionWorkspace({
59
70
  trees?: SectionTrees;
60
71
  catalog: DatasetCatalog;
61
72
  history?: boolean;
62
- workspace: ComponentType<SectionWorkspaceProps>;
73
+ features?: SectionFeatures;
74
+ workspace: SectionPageViewFn;
63
75
  page: SectionPage;
64
76
  lang: Lang;
65
77
  }) {
@@ -81,72 +93,30 @@ export function renderSectionWorkspace({
81
93
  const crumbs = sectionBreadcrumbItems(source, page, lang);
82
94
  const datasetId = page.url.split('/').filter(Boolean)[1];
83
95
  const tree = datasetId ? trees?.[lang]?.[datasetId] : undefined;
84
- const neighbours = tree ? findNeighbour(tree, page.url) : undefined;
85
- return (
86
- <Workspace
87
- page={page}
88
- crumbs={crumbs}
89
- footer={neighbours}
90
- previews={sectionBreadcrumbPreviews(crumbs, lang, catalog)}
91
- link={createRelativeLink(
92
- source as LoaderOutput<LoaderConfig>,
93
- page as unknown as Page,
94
- (props) => (
95
- <LinkPreview
96
- preview={lookupPagePreview(catalog, props.href, lang)}
97
- {...props}
98
- />
99
- ),
100
- )}
101
- contentDir={contentDir}
102
- copied={copied}
103
- history={history}
104
- />
105
- );
106
- }
107
-
108
- export async function SectionView({
109
- section,
110
- getSource,
111
- slug,
112
- contentDir,
113
- trees,
114
- catalog,
115
- history,
116
- workspace,
117
- }: {
118
- section: SectionKey;
119
- getSource: () => Promise<SectionSource>;
120
- slug: string[] | undefined;
121
- contentDir: string;
122
- trees?: SectionTrees;
123
- catalog: DatasetCatalog;
124
- history?: boolean;
125
- workspace: ComponentType<SectionWorkspaceProps>;
126
- }) {
127
- const source = await getSource();
128
- const content = langContent(
129
- slug,
130
- (s, l) => resolvePage(source, s, l),
131
- (page, lang) =>
132
- renderSectionWorkspace({
133
- source,
134
- slug,
135
- contentDir,
136
- trees,
137
- catalog,
138
- history,
139
- workspace,
140
- page: page as SectionPage,
141
- lang,
142
- }),
143
- );
144
- if (!content) notFound();
145
- return (
146
- <LangContentSwitch
147
- content={content}
148
- loadLang={localeLoader()?.bind(null, { section, slug })}
149
- />
150
- );
96
+ const pageUrl = stripLangFromUrl(page.url, [
97
+ ...sectionConfig().knownLanguages,
98
+ ]);
99
+ const neighbours = tree ? findNeighbour(tree, pageUrl) : undefined;
100
+ const view = await pageView({
101
+ page,
102
+ crumbs,
103
+ footer: neighbours,
104
+ previews: sectionBreadcrumbPreviews(crumbs, lang, catalog),
105
+ link: createRelativeLink(
106
+ source as LoaderOutput<LoaderConfig>,
107
+ page as unknown as Page,
108
+ (props) => (
109
+ <LinkPreview
110
+ preview={lookupPagePreview(catalog, props.href, lang)}
111
+ {...props}
112
+ />
113
+ ),
114
+ ),
115
+ contentDir,
116
+ copied,
117
+ history,
118
+ features,
119
+ });
120
+ return renderSectionView(view);
151
121
  }
152
122
 
package/src/lib/config.ts CHANGED
@@ -80,11 +80,25 @@ export function sectionConfig(): SectionConfigInput {
80
80
  return config;
81
81
  }
82
82
 
83
+ export interface LayoutDeps {
84
+ trackView?: (pathname: string) => void;
85
+ commentsMenuItem?: () => unknown;
86
+ }
87
+
88
+ export function registerLayoutDeps(input: LayoutDeps): void {
89
+ registry.set('layoutDeps', input);
90
+ }
91
+
92
+ export function layoutDeps(): LayoutDeps {
93
+ return (registry.get('layoutDeps') as LayoutDeps | undefined) ?? {};
94
+ }
95
+
83
96
  export type ClientDumpLoader = {
84
97
  loadDump(dataset: DatasetRef): Promise<SectionDump | null | undefined>;
85
98
  loadVersions(
86
99
  datasetId: string,
87
100
  ): Promise<{ latest?: string; versions: string[] } | null>;
101
+ warm?(dataset: DatasetRef): void;
88
102
  };
89
103
 
90
104
  export const {
@@ -0,0 +1,34 @@
1
+ import {
2
+ Children,
3
+ cloneElement,
4
+ createElement,
5
+ Fragment,
6
+ isValidElement,
7
+ type ReactElement,
8
+ type ReactNode,
9
+ } from 'react';
10
+
11
+ function unwrap(node: ReactNode): ReactNode {
12
+ return Children.map(node, (child) => {
13
+ if (!isValidElement(child)) return child;
14
+ const element = child as ReactElement<{
15
+ href?: unknown;
16
+ children?: ReactNode;
17
+ }>;
18
+ if (element.props.href !== undefined) {
19
+ return unwrap(element.props.children);
20
+ }
21
+ if (element.props.children !== undefined) {
22
+ return cloneElement(
23
+ element,
24
+ undefined,
25
+ unwrap(element.props.children),
26
+ );
27
+ }
28
+ return child;
29
+ });
30
+ }
31
+
32
+ export function tocTitle(item: { title: ReactNode }): ReactNode {
33
+ return createElement(Fragment, null, unwrap(item.title));
34
+ }
@@ -0,0 +1,19 @@
1
+ import type { SectionPage } from '@terrafirmagreg-team/kit/types';
2
+ import type { TranslationState } from '@terrafirmagreg-team/translate/server';
3
+ import type { ReactNode } from 'react';
4
+
5
+ export interface SectionPageContext {
6
+ page: SectionPage;
7
+ filePath: string;
8
+ datasetId?: string;
9
+ copied?: TranslationState;
10
+ history?: boolean;
11
+ }
12
+
13
+ export interface SectionFeatures {
14
+ fileUrl?: (filePath: string) => string;
15
+ actionsRight?: (ctx: SectionPageContext) => ReactNode;
16
+ wrapBody?: (body: ReactNode, ctx: SectionPageContext) => ReactNode;
17
+ footer?: (ctx: SectionPageContext) => ReactNode;
18
+ comments?: (ctx: SectionPageContext) => ReactNode;
19
+ }
@@ -1,54 +0,0 @@
1
- 'use client';
2
- import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
3
- import type { ComponentProps } from 'react';
4
-
5
- export function WorkspaceBody({ className, ...props }: ComponentProps<'div'>) {
6
- return (
7
- <div
8
- data-slot="workspace-body"
9
- {...props}
10
- className={cn('prose flex-1', className)}
11
- />
12
- );
13
- }
14
-
15
- export function WorkspaceDescription({
16
- children,
17
- className,
18
- ...props
19
- }: ComponentProps<'p'>) {
20
- if (children === undefined) return null;
21
- return (
22
- <p
23
- data-slot="workspace-description"
24
- {...props}
25
- className={cn('mb-8 text-lg text-muted-foreground', className)}
26
- >
27
- {children}
28
- </p>
29
- );
30
- }
31
-
32
- export function WorkspaceTitle({ className, ...props }: ComponentProps<'h1'>) {
33
- return (
34
- <h1
35
- data-slot="workspace-title"
36
- {...props}
37
- className={cn('text-[1.75em] font-semibold', className)}
38
- />
39
- );
40
- }
41
-
42
- export function WorkspaceFooter({
43
- className,
44
- ...props
45
- }: ComponentProps<'div'>) {
46
- return (
47
- <div
48
- data-slot="workspace-footer"
49
- className={cn('mt-auto empty:hidden', className)}
50
- {...props}
51
- />
52
- );
53
- }
54
-
@@ -1,13 +0,0 @@
1
- export type {
2
- WorkspacePaginationItems,
3
- WorkspaceProps,
4
- } from './workspace';
5
- export { Workspace } from './workspace';
6
- export {
7
- WorkspaceBody,
8
- WorkspaceDescription,
9
- WorkspaceTitle,
10
- WorkspaceFooter,
11
- } from './content';
12
- export { WorkspacePagination } from './pagination';
13
-
@@ -1,74 +0,0 @@
1
- 'use client';
2
- import { type ComponentProps, useMemo } from 'react';
3
- import { usePathname } from 'next/navigation';
4
- import Link from '@terrafirmagreg-team/ui/components/blocks/nav/link';
5
- import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react';
6
- import { useFooterItems } from '@terrafirmagreg-team/ui/hooks/use-tree';
7
- import { useLocationSearch } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
8
- import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
9
- import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
10
- import { isNavItemActive } from '@terrafirmagreg-team/ui/lib/utils/nav-active';
11
- import type { WorkspacePaginationItems } from './workspace';
12
-
13
- export function WorkspacePagination({
14
- items,
15
- className,
16
- ...props
17
- }: ComponentProps<'nav'> & { items?: WorkspacePaginationItems }) {
18
- const footerList = useFooterItems();
19
- const pathname = usePathname();
20
- const search = useLocationSearch();
21
- const { previous, next } = useMemo(() => {
22
- if (items) return items;
23
- const idx = footerList.findIndex((item) =>
24
- isNavItemActive(item.url, pathname, search),
25
- );
26
- if (idx === -1) return {};
27
- return { previous: footerList[idx - 1], next: footerList[idx + 1] };
28
- }, [footerList, items, pathname, search]);
29
-
30
- if (!previous && !next) return null;
31
- return (
32
- <nav
33
- data-slot="workspace-pagination"
34
- className={cn('@container grid grid-cols-2 gap-3 mt-8', className)}
35
- {...props}
36
- >
37
- {previous && <WorkspacePaginationItem item={previous} next={false} />}
38
- {next && <WorkspacePaginationItem item={next} next />}
39
- </nav>
40
- );
41
- }
42
-
43
- function WorkspacePaginationItem({
44
- item,
45
- next,
46
- }: {
47
- item: NonNullable<WorkspacePaginationItems['previous']>;
48
- next: boolean;
49
- }) {
50
- const t = useTranslations();
51
- const Icon = next ? ArrowRightIcon : ArrowLeftIcon;
52
-
53
- return (
54
- <Link
55
- href={item.url}
56
- className={cn(
57
- 'group flex flex-col gap-1 rounded-2xl border p-4 text-sm transition-colors hover:bg-accent/40 @max-md:col-span-full',
58
- next && 'col-start-2 items-end text-end',
59
- )}
60
- >
61
- <span className="inline-flex items-center gap-1.5 text-muted-foreground">
62
- {!next && (
63
- <Icon className="size-3.5 transition-transform group-hover:-translate-x-0.5 rtl:rotate-180" />
64
- )}
65
- {next ? t('Next Page', { note: 'ui.next_page' }) : t('Previous Page', { note: 'ui.prev_page' })}
66
- {next && (
67
- <Icon className="size-3.5 transition-transform group-hover:translate-x-0.5 rtl:rotate-180" />
68
- )}
69
- </span>
70
- <span className="font-medium">{item.name}</span>
71
- </Link>
72
- );
73
- }
74
-
@@ -1,149 +0,0 @@
1
- 'use client';
2
- import type { ComponentProps, ReactNode } from 'react';
3
- import { usePathname } from 'next/navigation';
4
- import type * as PageTree from '@terrafirmagreg-team/translate/types/tree';
5
- import type { TOCItemType } from '@terrafirmagreg-team/kit/components/toc/base';
6
- import { TOCProvider } from '@terrafirmagreg-team/kit/components/toc/base';
7
- import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
8
- import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
9
- import {
10
- RightSidebarOutline,
11
- WorkspaceOutlineDropdown,
12
- WorkspaceOutlinePopover,
13
- type OutlineProps,
14
- type WorkspaceOutlinePopoverProps,
15
- } from '@terrafirmagreg-team/kit/components/outline';
16
- import { LeftSidebarExpand } from '@terrafirmagreg-team/ui/components/layouts/left-sidebar';
17
- import { Breadcrumb } from '@terrafirmagreg-team/ui/components/blocks/nav/breadcrumb';
18
- import { WorkspaceFooter } from './content';
19
- import { WorkspacePagination } from './pagination';
20
-
21
- export type WorkspacePaginationItems = {
22
- previous?: Pick<PageTree.Item, 'name' | 'description' | 'url'>;
23
- next?: Pick<PageTree.Item, 'name' | 'description' | 'url'>;
24
- };
25
-
26
- export type WorkspaceProps = ComponentProps<'article'> & {
27
- toc?: TOCItemType[];
28
-
29
- full?: boolean;
30
- outline?: OutlineProps & { enabled?: boolean; single?: boolean };
31
- outlinePopover?: WorkspaceOutlinePopoverProps & { enabled?: boolean };
32
- breadcrumb?: {
33
- items?: { name: string; url?: string }[];
34
- previews?: Record<string, PagePreviewData>;
35
- };
36
- pagination?: { items?: WorkspacePaginationItems } | false;
37
-
38
- actions?: ReactNode;
39
-
40
- footer?: ReactNode;
41
- };
42
-
43
- export function Workspace({
44
- toc = [],
45
- full = false,
46
- outline = {},
47
- outlinePopover = {},
48
- breadcrumb,
49
- pagination = {},
50
- actions,
51
- footer,
52
- className,
53
- children,
54
- ...props
55
- }: WorkspaceProps) {
56
- const { enabled: outlineEnabled = !full, single, ...outlineProps } = outline;
57
- const { enabled: popoverEnabled = Boolean(toc.length > 0 || outlinePopover.header || outlinePopover.footer), ...popoverProps } = outlinePopover;
58
- const crumbs = breadcrumb?.items ?? [];
59
- const hasCrumbs = crumbs.length > 0;
60
- const pathname = usePathname();
61
-
62
- return (
63
- <TOCProvider
64
- single={single}
65
- toc={outlineEnabled || popoverEnabled ? toc : []}
66
- >
67
- <div
68
- data-slot="workspace-outline-popover"
69
- className="sticky top-[calc(var(--banner-height,0px)+var(--header-height))] z-30 [grid-area:toc-popover] self-start md:hidden"
70
- >
71
- {popoverEnabled && <WorkspaceOutlinePopover {...popoverProps} />}
72
- <div className="absolute inset-x-2 top-full h-[calc(var(--layout-height)_-_var(--header-height)_-_100%)] rounded-t-2xl border-x border-t outline-8 outline-card [clip-path:inset(0_-8px_-8px)] bg-origin-border bg-linear-to-b from-background to-transparent to-[16px] pointer-events-none" />
73
- </div>
74
- <div
75
- key={pathname}
76
- id="page-panel"
77
- data-slot="workspace-panel"
78
- className="@container relative flex flex-col min-w-0 min-h-0 [grid-area:main] mx-2 bg-background md:ms-0 md:my-2 md:overflow-clip md:rounded-2xl md:border md:shadow-sm print:overflow-visible"
79
- >
80
- <header
81
- data-slot="workspace-header"
82
- className={cn(
83
- 'absolute inset-x-0 top-0 z-10 flex items-center gap-1.5 h-(--header-height) ps-6 pe-4 bg-linear-to-b from-background to-transparent pointer-events-none *:pointer-events-auto max-md:hidden',
84
- hasCrumbs || popoverEnabled ? 'from-50%' : 'to-40%',
85
- )}
86
- >
87
- <LeftSidebarExpand />
88
- {hasCrumbs && (
89
- <Breadcrumb
90
- items={crumbs}
91
- previews={breadcrumb?.previews}
92
- />
93
- )}
94
- {popoverEnabled && (
95
- <div
96
- className={cn(
97
- 'flex items-center gap-1.5 min-w-0 transition-[opacity,visibility] duration-300 motion-reduce:transition-none',
98
- outlineEnabled &&
99
- 'invisible opacity-0 @max-5xl:visible @max-5xl:opacity-100 @max-5xl:transition-none',
100
- )}
101
- >
102
- {hasCrumbs && (
103
- <span className="text-sm text-muted-foreground/50">/</span>
104
- )}
105
- <WorkspaceOutlineDropdown {...popoverProps} />
106
- </div>
107
- )}
108
- <div data-slot="workspace-actions" className="ms-auto">
109
- {actions}
110
- </div>
111
- </header>
112
- <div
113
- data-slot="workspace-scroll"
114
- className="flex flex-1 items-start px-4 md:min-h-0 md:pt-(--header-height) md:px-[max(--spacing(6),calc((100%_-_var(--page-width)_-_var(--toc-width))/2))] md:overflow-y-auto md:overscroll-y-contain md:scrollbar-thin md:scrollbar-gutter-stable md:[--docs-height:calc(var(--layout-height)_-_var(--spacing)*4_-_2px_-_var(--header-height))]"
115
- >
116
- <article
117
- id="page"
118
- data-slot="workspace"
119
- data-full={full}
120
- className={cn(
121
- 'flex flex-col gap-4 w-full min-w-0 max-w-(--page-width) pt-6 pb-8',
122
- className,
123
- )}
124
- {...props}
125
- >
126
- {hasCrumbs && (
127
- <Breadcrumb
128
- items={crumbs}
129
- previews={breadcrumb?.previews}
130
- className="md:hidden"
131
- />
132
- )}
133
- {children}
134
- {pagination !== false || footer ? (
135
- <WorkspaceFooter>
136
- {pagination !== false && (
137
- <WorkspacePagination items={pagination.items} />
138
- )}
139
- {footer}
140
- </WorkspaceFooter>
141
- ) : null}
142
- </article>
143
- {outlineEnabled && <RightSidebarOutline {...outlineProps} />}
144
- </div>
145
- </div>
146
- </TOCProvider>
147
- );
148
- }
149
-