@terrafirmagreg-team/kit 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +68 -3
- package/src/components/blocks/nav-title.tsx +65 -0
- package/src/components/docs-image.tsx +49 -0
- package/src/components/outline/dropdown.tsx +67 -0
- package/src/components/outline/index.ts +10 -0
- package/src/components/outline/list.tsx +120 -0
- package/src/components/outline/outline.tsx +49 -0
- package/src/components/outline/popover.tsx +76 -0
- package/src/components/outline/types.ts +29 -0
- package/src/components/page-actions.tsx +201 -0
- package/src/components/relative-link.tsx +18 -0
- package/src/components/search/dialog.tsx +145 -0
- package/src/components/toc/base.tsx +65 -0
- package/src/components/toc/block.tsx +111 -0
- package/src/components/toc/clerk.tsx +265 -0
- package/src/components/toc/default.tsx +333 -0
- package/src/components/version-menu.tsx +128 -0
- package/src/components/views/dataset.tsx +63 -0
- package/src/components/views/section.tsx +108 -0
- package/src/components/workspace/content.tsx +53 -0
- package/src/components/workspace/index.ts +12 -0
- package/src/components/workspace/pagination.tsx +73 -0
- package/src/components/workspace/workspace.tsx +148 -0
- package/src/hooks/toc.ts +9 -0
- package/src/hooks/use-catalog.ts +11 -0
- package/src/hooks/use-current-dataset.ts +14 -0
- package/src/hooks/use-dataset-accent.ts +13 -0
- package/src/hooks/use-nav-options.ts +84 -0
- package/src/hooks/use-version-pref.ts +30 -0
- package/src/lib/breadcrumb.ts +78 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/dataset-links.ts +30 -0
- package/src/lib/datasets.ts +211 -0
- package/src/lib/landing.ts +46 -0
- package/src/lib/layout.ts +15 -0
- package/src/lib/legacy.ts +18 -0
- package/src/lib/location.ts +63 -0
- package/src/lib/markdown/dom.ts +90 -0
- package/src/lib/markdown/index.ts +13 -0
- package/src/lib/markdown/providers.ts +25 -0
- package/src/lib/mdx/accordions.ts +93 -0
- package/src/lib/mdx/banner.ts +52 -0
- package/src/lib/mdx/directive-utils.ts +125 -0
- package/src/lib/mdx/directives.ts +141 -0
- package/src/lib/mdx/game-export.ts +44 -0
- package/src/lib/mdx/html-style.ts +135 -0
- package/src/lib/mdx/preview.ts +252 -0
- package/src/lib/mdx/references.ts +56 -0
- package/src/lib/mdx/tabs.ts +80 -0
- package/src/lib/mdx-components.tsx +139 -0
- package/src/lib/order.ts +88 -0
- package/src/lib/pack-assets.ts +45 -0
- package/src/lib/page.ts +108 -0
- package/src/lib/pin-tree.ts +35 -0
- package/src/lib/preview/index.ts +8 -0
- package/src/lib/preview/lookup.ts +48 -0
- package/src/lib/preview/og.tsx +115 -0
- package/src/lib/preview/resolvers.ts +98 -0
- package/src/lib/search/config.ts +13 -0
- package/src/lib/search/filters.ts +22 -0
- package/src/lib/search/index.ts +19 -0
- package/src/lib/search/indexes.ts +43 -0
- package/src/lib/search/merge.ts +22 -0
- package/src/lib/search/server.ts +18 -0
- package/src/lib/search/sources.ts +19 -0
- package/src/lib/search/structured.ts +23 -0
- package/src/lib/section/schema.ts +36 -0
- package/src/lib/section/source.ts +40 -0
- package/src/lib/sources.ts +21 -0
- package/src/lib/tree.ts +60 -0
- package/src/lib/urls.ts +16 -0
- package/src/locales/en_us.json +10 -0
- package/src/locales/ru_ru.json +10 -0
- package/src/locales.ts +5 -0
- package/src/types/index.ts +83 -0
- package/src/types/mdx.ts +55 -0
- package/src/types/turndown-plugin-gfm.d.ts +9 -0
- package/README.md +0 -3
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CheckIcon, ChevronDown } from 'lucide-react';
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuTrigger,
|
|
11
|
+
} from '@terrafirmagreg-team/ui/components/ui/dropdown-menu';
|
|
12
|
+
import { useUiComponents } from '@terrafirmagreg-team/ui/provider';
|
|
13
|
+
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
14
|
+
import { datasetRef, sectionUrl } from '@terrafirmagreg-team/kit/lib/location';
|
|
15
|
+
|
|
16
|
+
import type { SectionKey } from '@terrafirmagreg-team/kit/types';
|
|
17
|
+
|
|
18
|
+
export function VersionMenu({
|
|
19
|
+
section,
|
|
20
|
+
datasetId,
|
|
21
|
+
currentRef,
|
|
22
|
+
label,
|
|
23
|
+
versions,
|
|
24
|
+
onSelect,
|
|
25
|
+
}: {
|
|
26
|
+
section?: SectionKey;
|
|
27
|
+
datasetId: string;
|
|
28
|
+
|
|
29
|
+
currentRef: string;
|
|
30
|
+
|
|
31
|
+
label?: string;
|
|
32
|
+
versions: { versions: string[]; latest?: string } | null | undefined;
|
|
33
|
+
|
|
34
|
+
onSelect?: (version: string | undefined) => void;
|
|
35
|
+
}) {
|
|
36
|
+
const [open, setOpen] = useState(false);
|
|
37
|
+
const pinned =
|
|
38
|
+
currentRef !== datasetId
|
|
39
|
+
? currentRef.slice(datasetId.length + 1)
|
|
40
|
+
: undefined;
|
|
41
|
+
const shown = label ?? pinned;
|
|
42
|
+
const text = shown ? `v${shown}` : datasetId;
|
|
43
|
+
if (!versions || versions.versions.length < 2 || (!section && !onSelect)) {
|
|
44
|
+
return <>{text}</>;
|
|
45
|
+
}
|
|
46
|
+
const ordered = [...versions.versions].sort((a, b) =>
|
|
47
|
+
b.localeCompare(a, undefined, { numeric: true }),
|
|
48
|
+
);
|
|
49
|
+
if (versions.latest) {
|
|
50
|
+
const i = ordered.indexOf(versions.latest);
|
|
51
|
+
if (i > 0) ordered.unshift(ordered.splice(i, 1)[0]);
|
|
52
|
+
}
|
|
53
|
+
return (
|
|
54
|
+
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
|
|
55
|
+
<DropdownMenuTrigger className="inline-flex items-center gap-0.5 outline-none hover:text-foreground">
|
|
56
|
+
{text}
|
|
57
|
+
<ChevronDown className="size-3" />
|
|
58
|
+
</DropdownMenuTrigger>
|
|
59
|
+
<DropdownMenuContent
|
|
60
|
+
side="bottom"
|
|
61
|
+
align="start"
|
|
62
|
+
className="max-h-36 min-w-32"
|
|
63
|
+
>
|
|
64
|
+
{}
|
|
65
|
+
<VersionMenuItems
|
|
66
|
+
ordered={ordered}
|
|
67
|
+
latest={versions.latest}
|
|
68
|
+
datasetId={datasetId}
|
|
69
|
+
currentRef={currentRef}
|
|
70
|
+
section={section}
|
|
71
|
+
onSelect={onSelect}
|
|
72
|
+
/>
|
|
73
|
+
</DropdownMenuContent>
|
|
74
|
+
</DropdownMenu>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function VersionMenuItems({
|
|
79
|
+
ordered,
|
|
80
|
+
latest,
|
|
81
|
+
datasetId,
|
|
82
|
+
currentRef,
|
|
83
|
+
section,
|
|
84
|
+
onSelect,
|
|
85
|
+
}: {
|
|
86
|
+
ordered: string[];
|
|
87
|
+
latest: string | undefined;
|
|
88
|
+
datasetId: string;
|
|
89
|
+
currentRef: string;
|
|
90
|
+
section?: SectionKey;
|
|
91
|
+
onSelect?: (version: string | undefined) => void;
|
|
92
|
+
}) {
|
|
93
|
+
const { Link } = useUiComponents();
|
|
94
|
+
const t = useTranslations();
|
|
95
|
+
|
|
96
|
+
const search = useSearchParams().toString();
|
|
97
|
+
return ordered.map((version) => {
|
|
98
|
+
const isLatest = version === latest;
|
|
99
|
+
const ref = isLatest ? datasetId : datasetRef(datasetId, version);
|
|
100
|
+
return (
|
|
101
|
+
<DropdownMenuItem
|
|
102
|
+
key={version}
|
|
103
|
+
closeOnClick
|
|
104
|
+
render={
|
|
105
|
+
onSelect ? undefined : (
|
|
106
|
+
<Link
|
|
107
|
+
href={`${sectionUrl(section!, ref)}${search ? `?${search}` : ''}`}
|
|
108
|
+
/>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
onClick={
|
|
112
|
+
onSelect ? () => onSelect(isLatest ? undefined : version) : undefined
|
|
113
|
+
}
|
|
114
|
+
className="gap-2 px-2.5 py-1.5 text-xs"
|
|
115
|
+
>
|
|
116
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
117
|
+
<span className="font-medium">v{version}</span>
|
|
118
|
+
{isLatest && (
|
|
119
|
+
<span className="text-2xs text-muted-foreground">
|
|
120
|
+
{t('ui.version_latest')}
|
|
121
|
+
</span>
|
|
122
|
+
)}
|
|
123
|
+
</span>
|
|
124
|
+
{ref === currentRef && <CheckIcon className="size-3.5 text-primary" />}
|
|
125
|
+
</DropdownMenuItem>
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { notFound } from 'next/navigation';
|
|
3
|
+
import type { Metadata } from 'next';
|
|
4
|
+
import { LangContentSwitch } from '@terrafirmagreg-team/ui/components/blocks/lang/content';
|
|
5
|
+
import {
|
|
6
|
+
generateDatasetLandingParams,
|
|
7
|
+
getDatasetLanding,
|
|
8
|
+
resolveDatasetLanding,
|
|
9
|
+
} from '@terrafirmagreg-team/kit/lib/landing';
|
|
10
|
+
import { sectionDatasets } from '@terrafirmagreg-team/kit/lib/datasets';
|
|
11
|
+
import { sectionUrl } from '@terrafirmagreg-team/kit/lib/location';
|
|
12
|
+
import { canonicalUrl } from '@terrafirmagreg-team/kit/lib/urls';
|
|
13
|
+
import type { DatasetLanding, SectionKey } from '@terrafirmagreg-team/kit/types';
|
|
14
|
+
import { langContent } from '@terrafirmagreg-team/translate/server';
|
|
15
|
+
|
|
16
|
+
interface DatasetPageParams {
|
|
17
|
+
params: Promise<{ slug?: string[] }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createDatasetLandingPage<S extends SectionKey>(
|
|
21
|
+
section: S,
|
|
22
|
+
render: (page: DatasetLanding) => ReactNode,
|
|
23
|
+
) {
|
|
24
|
+
async function Page({ params }: DatasetPageParams) {
|
|
25
|
+
const { slug } = await params;
|
|
26
|
+
const datasets = await sectionDatasets(section);
|
|
27
|
+
const content = langContent(
|
|
28
|
+
slug,
|
|
29
|
+
(s, l) => resolveDatasetLanding(section, s, l, datasets),
|
|
30
|
+
render,
|
|
31
|
+
);
|
|
32
|
+
if (!content) notFound();
|
|
33
|
+
return <LangContentSwitch content={content} />;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function generateStaticParams() {
|
|
37
|
+
return generateDatasetLandingParams(section);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function generateMetadata({
|
|
41
|
+
params,
|
|
42
|
+
}: DatasetPageParams): Promise<Metadata> {
|
|
43
|
+
const { slug } = await params;
|
|
44
|
+
const page = getDatasetLanding(
|
|
45
|
+
section,
|
|
46
|
+
slug,
|
|
47
|
+
'en_us',
|
|
48
|
+
await sectionDatasets(section),
|
|
49
|
+
);
|
|
50
|
+
if (!page) notFound();
|
|
51
|
+
return {
|
|
52
|
+
title: page.title,
|
|
53
|
+
description: page.description,
|
|
54
|
+
alternates: {
|
|
55
|
+
canonical: canonicalUrl(
|
|
56
|
+
`${sectionUrl(section)}/${(slug ?? []).join('/')}`,
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { Page, generateStaticParams, generateMetadata };
|
|
63
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createRelativeLink } from '@terrafirmagreg-team/kit/components/relative-link';
|
|
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
|
+
import { LinkPreview } from '@terrafirmagreg-team/ui/components/blocks/link/link-preview';
|
|
6
|
+
import { lookupPagePreview } from '@terrafirmagreg-team/kit/lib/preview/lookup';
|
|
7
|
+
import {
|
|
8
|
+
sectionBreadcrumbItems,
|
|
9
|
+
sectionBreadcrumbPreviews,
|
|
10
|
+
type SectionCrumb,
|
|
11
|
+
} from '@terrafirmagreg-team/kit/lib/breadcrumb';
|
|
12
|
+
import { langContent } from '@terrafirmagreg-team/translate/server';
|
|
13
|
+
import { loadLocaleRegistry } from '@terrafirmagreg-team/translate/server';
|
|
14
|
+
import { readPageSource, translationState } from '@terrafirmagreg-team/translate/server';
|
|
15
|
+
import { resolvePage, type SectionSource } from '@terrafirmagreg-team/kit/lib/page';
|
|
16
|
+
import { findNeighbour } from 'fumadocs-core/page-tree';
|
|
17
|
+
import type {
|
|
18
|
+
DatasetCatalog,
|
|
19
|
+
SectionPage,
|
|
20
|
+
SectionTrees,
|
|
21
|
+
} from '@terrafirmagreg-team/kit/types';
|
|
22
|
+
import type { WorkspacePaginationItems } from '@terrafirmagreg-team/kit/components/workspace';
|
|
23
|
+
import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
|
|
24
|
+
import type { TranslationState } from '@terrafirmagreg-team/translate/server';
|
|
25
|
+
import type {
|
|
26
|
+
ComponentProps,
|
|
27
|
+
ComponentType,
|
|
28
|
+
ReactNode,
|
|
29
|
+
} from 'react';
|
|
30
|
+
|
|
31
|
+
export interface SectionWorkspaceProps {
|
|
32
|
+
page: SectionPage;
|
|
33
|
+
link: (props: ComponentProps<'a'>) => ReactNode | Promise<ReactNode>;
|
|
34
|
+
contentDir: string;
|
|
35
|
+
copied?: TranslationState;
|
|
36
|
+
crumbs?: SectionCrumb[];
|
|
37
|
+
previews?: Record<string, PagePreviewData>;
|
|
38
|
+
footer?: WorkspacePaginationItems;
|
|
39
|
+
history?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function SectionView({
|
|
43
|
+
source,
|
|
44
|
+
slug,
|
|
45
|
+
contentDir,
|
|
46
|
+
trees,
|
|
47
|
+
catalog,
|
|
48
|
+
history,
|
|
49
|
+
workspace: Workspace,
|
|
50
|
+
}: {
|
|
51
|
+
source: SectionSource;
|
|
52
|
+
slug: string[] | undefined;
|
|
53
|
+
contentDir: string;
|
|
54
|
+
trees?: SectionTrees;
|
|
55
|
+
catalog: DatasetCatalog;
|
|
56
|
+
history?: boolean;
|
|
57
|
+
workspace: ComponentType<SectionWorkspaceProps>;
|
|
58
|
+
}) {
|
|
59
|
+
const { defaultLanguage } = loadLocaleRegistry();
|
|
60
|
+
const content = langContent(
|
|
61
|
+
slug,
|
|
62
|
+
(s, l) => resolvePage(source, s, l),
|
|
63
|
+
(page, lang) => {
|
|
64
|
+
const self = page as SectionPage;
|
|
65
|
+
let copied: 'untranslated' | 'partial' | undefined;
|
|
66
|
+
if (lang !== defaultLanguage) {
|
|
67
|
+
const fallback = resolvePage(source, slug, defaultLanguage)?.page as
|
|
68
|
+
| SectionPage
|
|
69
|
+
| undefined;
|
|
70
|
+
if (fallback && fallback !== self) {
|
|
71
|
+
const own = readPageSource(contentDir, self.path);
|
|
72
|
+
const original = readPageSource(contentDir, fallback.path);
|
|
73
|
+
if (own && original) {
|
|
74
|
+
const state = translationState(own, original);
|
|
75
|
+
if (state !== 'ok') copied = state;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const crumbs = sectionBreadcrumbItems(source, self, lang);
|
|
80
|
+
const datasetId = self.url.split('/').filter(Boolean)[1];
|
|
81
|
+
const tree = datasetId ? trees?.[lang]?.[datasetId] : undefined;
|
|
82
|
+
const neighbours = tree ? findNeighbour(tree, self.url) : undefined;
|
|
83
|
+
return (
|
|
84
|
+
<Workspace
|
|
85
|
+
page={self}
|
|
86
|
+
crumbs={crumbs}
|
|
87
|
+
footer={neighbours}
|
|
88
|
+
previews={sectionBreadcrumbPreviews(crumbs, lang, catalog)}
|
|
89
|
+
link={createRelativeLink(
|
|
90
|
+
source as LoaderOutput<LoaderConfig>,
|
|
91
|
+
page as Page,
|
|
92
|
+
(props) => (
|
|
93
|
+
<LinkPreview
|
|
94
|
+
preview={lookupPagePreview(catalog, props.href, lang)}
|
|
95
|
+
{...props}
|
|
96
|
+
/>
|
|
97
|
+
),
|
|
98
|
+
)}
|
|
99
|
+
contentDir={contentDir}
|
|
100
|
+
copied={copied}
|
|
101
|
+
history={history}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
if (!content) notFound();
|
|
107
|
+
return <LangContentSwitch content={content} />;
|
|
108
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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';
|
|
@@ -0,0 +1,73 @@
|
|
|
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('ui.next_page') : t('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
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
}
|
package/src/hooks/toc.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
import type { DatasetCatalog } from '@terrafirmagreg-team/kit/types';
|
|
4
|
+
|
|
5
|
+
export const SectionCatalogContext = createContext<DatasetCatalog | null>(null);
|
|
6
|
+
|
|
7
|
+
export function useSectionCatalog(): DatasetCatalog {
|
|
8
|
+
const catalog = useContext(SectionCatalogContext);
|
|
9
|
+
if (!catalog) throw new Error('SectionCatalogContext is not provided');
|
|
10
|
+
return catalog;
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { usePathname } from 'next/navigation';
|
|
3
|
+
import { parseDatasetRef, parseSectionLocation } from '@terrafirmagreg-team/kit/lib/location';
|
|
4
|
+
import type { SectionDataset } from '@terrafirmagreg-team/kit/types';
|
|
5
|
+
|
|
6
|
+
export function useCurrentDataset(
|
|
7
|
+
datasets: readonly SectionDataset[],
|
|
8
|
+
): string | undefined {
|
|
9
|
+
const pathname = usePathname();
|
|
10
|
+
const { dataset } = parseSectionLocation(pathname);
|
|
11
|
+
if (!dataset) return undefined;
|
|
12
|
+
const { id } = parseDatasetRef(dataset);
|
|
13
|
+
return datasets.some((row) => row.id === id) ? dataset : undefined;
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useLayoutEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
export function useDatasetAccent(accent?: string) {
|
|
5
|
+
useLayoutEffect(() => {
|
|
6
|
+
if (!accent) return;
|
|
7
|
+
const root = document.documentElement;
|
|
8
|
+
root.style.setProperty('--color-link-accent', accent);
|
|
9
|
+
return () => {
|
|
10
|
+
root.style.removeProperty('--color-link-accent');
|
|
11
|
+
};
|
|
12
|
+
}, [accent]);
|
|
13
|
+
}
|