@terrafirmagreg-team/kit 1.2.3 → 1.2.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/kit",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "type": "module",
5
5
  "description": "Fumadocs content engine",
6
6
  "license": "LGPL-3.0-only",
@@ -25,12 +25,12 @@
25
25
  "@base-ui/react": "^1.9.0",
26
26
  "@icons-pack/react-simple-icons": "13.15.1",
27
27
  "@terrafirmagreg-team/data": "1.2.1",
28
- "@terrafirmagreg-team/mdx": "1.2.1",
28
+ "@terrafirmagreg-team/mdx": "1.2.2",
29
29
  "@terrafirmagreg-team/sites": "1.2.1",
30
- "@terrafirmagreg-team/translate": "1.2.3",
31
- "@terrafirmagreg-team/ui": "1.2.2",
32
- "fumadocs-core": "16.17.8",
33
- "lucide-react": "^0.577.0",
30
+ "@terrafirmagreg-team/translate": "1.2.4",
31
+ "@terrafirmagreg-team/ui": "1.2.4",
32
+ "fumadocs-core": "16.17.10",
33
+ "lucide-react": "^1.63.0",
34
34
  "server-only": "^0.0.1",
35
35
  "zod": "4.6.7"
36
36
  },
@@ -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={toc}>
37
- <AnchorProvider toc={toc} {...props}>
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
- return ordered.map((version) => {
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 params = new URLSearchParams(current);
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
- Workspace,
10
- type WorkspaceProps,
11
- } from '@terrafirmagreg-team/kit/components/workspace';
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 interface SectionLandingView {
62
- node: ReactNode;
63
- workspace?: WorkspaceProps;
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<S extends SectionKey> {
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<SectionSource>;
71
- contentDir: string;
80
+ source: {
81
+ getSource: () => TSource | Promise<TSource>;
82
+ contentDir?: string;
72
83
  history?: boolean;
73
84
  };
74
- sidebar?: {
75
- feature?: ComponentType;
76
- useTree?: (tree: PageTree.Root) => PageTree.Root;
77
- };
85
+ features?: SectionFeatures;
78
86
  layout: ComponentType<SectionLayoutViewProps>;
79
87
  landing?: (
80
- page: DatasetLanding,
81
- lang: Lang,
88
+ props: SectionLandingProps<TSource>,
82
89
  ) => SectionLandingView | Promise<SectionLandingView>;
83
- page?: ComponentType<SectionWorkspaceProps>;
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<S extends SectionKey>({
107
+ export function createSectionRoutes<
108
+ S extends SectionKey,
109
+ TSource = SectionSource,
110
+ >({
101
111
  section,
102
112
  trees,
103
113
  source,
104
- sidebar,
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: SectionSource | undefined, datasets: readonly SectionDataset[]) =>
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 (!src) return { page, isFallback: false };
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 src ? resolvePage(src, slug, lang) : undefined;
138
+ return PageView ? resolvePage(src as SectionSource, slug, lang) : undefined;
129
139
  };
130
140
 
131
141
  const render = async (
132
- src: SectionSource | undefined,
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(page as DatasetLanding, lang);
141
- return <Workspace {...view.workspace}>{view.node}</Workspace>;
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 (!src || !PageView) return null;
163
+ if (!PageView) return null;
144
164
  return renderSectionWorkspace({
145
- source: src,
165
+ source: src as SectionSource,
146
166
  slug,
147
- contentDir: source?.contentDir ?? `content/${section}`,
167
+ contentDir: source.contentDir ?? `content/${section}`,
148
168
  trees: resolvedTrees,
149
169
  catalog,
150
- history: source?.history,
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?.getSource(), await sectionDatasets(section))(
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?.getSource(),
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?.getSource(),
216
+ source.getSource(),
198
217
  sectionDatasets(section),
199
218
  ]);
200
219
 
201
- if (slug?.length === 1 && src && !landing) {
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 (source) {
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
- const src = await source?.getSource();
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 type { WorkspacePaginationItems } from '@terrafirmagreg-team/kit/components/workspace';
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 function renderSectionWorkspace({
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
- workspace: Workspace,
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
- workspace: ComponentType<SectionWorkspaceProps>;
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 neighbours = tree ? findNeighbour(tree, page.url) : undefined;
85
- return (
86
- <Workspace
87
- page={page}
88
- crumbs={crumbs}
89
- footer={neighbours}
90
- previews={sectionBreadcrumbPreviews(crumbs, lang, catalog)}
91
- link={createRelativeLink(
92
- source as LoaderOutput<LoaderConfig>,
93
- page as unknown as Page,
94
- (props) => (
95
- <LinkPreview
96
- preview={lookupPagePreview(catalog, props.href, lang)}
97
- {...props}
98
- />
99
- ),
100
- )}
101
- contentDir={contentDir}
102
- copied={copied}
103
- history={history}
104
- />
105
- );
106
- }
107
-
108
- export async function SectionView({
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,20 @@
1
+ import type { SectionEmbeds } from '@terrafirmagreg-team/kit/types/embeds';
2
+
3
+ export type SectionEmbedId = keyof SectionEmbeds;
4
+
5
+ type EmbedOf<K extends SectionEmbedId> = NonNullable<SectionEmbeds[K]>;
6
+
7
+ const registry = ((globalThis as Record<symbol, unknown>)[
8
+ Symbol.for('terrafirmagreg.embeds')
9
+ ] ??= new Map<SectionEmbedId, unknown>()) as Map<SectionEmbedId, unknown>;
10
+
11
+ export function registerSectionEmbed<K extends SectionEmbedId>(
12
+ id: K,
13
+ embed: EmbedOf<K>,
14
+ ): void {
15
+ registry.set(id, embed);
16
+ }
17
+
18
+ export function sectionEmbeds(): SectionEmbeds {
19
+ return Object.fromEntries(registry) as SectionEmbeds;
20
+ }
@@ -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
-