@terrafirmagreg-team/kit 1.2.3 → 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 +66 -46
- 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
|
+
}
|
|
@@ -6,9 +6,11 @@ import { flattenTree } from 'fumadocs-core/page-tree';
|
|
|
6
6
|
import { LangRedirect } from '@terrafirmagreg-team/ui/components/blocks/lang/redirect';
|
|
7
7
|
import { LangContentSwitch } from '@terrafirmagreg-team/ui/components/blocks/lang/content';
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
renderSectionView,
|
|
10
|
+
renderSectionWorkspace,
|
|
11
|
+
type SectionPageView,
|
|
12
|
+
type SectionPageViewFn,
|
|
13
|
+
} from '@terrafirmagreg-team/kit/components/views/section';
|
|
12
14
|
import { baseOptions } from '@terrafirmagreg-team/kit/lib/layout';
|
|
13
15
|
import {
|
|
14
16
|
datasetCatalog,
|
|
@@ -31,10 +33,6 @@ import {
|
|
|
31
33
|
registerLocaleRenderer,
|
|
32
34
|
} from '@terrafirmagreg-team/kit/lib/locale/renderer';
|
|
33
35
|
import { canonicalUrl } from '@terrafirmagreg-team/kit/lib/urls';
|
|
34
|
-
import {
|
|
35
|
-
renderSectionWorkspace,
|
|
36
|
-
type SectionWorkspaceProps,
|
|
37
|
-
} from '@terrafirmagreg-team/kit/components/views/section';
|
|
38
36
|
import { sectionUrl } from '@terrafirmagreg-team/sites/location';
|
|
39
37
|
import { langContent, loadLocaleRegistry } from '@terrafirmagreg-team/translate/server';
|
|
40
38
|
import type {
|
|
@@ -46,6 +44,7 @@ import type {
|
|
|
46
44
|
SectionSource,
|
|
47
45
|
SectionTrees,
|
|
48
46
|
} from '@terrafirmagreg-team/kit/types';
|
|
47
|
+
import type { SectionFeatures } from '@terrafirmagreg-team/kit/types/features';
|
|
49
48
|
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
50
49
|
|
|
51
50
|
export interface SectionLayoutViewProps {
|
|
@@ -58,29 +57,37 @@ export interface SectionLayoutViewProps {
|
|
|
58
57
|
children?: ReactNode;
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
|
|
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;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
export interface SectionRoutesOptions<
|
|
74
|
+
export interface SectionRoutesOptions<
|
|
75
|
+
S extends SectionKey,
|
|
76
|
+
TSource = SectionSource,
|
|
77
|
+
> {
|
|
67
78
|
section: S;
|
|
68
79
|
trees?: () => Promise<SectionTrees>;
|
|
69
|
-
source
|
|
70
|
-
getSource: () => Promise<
|
|
71
|
-
contentDir
|
|
80
|
+
source: {
|
|
81
|
+
getSource: () => TSource | Promise<TSource>;
|
|
82
|
+
contentDir?: string;
|
|
72
83
|
history?: boolean;
|
|
73
84
|
};
|
|
74
|
-
|
|
75
|
-
feature?: ComponentType;
|
|
76
|
-
useTree?: (tree: PageTree.Root) => PageTree.Root;
|
|
77
|
-
};
|
|
85
|
+
features?: SectionFeatures;
|
|
78
86
|
layout: ComponentType<SectionLayoutViewProps>;
|
|
79
87
|
landing?: (
|
|
80
|
-
|
|
81
|
-
lang: Lang,
|
|
88
|
+
props: SectionLandingProps<TSource>,
|
|
82
89
|
) => SectionLandingView | Promise<SectionLandingView>;
|
|
83
|
-
page?:
|
|
90
|
+
page?: SectionPageViewFn;
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
interface RouteParams {
|
|
@@ -97,25 +104,28 @@ export function createSectionRedirect(section: SectionKey) {
|
|
|
97
104
|
};
|
|
98
105
|
}
|
|
99
106
|
|
|
100
|
-
export function createSectionRoutes<
|
|
107
|
+
export function createSectionRoutes<
|
|
108
|
+
S extends SectionKey,
|
|
109
|
+
TSource = SectionSource,
|
|
110
|
+
>({
|
|
101
111
|
section,
|
|
102
112
|
trees,
|
|
103
113
|
source,
|
|
104
|
-
|
|
114
|
+
features,
|
|
105
115
|
layout: LayoutView,
|
|
106
116
|
landing,
|
|
107
117
|
page: PageView,
|
|
108
|
-
}: SectionRoutesOptions<S>) {
|
|
118
|
+
}: SectionRoutesOptions<S, TSource>) {
|
|
109
119
|
const resolve =
|
|
110
|
-
(src:
|
|
120
|
+
(src: TSource, datasets: readonly SectionDataset[]) =>
|
|
111
121
|
(slug: string[] | undefined, lang: Lang): Resolved => {
|
|
112
122
|
if (slug?.length === 1) {
|
|
113
123
|
if (!findDataset(datasets, { id: slug[0] })) return undefined;
|
|
114
124
|
if (landing) {
|
|
115
125
|
const page = getDatasetLanding(section, slug, lang, datasets);
|
|
116
126
|
if (!page) return undefined;
|
|
117
|
-
if (!
|
|
118
|
-
const own = src
|
|
127
|
+
if (!PageView) return { page, isFallback: false };
|
|
128
|
+
const own = (src as SectionSource)
|
|
119
129
|
.getPages()
|
|
120
130
|
.some(
|
|
121
131
|
(p) =>
|
|
@@ -125,11 +135,11 @@ export function createSectionRoutes<S extends SectionKey>({
|
|
|
125
135
|
return { page, isFallback: !own };
|
|
126
136
|
}
|
|
127
137
|
}
|
|
128
|
-
return
|
|
138
|
+
return PageView ? resolvePage(src as SectionSource, slug, lang) : undefined;
|
|
129
139
|
};
|
|
130
140
|
|
|
131
141
|
const render = async (
|
|
132
|
-
src:
|
|
142
|
+
src: TSource,
|
|
133
143
|
slug: string[] | undefined,
|
|
134
144
|
resolvedTrees: SectionTrees | undefined,
|
|
135
145
|
catalog: DatasetCatalog,
|
|
@@ -137,17 +147,28 @@ export function createSectionRoutes<S extends SectionKey>({
|
|
|
137
147
|
lang: Lang,
|
|
138
148
|
): Promise<ReactNode> => {
|
|
139
149
|
if (slug?.length === 1 && landing) {
|
|
140
|
-
const view = await landing(
|
|
141
|
-
|
|
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);
|
|
142
162
|
}
|
|
143
|
-
if (!
|
|
163
|
+
if (!PageView) return null;
|
|
144
164
|
return renderSectionWorkspace({
|
|
145
|
-
source: src,
|
|
165
|
+
source: src as SectionSource,
|
|
146
166
|
slug,
|
|
147
|
-
contentDir: source
|
|
167
|
+
contentDir: source.contentDir ?? `content/${section}`,
|
|
148
168
|
trees: resolvedTrees,
|
|
149
169
|
catalog,
|
|
150
|
-
history: source
|
|
170
|
+
history: source.history,
|
|
171
|
+
features,
|
|
151
172
|
workspace: PageView,
|
|
152
173
|
page: page as SectionPage,
|
|
153
174
|
lang,
|
|
@@ -156,13 +177,13 @@ export function createSectionRoutes<S extends SectionKey>({
|
|
|
156
177
|
|
|
157
178
|
registerLocaleRenderer(section, {
|
|
158
179
|
resolve: async (slug, lang) =>
|
|
159
|
-
resolve(await source
|
|
180
|
+
resolve(await source.getSource(), await sectionDatasets(section))(
|
|
160
181
|
slug,
|
|
161
182
|
lang,
|
|
162
183
|
),
|
|
163
184
|
render: async (page, slug, lang) =>
|
|
164
185
|
render(
|
|
165
|
-
await source
|
|
186
|
+
await source.getSource(),
|
|
166
187
|
slug,
|
|
167
188
|
await trees?.(),
|
|
168
189
|
await datasetCatalog(),
|
|
@@ -182,8 +203,6 @@ export function createSectionRoutes<S extends SectionKey>({
|
|
|
182
203
|
section={section}
|
|
183
204
|
trees={resolvedTrees}
|
|
184
205
|
catalog={catalog}
|
|
185
|
-
useTree={sidebar?.useTree}
|
|
186
|
-
sidebar={{ feature: sidebar?.feature }}
|
|
187
206
|
navTitle={slots?.navTitle}
|
|
188
207
|
>
|
|
189
208
|
{children}
|
|
@@ -194,13 +213,13 @@ export function createSectionRoutes<S extends SectionKey>({
|
|
|
194
213
|
async function Page({ params }: RouteParams) {
|
|
195
214
|
const { slug } = await params;
|
|
196
215
|
const [src, datasets] = await Promise.all([
|
|
197
|
-
source
|
|
216
|
+
source.getSource(),
|
|
198
217
|
sectionDatasets(section),
|
|
199
218
|
]);
|
|
200
219
|
|
|
201
|
-
if (slug?.length === 1 &&
|
|
220
|
+
if (slug?.length === 1 && !landing && PageView) {
|
|
202
221
|
const { defaultLanguage } = loadLocaleRegistry();
|
|
203
|
-
if (!getPage(src, slug, defaultLanguage)) {
|
|
222
|
+
if (!getPage(src as SectionSource, slug, defaultLanguage)) {
|
|
204
223
|
const resolvedTrees = trees ? await trees() : undefined;
|
|
205
224
|
const first = flattenTree(
|
|
206
225
|
resolvedTrees?.[defaultLanguage]?.[slug[0]]?.children ?? [],
|
|
@@ -236,9 +255,9 @@ export function createSectionRoutes<S extends SectionKey>({
|
|
|
236
255
|
}
|
|
237
256
|
|
|
238
257
|
function generateStaticParams() {
|
|
239
|
-
if (
|
|
240
|
-
return source.getSource().then((src) =>
|
|
241
|
-
generateDatasetParams(section, src),
|
|
258
|
+
if (PageView) {
|
|
259
|
+
return Promise.resolve(source.getSource()).then((src) =>
|
|
260
|
+
generateDatasetParams(section, src as SectionSource),
|
|
242
261
|
);
|
|
243
262
|
}
|
|
244
263
|
return generateDatasetLandingParams(section);
|
|
@@ -262,7 +281,8 @@ export function createSectionRoutes<S extends SectionKey>({
|
|
|
262
281
|
},
|
|
263
282
|
};
|
|
264
283
|
}
|
|
265
|
-
|
|
284
|
+
if (!PageView) return {};
|
|
285
|
+
const src = (await source.getSource()) as SectionSource | undefined;
|
|
266
286
|
if (src) {
|
|
267
287
|
const { defaultLanguage } = loadLocaleRegistry();
|
|
268
288
|
if (slug?.length === 1 && !landing && !getPage(src, slug, defaultLanguage))
|
|
@@ -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
|
-
|