@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 +1 -1
- package/src/components/layouts/layout.tsx +192 -0
- package/src/components/toc/base.tsx +8 -3
- package/src/components/toc/page.tsx +32 -0
- package/src/components/version-menu.tsx +31 -11
- package/src/components/views/frame.tsx +140 -0
- package/src/components/views/routes.tsx +296 -0
- package/src/components/views/section.tsx +51 -81
- package/src/lib/config.ts +14 -0
- package/src/lib/toc-title.ts +34 -0
- package/src/types/features.ts +19 -0
- package/src/components/workspace/content.tsx +0 -54
- package/src/components/workspace/index.ts +0 -13
- package/src/components/workspace/pagination.tsx +0 -74
- package/src/components/workspace/workspace.tsx +0 -149
package/package.json
CHANGED
|
@@ -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={
|
|
37
|
-
<AnchorProvider toc={
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|