@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.
Files changed (78) hide show
  1. package/package.json +68 -3
  2. package/src/components/blocks/nav-title.tsx +65 -0
  3. package/src/components/docs-image.tsx +49 -0
  4. package/src/components/outline/dropdown.tsx +67 -0
  5. package/src/components/outline/index.ts +10 -0
  6. package/src/components/outline/list.tsx +120 -0
  7. package/src/components/outline/outline.tsx +49 -0
  8. package/src/components/outline/popover.tsx +76 -0
  9. package/src/components/outline/types.ts +29 -0
  10. package/src/components/page-actions.tsx +201 -0
  11. package/src/components/relative-link.tsx +18 -0
  12. package/src/components/search/dialog.tsx +145 -0
  13. package/src/components/toc/base.tsx +65 -0
  14. package/src/components/toc/block.tsx +111 -0
  15. package/src/components/toc/clerk.tsx +265 -0
  16. package/src/components/toc/default.tsx +333 -0
  17. package/src/components/version-menu.tsx +128 -0
  18. package/src/components/views/dataset.tsx +63 -0
  19. package/src/components/views/section.tsx +108 -0
  20. package/src/components/workspace/content.tsx +53 -0
  21. package/src/components/workspace/index.ts +12 -0
  22. package/src/components/workspace/pagination.tsx +73 -0
  23. package/src/components/workspace/workspace.tsx +148 -0
  24. package/src/hooks/toc.ts +9 -0
  25. package/src/hooks/use-catalog.ts +11 -0
  26. package/src/hooks/use-current-dataset.ts +14 -0
  27. package/src/hooks/use-dataset-accent.ts +13 -0
  28. package/src/hooks/use-nav-options.ts +84 -0
  29. package/src/hooks/use-version-pref.ts +30 -0
  30. package/src/lib/breadcrumb.ts +78 -0
  31. package/src/lib/config.ts +109 -0
  32. package/src/lib/dataset-links.ts +30 -0
  33. package/src/lib/datasets.ts +211 -0
  34. package/src/lib/landing.ts +46 -0
  35. package/src/lib/layout.ts +15 -0
  36. package/src/lib/legacy.ts +18 -0
  37. package/src/lib/location.ts +63 -0
  38. package/src/lib/markdown/dom.ts +90 -0
  39. package/src/lib/markdown/index.ts +13 -0
  40. package/src/lib/markdown/providers.ts +25 -0
  41. package/src/lib/mdx/accordions.ts +93 -0
  42. package/src/lib/mdx/banner.ts +52 -0
  43. package/src/lib/mdx/directive-utils.ts +125 -0
  44. package/src/lib/mdx/directives.ts +141 -0
  45. package/src/lib/mdx/game-export.ts +44 -0
  46. package/src/lib/mdx/html-style.ts +135 -0
  47. package/src/lib/mdx/preview.ts +252 -0
  48. package/src/lib/mdx/references.ts +56 -0
  49. package/src/lib/mdx/tabs.ts +80 -0
  50. package/src/lib/mdx-components.tsx +139 -0
  51. package/src/lib/order.ts +88 -0
  52. package/src/lib/pack-assets.ts +45 -0
  53. package/src/lib/page.ts +108 -0
  54. package/src/lib/pin-tree.ts +35 -0
  55. package/src/lib/preview/index.ts +8 -0
  56. package/src/lib/preview/lookup.ts +48 -0
  57. package/src/lib/preview/og.tsx +115 -0
  58. package/src/lib/preview/resolvers.ts +98 -0
  59. package/src/lib/search/config.ts +13 -0
  60. package/src/lib/search/filters.ts +22 -0
  61. package/src/lib/search/index.ts +19 -0
  62. package/src/lib/search/indexes.ts +43 -0
  63. package/src/lib/search/merge.ts +22 -0
  64. package/src/lib/search/server.ts +18 -0
  65. package/src/lib/search/sources.ts +19 -0
  66. package/src/lib/search/structured.ts +23 -0
  67. package/src/lib/section/schema.ts +36 -0
  68. package/src/lib/section/source.ts +40 -0
  69. package/src/lib/sources.ts +21 -0
  70. package/src/lib/tree.ts +60 -0
  71. package/src/lib/urls.ts +16 -0
  72. package/src/locales/en_us.json +10 -0
  73. package/src/locales/ru_ru.json +10 -0
  74. package/src/locales.ts +5 -0
  75. package/src/types/index.ts +83 -0
  76. package/src/types/mdx.ts +55 -0
  77. package/src/types/turndown-plugin-gfm.d.ts +9 -0
  78. 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
+ }
@@ -0,0 +1,9 @@
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
+ }
@@ -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
+ }