@terrafirmagreg-team/kit 0.0.0-stage → 1.2.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 +56 -3
- package/src/components/blocks/nav-title.tsx +62 -0
- package/src/components/outline/dropdown.tsx +68 -0
- package/src/components/outline/index.ts +11 -0
- package/src/components/outline/list.tsx +121 -0
- package/src/components/outline/outline.tsx +50 -0
- package/src/components/outline/popover.tsx +77 -0
- package/src/components/page-actions.tsx +202 -0
- package/src/components/relative-link.tsx +19 -0
- package/src/components/toc/base.tsx +66 -0
- package/src/components/toc/block.tsx +112 -0
- package/src/components/toc/clerk.tsx +266 -0
- package/src/components/toc/default.tsx +334 -0
- package/src/components/version-menu.tsx +130 -0
- package/src/components/views/dataset.tsx +72 -0
- package/src/components/views/section.tsx +152 -0
- package/src/components/workspace/content.tsx +54 -0
- package/src/components/workspace/index.ts +13 -0
- package/src/components/workspace/pagination.tsx +74 -0
- package/src/components/workspace/workspace.tsx +149 -0
- package/src/hooks/use-catalog.ts +12 -0
- package/src/hooks/use-current-dataset.ts +24 -0
- package/src/hooks/use-dataset-accent.ts +14 -0
- package/src/hooks/use-nav-options.ts +87 -0
- package/src/hooks/use-section-names.ts +40 -0
- package/src/hooks/use-toc.ts +10 -0
- package/src/hooks/use-version-pref.ts +31 -0
- package/src/hooks/use-versioned-dataset.ts +21 -0
- package/src/lib/breadcrumb.ts +65 -0
- package/src/lib/config.ts +111 -0
- package/src/lib/dataset-links.ts +31 -0
- package/src/lib/datasets.ts +241 -0
- package/src/lib/landing.ts +48 -0
- package/src/lib/layout.ts +16 -0
- package/src/lib/locale/renderer.ts +49 -0
- package/src/lib/order.ts +89 -0
- package/src/lib/page.ts +109 -0
- package/src/lib/pin-tree.ts +38 -0
- package/src/lib/preview/lookup.ts +51 -0
- package/src/lib/section/schema.ts +32 -0
- package/src/lib/section/source.ts +77 -0
- package/src/lib/sources.ts +22 -0
- package/src/lib/tree.ts +61 -0
- package/src/lib/urls.ts +17 -0
- package/src/locales/en_us.json +7 -0
- package/src/locales/ru_ru.json +7 -0
- package/src/locales.ts +6 -0
- package/src/types/embeds.ts +54 -0
- package/src/types/index.ts +87 -0
- package/src/types/outline.ts +30 -0
- package/README.md +0 -3
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { usePathname } from 'next/navigation';
|
|
4
|
+
import { useLocationSearchParams } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
|
|
5
|
+
import {
|
|
6
|
+
type DatasetRef,
|
|
7
|
+
parseSectionLocation,
|
|
8
|
+
resolveDatasetRef,
|
|
9
|
+
} from '@terrafirmagreg-team/sites/location';
|
|
10
|
+
import type { SectionDataset } from '@terrafirmagreg-team/kit/types';
|
|
11
|
+
|
|
12
|
+
export function useCurrentDataset(
|
|
13
|
+
datasets: readonly SectionDataset[],
|
|
14
|
+
): DatasetRef | undefined {
|
|
15
|
+
const pathname = usePathname();
|
|
16
|
+
const versionParam = useLocationSearchParams().get('version') ?? undefined;
|
|
17
|
+
const { dataset } = parseSectionLocation(pathname);
|
|
18
|
+
const meta = datasets.find((row) => row.id === dataset);
|
|
19
|
+
return useMemo(
|
|
20
|
+
() => dataset && meta ? resolveDatasetRef(dataset, versionParam, meta).ref : undefined,
|
|
21
|
+
[dataset, versionParam, meta],
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
}
|
|
14
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { type ReactNode, createElement, useMemo } from 'react';
|
|
3
|
+
import { DynamicIcon, type IconName } from 'lucide-react/dynamic';
|
|
4
|
+
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
5
|
+
import { sectionTabs } from '@terrafirmagreg-team/sites/sections';
|
|
6
|
+
import {
|
|
7
|
+
isDumpSection,
|
|
8
|
+
resolveDatasetRef,
|
|
9
|
+
sectionUrl,
|
|
10
|
+
} from '@terrafirmagreg-team/sites/location';
|
|
11
|
+
import { useCurrentDataset } from '@terrafirmagreg-team/kit/hooks/use-current-dataset';
|
|
12
|
+
import { useSectionNames } from '@terrafirmagreg-team/kit/hooks/use-section-names';
|
|
13
|
+
import { useVersionPreference } from '@terrafirmagreg-team/kit/hooks/use-version-pref';
|
|
14
|
+
import type { LeftSidebarScope } from '@terrafirmagreg-team/kit/types';
|
|
15
|
+
import type {
|
|
16
|
+
SwitcherOption,
|
|
17
|
+
SwitcherProps,
|
|
18
|
+
} from '@terrafirmagreg-team/ui/types/nav';
|
|
19
|
+
|
|
20
|
+
function datasetIcon(dataset: { icon?: IconName }): ReactNode {
|
|
21
|
+
if (!dataset.icon) return undefined;
|
|
22
|
+
return createElement(DynamicIcon, {
|
|
23
|
+
name: dataset.icon,
|
|
24
|
+
className: 'size-4 shrink-0',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useNavOptions({ section, catalog }: LeftSidebarScope): {
|
|
29
|
+
section: SwitcherProps;
|
|
30
|
+
dataset: SwitcherProps;
|
|
31
|
+
} {
|
|
32
|
+
const t = useTranslations();
|
|
33
|
+
const sectionNames = useSectionNames();
|
|
34
|
+
const current = useCurrentDataset(catalog[section]);
|
|
35
|
+
const baseId = current ? current.id : undefined;
|
|
36
|
+
const pinned = current ? current.version : undefined;
|
|
37
|
+
const preferred = useVersionPreference(baseId);
|
|
38
|
+
const version = pinned ?? preferred;
|
|
39
|
+
|
|
40
|
+
const sectionOptions: SwitcherOption[] = useMemo(
|
|
41
|
+
() =>
|
|
42
|
+
sectionTabs.map(({ key, icon: Icon }) => {
|
|
43
|
+
const meta = baseId
|
|
44
|
+
? catalog[key].find(({ id }) => id === baseId)
|
|
45
|
+
: undefined;
|
|
46
|
+
const pinned =
|
|
47
|
+
isDumpSection(key) && baseId
|
|
48
|
+
? resolveDatasetRef(baseId, version, meta).pinned
|
|
49
|
+
: undefined;
|
|
50
|
+
const suffix = pinned ? `?version=${encodeURIComponent(pinned)}` : '';
|
|
51
|
+
return {
|
|
52
|
+
value: key,
|
|
53
|
+
title: sectionNames[key]?.title ?? key,
|
|
54
|
+
description: sectionNames[key]?.desc,
|
|
55
|
+
icon: Icon ? createElement(Icon) : undefined,
|
|
56
|
+
url: meta ? `${sectionUrl(key, baseId)}${suffix}` : sectionUrl(key),
|
|
57
|
+
};
|
|
58
|
+
}),
|
|
59
|
+
[sectionNames, baseId, version, catalog],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const datasetOptions: SwitcherOption[] = useMemo(
|
|
63
|
+
() =>
|
|
64
|
+
catalog[section].map((datasetItem) => ({
|
|
65
|
+
value: datasetItem.id,
|
|
66
|
+
title: datasetItem.title ?? datasetItem.id,
|
|
67
|
+
description: datasetItem.description,
|
|
68
|
+
icon: datasetIcon(datasetItem),
|
|
69
|
+
url: sectionUrl(section, datasetItem.id),
|
|
70
|
+
})),
|
|
71
|
+
[section, catalog],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
section: {
|
|
76
|
+
options: sectionOptions,
|
|
77
|
+
selected: section,
|
|
78
|
+
placeholder: t('Select section', { note: 'ui.select_section' }),
|
|
79
|
+
},
|
|
80
|
+
dataset: {
|
|
81
|
+
options: datasetOptions,
|
|
82
|
+
selected: baseId,
|
|
83
|
+
placeholder: t('Select dataset', { note: 'ui.select_dataset' }),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
4
|
+
|
|
5
|
+
export function useSectionNames(): Record<
|
|
6
|
+
string,
|
|
7
|
+
{ title: string; desc: string }
|
|
8
|
+
> {
|
|
9
|
+
const t = useTranslations();
|
|
10
|
+
return useMemo(
|
|
11
|
+
() => ({
|
|
12
|
+
docs: {
|
|
13
|
+
title: t('Docs', { note: 'sections.docs' }),
|
|
14
|
+
desc: t('Documentation', { note: 'sections.desc.docs' }),
|
|
15
|
+
},
|
|
16
|
+
changelog: {
|
|
17
|
+
title: t('Changelog', { note: 'sections.changelog' }),
|
|
18
|
+
desc: t('Release history and version notes', {
|
|
19
|
+
note: 'sections.desc.changelog',
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
recipe: {
|
|
23
|
+
title: t('Recipes', { note: 'sections.recipe' }),
|
|
24
|
+
desc: t('Crafting and processing recipes', {
|
|
25
|
+
note: 'sections.desc.recipe',
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
quest: {
|
|
29
|
+
title: t('Quests', { note: 'sections.quest' }),
|
|
30
|
+
desc: t('Quest chapters and objectives', { note: 'sections.desc.quest' }),
|
|
31
|
+
},
|
|
32
|
+
guide: {
|
|
33
|
+
title: t('Guide', { note: 'sections.guide' }),
|
|
34
|
+
desc: t('Gameplay guide', { note: 'sections.desc.guide' }),
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
[t],
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { TOCItemType } from 'fumadocs-core/toc';
|
|
3
|
+
import { createContext, use } from 'react';
|
|
4
|
+
|
|
5
|
+
export const TOCContext = createContext<TOCItemType[]>([]);
|
|
6
|
+
|
|
7
|
+
export function useTOCItems(): TOCItemType[] {
|
|
8
|
+
return use(TOCContext);
|
|
9
|
+
}
|
|
10
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
jsonCodec,
|
|
6
|
+
readStoredValue,
|
|
7
|
+
subscribeStoredValue,
|
|
8
|
+
writeStoredValue,
|
|
9
|
+
} from '@terrafirmagreg-team/ui/lib/utils/stored-value';
|
|
10
|
+
|
|
11
|
+
const STORAGE_KEY = 'tfg.dataset-versions';
|
|
12
|
+
const codec = jsonCodec<Record<string, string>>({});
|
|
13
|
+
|
|
14
|
+
export function setVersionPreference(datasetId: string, version?: string) {
|
|
15
|
+
const next = { ...readStoredValue(STORAGE_KEY, codec.read) };
|
|
16
|
+
if (version) next[datasetId] = version;
|
|
17
|
+
else delete next[datasetId];
|
|
18
|
+
writeStoredValue(STORAGE_KEY, next, codec.write);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useVersionPreference(datasetId?: string): string | undefined {
|
|
22
|
+
return useSyncExternalStore(
|
|
23
|
+
(onChange) => subscribeStoredValue(STORAGE_KEY, onChange),
|
|
24
|
+
() =>
|
|
25
|
+
datasetId
|
|
26
|
+
? readStoredValue(STORAGE_KEY, codec.read)[datasetId]
|
|
27
|
+
: undefined,
|
|
28
|
+
() => undefined,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useLocationSearchParams } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
|
|
3
|
+
import {
|
|
4
|
+
resolveDatasetRef,
|
|
5
|
+
type DatasetVersionInfo,
|
|
6
|
+
type DatasetRef,
|
|
7
|
+
} from '@terrafirmagreg-team/sites/location';
|
|
8
|
+
|
|
9
|
+
const refs = new Map<string, DatasetRef | undefined>();
|
|
10
|
+
|
|
11
|
+
export function useVersionedDataset(
|
|
12
|
+
id: string | undefined,
|
|
13
|
+
info?: DatasetVersionInfo,
|
|
14
|
+
): DatasetRef | undefined {
|
|
15
|
+
const version = useLocationSearchParams().get('version') ?? undefined;
|
|
16
|
+
const key = `${id ?? ''} ${version ?? ''} ${info?.latest ?? ''} ${info?.versions?.join(',') ?? ''}`;
|
|
17
|
+
if (!refs.has(key))
|
|
18
|
+
refs.set(key, id ? resolveDatasetRef(id, version, info).ref : undefined);
|
|
19
|
+
return refs.get(key);
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import type * as PageTree from 'fumadocs-core/page-tree';
|
|
3
|
+
import { searchPath } from 'fumadocs-core/breadcrumb';
|
|
4
|
+
import { stripLangFromUrl } from '@terrafirmagreg-team/translate/lib/slug';
|
|
5
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
6
|
+
import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
|
|
7
|
+
import { loadLocaleRegistry } from '@terrafirmagreg-team/translate/server';
|
|
8
|
+
import { localizePageTree } from '@terrafirmagreg-team/translate/server';
|
|
9
|
+
import { sectionConfig } from './config';
|
|
10
|
+
import { lookupPagePreview } from './preview/lookup';
|
|
11
|
+
import { datasetId } from './tree';
|
|
12
|
+
import type { SectionSource } from './page';
|
|
13
|
+
import type { DatasetCatalog, SectionPage } from '@terrafirmagreg-team/kit/types';
|
|
14
|
+
|
|
15
|
+
export type SectionCrumb = { name: string; url?: string };
|
|
16
|
+
|
|
17
|
+
function nodeName(name: unknown): string {
|
|
18
|
+
return typeof name === 'string' ? name : '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function sectionBreadcrumbItems(
|
|
22
|
+
source: SectionSource,
|
|
23
|
+
page: SectionPage,
|
|
24
|
+
lang: Lang,
|
|
25
|
+
): SectionCrumb[] {
|
|
26
|
+
const languages = [...sectionConfig().knownLanguages];
|
|
27
|
+
const tree = localizePageTree(source.getPageTree(), lang, {
|
|
28
|
+
languages,
|
|
29
|
+
fallbackLanguage: loadLocaleRegistry().defaultLanguage,
|
|
30
|
+
});
|
|
31
|
+
const dataset = page.slugs[0];
|
|
32
|
+
const folder = tree.children.find(
|
|
33
|
+
(node): node is PageTree.Folder =>
|
|
34
|
+
node.type === 'folder' && datasetId(node) === dataset,
|
|
35
|
+
);
|
|
36
|
+
if (!folder) return [{ name: page.data.title }];
|
|
37
|
+
|
|
38
|
+
const pageUrl = stripLangFromUrl(page.url, languages);
|
|
39
|
+
if (folder.index?.url === pageUrl) return [];
|
|
40
|
+
|
|
41
|
+
const trail = searchPath(folder.children, pageUrl);
|
|
42
|
+
const items: SectionCrumb[] = [];
|
|
43
|
+
for (const node of trail?.slice(0, -1) ?? []) {
|
|
44
|
+
if (node.type === 'folder') {
|
|
45
|
+
items.push({ name: nodeName(node.name), url: node.index?.url });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
items.push({ name: page.data.title });
|
|
49
|
+
return items;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function sectionBreadcrumbPreviews(
|
|
53
|
+
items: readonly SectionCrumb[],
|
|
54
|
+
lang: Lang,
|
|
55
|
+
catalog: DatasetCatalog,
|
|
56
|
+
): Record<string, PagePreviewData> {
|
|
57
|
+
const previews: Record<string, PagePreviewData> = {};
|
|
58
|
+
for (const item of items) {
|
|
59
|
+
if (!item.url) continue;
|
|
60
|
+
const preview = lookupPagePreview(catalog, item.url, lang);
|
|
61
|
+
if (preview) previews[item.url] = preview;
|
|
62
|
+
}
|
|
63
|
+
return previews;
|
|
64
|
+
}
|
|
65
|
+
|