@terrafirmagreg-team/kit 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +68 -3
- package/src/components/blocks/nav-title.tsx +65 -0
- package/src/components/docs-image.tsx +49 -0
- package/src/components/outline/dropdown.tsx +67 -0
- package/src/components/outline/index.ts +10 -0
- package/src/components/outline/list.tsx +120 -0
- package/src/components/outline/outline.tsx +49 -0
- package/src/components/outline/popover.tsx +76 -0
- package/src/components/outline/types.ts +29 -0
- package/src/components/page-actions.tsx +201 -0
- package/src/components/relative-link.tsx +18 -0
- package/src/components/search/dialog.tsx +145 -0
- package/src/components/toc/base.tsx +65 -0
- package/src/components/toc/block.tsx +111 -0
- package/src/components/toc/clerk.tsx +265 -0
- package/src/components/toc/default.tsx +333 -0
- package/src/components/version-menu.tsx +128 -0
- package/src/components/views/dataset.tsx +63 -0
- package/src/components/views/section.tsx +108 -0
- package/src/components/workspace/content.tsx +53 -0
- package/src/components/workspace/index.ts +12 -0
- package/src/components/workspace/pagination.tsx +73 -0
- package/src/components/workspace/workspace.tsx +148 -0
- package/src/hooks/toc.ts +9 -0
- package/src/hooks/use-catalog.ts +11 -0
- package/src/hooks/use-current-dataset.ts +14 -0
- package/src/hooks/use-dataset-accent.ts +13 -0
- package/src/hooks/use-nav-options.ts +84 -0
- package/src/hooks/use-version-pref.ts +30 -0
- package/src/lib/breadcrumb.ts +78 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/dataset-links.ts +30 -0
- package/src/lib/datasets.ts +211 -0
- package/src/lib/landing.ts +46 -0
- package/src/lib/layout.ts +15 -0
- package/src/lib/legacy.ts +18 -0
- package/src/lib/location.ts +63 -0
- package/src/lib/markdown/dom.ts +90 -0
- package/src/lib/markdown/index.ts +13 -0
- package/src/lib/markdown/providers.ts +25 -0
- package/src/lib/mdx/accordions.ts +93 -0
- package/src/lib/mdx/banner.ts +52 -0
- package/src/lib/mdx/directive-utils.ts +125 -0
- package/src/lib/mdx/directives.ts +141 -0
- package/src/lib/mdx/game-export.ts +44 -0
- package/src/lib/mdx/html-style.ts +135 -0
- package/src/lib/mdx/preview.ts +252 -0
- package/src/lib/mdx/references.ts +56 -0
- package/src/lib/mdx/tabs.ts +80 -0
- package/src/lib/mdx-components.tsx +139 -0
- package/src/lib/order.ts +88 -0
- package/src/lib/pack-assets.ts +45 -0
- package/src/lib/page.ts +108 -0
- package/src/lib/pin-tree.ts +35 -0
- package/src/lib/preview/index.ts +8 -0
- package/src/lib/preview/lookup.ts +48 -0
- package/src/lib/preview/og.tsx +115 -0
- package/src/lib/preview/resolvers.ts +98 -0
- package/src/lib/search/config.ts +13 -0
- package/src/lib/search/filters.ts +22 -0
- package/src/lib/search/index.ts +19 -0
- package/src/lib/search/indexes.ts +43 -0
- package/src/lib/search/merge.ts +22 -0
- package/src/lib/search/server.ts +18 -0
- package/src/lib/search/sources.ts +19 -0
- package/src/lib/search/structured.ts +23 -0
- package/src/lib/section/schema.ts +36 -0
- package/src/lib/section/source.ts +40 -0
- package/src/lib/sources.ts +21 -0
- package/src/lib/tree.ts +60 -0
- package/src/lib/urls.ts +16 -0
- package/src/locales/en_us.json +10 -0
- package/src/locales/ru_ru.json +10 -0
- package/src/locales.ts +5 -0
- package/src/types/index.ts +83 -0
- package/src/types/mdx.ts +55 -0
- package/src/types/turndown-plugin-gfm.d.ts +9 -0
- package/README.md +0 -3
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { type ComponentProps } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Check,
|
|
5
|
+
ChevronDown,
|
|
6
|
+
Copy,
|
|
7
|
+
ExternalLinkIcon,
|
|
8
|
+
Languages,
|
|
9
|
+
TextIcon,
|
|
10
|
+
} from 'lucide-react';
|
|
11
|
+
import { SiGithub } from '@icons-pack/react-simple-icons';
|
|
12
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
13
|
+
import { Button } from '@terrafirmagreg-team/ui/components/ui/button';
|
|
14
|
+
import {
|
|
15
|
+
Popover,
|
|
16
|
+
PopoverTrigger,
|
|
17
|
+
PopoverContent,
|
|
18
|
+
} from '@terrafirmagreg-team/ui/components/ui/popover';
|
|
19
|
+
import { ItemRow } from '@terrafirmagreg-team/ui/components/blocks/list/row';
|
|
20
|
+
import { useUntranslated } from '@terrafirmagreg-team/ui/hooks/lang';
|
|
21
|
+
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
22
|
+
import { useCopyButton } from '@terrafirmagreg-team/ui/hooks/use-copy-button';
|
|
23
|
+
import {
|
|
24
|
+
openPageMarkdown,
|
|
25
|
+
pageMarkdown,
|
|
26
|
+
} from '@terrafirmagreg-team/kit/lib/markdown';
|
|
27
|
+
import type { TranslationState } from '@terrafirmagreg-team/translate/server';
|
|
28
|
+
|
|
29
|
+
function ActionButton({
|
|
30
|
+
className,
|
|
31
|
+
...props
|
|
32
|
+
}: ComponentProps<typeof Button>) {
|
|
33
|
+
return (
|
|
34
|
+
<Button
|
|
35
|
+
variant="secondary"
|
|
36
|
+
size="sm"
|
|
37
|
+
className={cn(
|
|
38
|
+
'gap-2 border-foreground/10 hover:bg-background dark:hover:bg-accent [&_svg]:size-3.5 [&_svg]:text-muted-foreground',
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function MarkdownCopyButton({
|
|
47
|
+
getMarkdown,
|
|
48
|
+
...props
|
|
49
|
+
}: ComponentProps<'button'> & {
|
|
50
|
+
getMarkdown: () => Promise<string>;
|
|
51
|
+
}) {
|
|
52
|
+
const t = useTranslations();
|
|
53
|
+
const { isLoading, checked, error, onClick } = useCopyButton(getMarkdown);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<ActionButton
|
|
57
|
+
disabled={isLoading}
|
|
58
|
+
aria-live="polite"
|
|
59
|
+
onClick={onClick}
|
|
60
|
+
{...props}
|
|
61
|
+
>
|
|
62
|
+
{checked ? <Check /> : <Copy />}
|
|
63
|
+
{props.children ??
|
|
64
|
+
(error
|
|
65
|
+
? t('ui.copy_error')
|
|
66
|
+
: checked
|
|
67
|
+
? t('ui.copied_markdown')
|
|
68
|
+
: t('ui.copy_markdown'))}
|
|
69
|
+
</ActionButton>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function CopyPageMarkdown({ title }: { title: string }) {
|
|
74
|
+
return (
|
|
75
|
+
<MarkdownCopyButton
|
|
76
|
+
getMarkdown={() =>
|
|
77
|
+
pageMarkdown(document.querySelector('[data-md-root]'), title)
|
|
78
|
+
}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function CrowdinButton({ url }: { url: string }) {
|
|
84
|
+
const t = useTranslations();
|
|
85
|
+
return (
|
|
86
|
+
<ActionButton
|
|
87
|
+
nativeButton={false}
|
|
88
|
+
render={<a href={url} target="_blank" rel="noreferrer" />}
|
|
89
|
+
>
|
|
90
|
+
<Languages />
|
|
91
|
+
{t('untranslated.translate')}
|
|
92
|
+
</ActionButton>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function UntranslatedActions({
|
|
97
|
+
copied,
|
|
98
|
+
crowdinUrl,
|
|
99
|
+
}: {
|
|
100
|
+
copied?: TranslationState;
|
|
101
|
+
crowdinUrl?: string;
|
|
102
|
+
}) {
|
|
103
|
+
const missing = useUntranslated();
|
|
104
|
+
if (!crowdinUrl || (!missing && !copied)) return null;
|
|
105
|
+
return <CrowdinButton url={crowdinUrl} />;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function ViewOptionsPopover({
|
|
109
|
+
githubUrl,
|
|
110
|
+
title,
|
|
111
|
+
mdUrl,
|
|
112
|
+
...props
|
|
113
|
+
}: ComponentProps<typeof PopoverTrigger> & {
|
|
114
|
+
githubUrl?: string;
|
|
115
|
+
title?: string;
|
|
116
|
+
mdUrl?: string;
|
|
117
|
+
}) {
|
|
118
|
+
const t = useTranslations();
|
|
119
|
+
const items = [
|
|
120
|
+
githubUrl && {
|
|
121
|
+
title: t('ui.open_github'),
|
|
122
|
+
href: githubUrl,
|
|
123
|
+
icon: <SiGithub color="currentColor" className="size-4" />,
|
|
124
|
+
},
|
|
125
|
+
(mdUrl || title !== undefined) && {
|
|
126
|
+
title: t('ui.view_markdown'),
|
|
127
|
+
...(mdUrl
|
|
128
|
+
? { href: mdUrl }
|
|
129
|
+
: {
|
|
130
|
+
onClick: () =>
|
|
131
|
+
void openPageMarkdown(
|
|
132
|
+
document.querySelector('[data-md-root]'),
|
|
133
|
+
title!,
|
|
134
|
+
),
|
|
135
|
+
}),
|
|
136
|
+
icon: <TextIcon className="size-4" />,
|
|
137
|
+
},
|
|
138
|
+
].filter((v) => !!v);
|
|
139
|
+
if (!items.length) return null;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Popover>
|
|
143
|
+
<PopoverTrigger
|
|
144
|
+
{...props}
|
|
145
|
+
render={
|
|
146
|
+
<Button
|
|
147
|
+
variant="secondary"
|
|
148
|
+
size="sm"
|
|
149
|
+
className="border-foreground/10 hover:bg-background dark:hover:bg-accent"
|
|
150
|
+
/>
|
|
151
|
+
}
|
|
152
|
+
className={(state) =>
|
|
153
|
+
cn(
|
|
154
|
+
'gap-2 data-[popup-open]:bg-accent data-[popup-open]:text-accent-foreground',
|
|
155
|
+
typeof props.className === 'function' ? props.className(state) : props.className,
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
>
|
|
159
|
+
{props.children ?? t('ui.open')}
|
|
160
|
+
<ChevronDown className="size-3.5 text-muted-foreground" />
|
|
161
|
+
</PopoverTrigger>
|
|
162
|
+
<PopoverContent className="flex flex-col">
|
|
163
|
+
{items.map((item) => (
|
|
164
|
+
<ItemRow
|
|
165
|
+
key={item.title}
|
|
166
|
+
{...('href' in item
|
|
167
|
+
? { href: item.href, target: '_blank', rel: 'noreferrer noopener' }
|
|
168
|
+
: { onClick: item.onClick })}
|
|
169
|
+
media={item.icon}
|
|
170
|
+
title={item.title}
|
|
171
|
+
trailing={
|
|
172
|
+
<ExternalLinkIcon className="size-3.5 text-muted-foreground" />
|
|
173
|
+
}
|
|
174
|
+
/>
|
|
175
|
+
))}
|
|
176
|
+
</PopoverContent>
|
|
177
|
+
</Popover>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function PageActions({
|
|
182
|
+
copied,
|
|
183
|
+
crowdinUrl,
|
|
184
|
+
title,
|
|
185
|
+
mdUrl,
|
|
186
|
+
githubUrl,
|
|
187
|
+
}: {
|
|
188
|
+
copied?: TranslationState;
|
|
189
|
+
crowdinUrl?: string;
|
|
190
|
+
title: string;
|
|
191
|
+
mdUrl?: string;
|
|
192
|
+
githubUrl?: string;
|
|
193
|
+
}) {
|
|
194
|
+
return (
|
|
195
|
+
<>
|
|
196
|
+
<CopyPageMarkdown title={title} />
|
|
197
|
+
<ViewOptionsPopover title={title} mdUrl={mdUrl} githubUrl={githubUrl} />
|
|
198
|
+
<UntranslatedActions copied={copied} crowdinUrl={crowdinUrl} />
|
|
199
|
+
</>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LoaderConfig, LoaderOutput, Page } from 'fumadocs-core/source';
|
|
2
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
3
|
+
import Link from 'fumadocs-core/link';
|
|
4
|
+
|
|
5
|
+
export function createRelativeLink<C extends LoaderConfig>(
|
|
6
|
+
source: LoaderOutput<C>,
|
|
7
|
+
page: Page | C['page'],
|
|
8
|
+
OverrideLink: (props: ComponentProps<'a'>) => ReactNode = Link,
|
|
9
|
+
): (props: ComponentProps<'a'>) => ReactNode | Promise<ReactNode> {
|
|
10
|
+
return async function RelativeLink({ href, ...props }) {
|
|
11
|
+
return (
|
|
12
|
+
<OverrideLink
|
|
13
|
+
href={href ? source.resolveHref(href, page) : href}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import {
|
|
3
|
+
SearchDialog as SearchDialogRoot,
|
|
4
|
+
SearchDialogClose,
|
|
5
|
+
SearchDialogContent,
|
|
6
|
+
SearchDialogFooter,
|
|
7
|
+
SearchDialogHeader,
|
|
8
|
+
SearchDialogIcon,
|
|
9
|
+
SearchDialogInput,
|
|
10
|
+
SearchDialogList,
|
|
11
|
+
SearchDialogOverlay,
|
|
12
|
+
SearchTagsList,
|
|
13
|
+
SearchTagsListItem,
|
|
14
|
+
type SharedProps,
|
|
15
|
+
} from '@terrafirmagreg-team/ui/components/blocks/search/dialog';
|
|
16
|
+
import {
|
|
17
|
+
useDocsSearch,
|
|
18
|
+
type SearchClient,
|
|
19
|
+
} from 'fumadocs-core/search/client';
|
|
20
|
+
import { staticClient } from 'fumadocs-core/search/client/orama-static';
|
|
21
|
+
import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
|
|
22
|
+
import { usePathname } from 'next/navigation';
|
|
23
|
+
import { useMemo, useState } from 'react';
|
|
24
|
+
import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
|
|
25
|
+
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
26
|
+
import { useSectionCatalog } from '@terrafirmagreg-team/kit/hooks/use-catalog';
|
|
27
|
+
import {
|
|
28
|
+
dumpSectionKeys,
|
|
29
|
+
isDumpSection,
|
|
30
|
+
parseDatasetRef,
|
|
31
|
+
parseSectionLocation,
|
|
32
|
+
} from '@terrafirmagreg-team/kit/lib/location';
|
|
33
|
+
import { searchHitFilter, mergeClients } from '@terrafirmagreg-team/kit/lib/search';
|
|
34
|
+
import { getSearchClientSources } from '@terrafirmagreg-team/kit/lib/search/sources';
|
|
35
|
+
|
|
36
|
+
type Scope = 'dataset' | 'section' | 'all';
|
|
37
|
+
|
|
38
|
+
export function SearchDialog(props: SharedProps & { api: string }) {
|
|
39
|
+
const locale = useResolvedLang();
|
|
40
|
+
const t = useTranslations();
|
|
41
|
+
const catalog = useSectionCatalog();
|
|
42
|
+
const pathname = usePathname();
|
|
43
|
+
const { section, dataset: locationDataset } =
|
|
44
|
+
parseSectionLocation(pathname);
|
|
45
|
+
const dataset = locationDataset
|
|
46
|
+
? parseDatasetRef(locationDataset).id
|
|
47
|
+
: undefined;
|
|
48
|
+
const scopeKey = `${section ?? ''}/${locationDataset ?? ''}`;
|
|
49
|
+
const defaultScope: Scope = dataset
|
|
50
|
+
? 'dataset'
|
|
51
|
+
: section
|
|
52
|
+
? 'section'
|
|
53
|
+
: 'all';
|
|
54
|
+
const [picked, setPicked] = useState<{ key: string; value: Scope }>();
|
|
55
|
+
const scope = picked?.key === scopeKey ? picked.value : defaultScope;
|
|
56
|
+
const setScope = (value: Scope) => setPicked({ key: scopeKey, value });
|
|
57
|
+
|
|
58
|
+
const tag = useMemo((): string | string[] | undefined => {
|
|
59
|
+
if (!section || scope === 'all') return undefined;
|
|
60
|
+
if (scope === 'dataset' && dataset) {
|
|
61
|
+
return `${section}:${dataset}`;
|
|
62
|
+
}
|
|
63
|
+
const ids = catalog[section].map(({ id }) => `${section}:${id}`);
|
|
64
|
+
return ids.length > 0 ? ids : `${section}:`;
|
|
65
|
+
}, [scope, section, dataset, catalog]);
|
|
66
|
+
|
|
67
|
+
const client = useMemo((): SearchClient => {
|
|
68
|
+
const content = staticClient({ locale, tag, from: props.api });
|
|
69
|
+
|
|
70
|
+
const scopedSections =
|
|
71
|
+
scope === 'all'
|
|
72
|
+
? dumpSectionKeys()
|
|
73
|
+
: section && isDumpSection(section)
|
|
74
|
+
? [section]
|
|
75
|
+
: [];
|
|
76
|
+
const refs = scopedSections.flatMap((sec) =>
|
|
77
|
+
(catalog[sec] ?? []).map(({ id }) =>
|
|
78
|
+
sec === section && id === dataset && locationDataset
|
|
79
|
+
? locationDataset
|
|
80
|
+
: id,
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
const extra = getSearchClientSources().map((make) =>
|
|
84
|
+
make({ refs, locale, tag }),
|
|
85
|
+
);
|
|
86
|
+
return mergeClients(content, ...extra);
|
|
87
|
+
}, [locale, tag, section, scope, dataset, locationDataset, catalog, props.api]);
|
|
88
|
+
const { search, setSearch, query } = useDocsSearch({ client });
|
|
89
|
+
const items = useMemo(() => {
|
|
90
|
+
const list = query.data !== 'empty' ? (query.data ?? []) : [];
|
|
91
|
+
const hitFilter =
|
|
92
|
+
section && typeof window !== 'undefined'
|
|
93
|
+
? searchHitFilter(
|
|
94
|
+
section,
|
|
95
|
+
new URLSearchParams(window.location.search),
|
|
96
|
+
)
|
|
97
|
+
: undefined;
|
|
98
|
+
return hitFilter ? list.filter(hitFilter) : list;
|
|
99
|
+
}, [query.data, section]);
|
|
100
|
+
const datasetMeta = catalog[section ?? 'docs']?.find(
|
|
101
|
+
({ id }) => id === dataset,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<SearchDialogRoot search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
|
|
106
|
+
<SearchDialogOverlay />
|
|
107
|
+
<SearchDialogContent>
|
|
108
|
+
<SearchDialogHeader>
|
|
109
|
+
<SearchDialogIcon />
|
|
110
|
+
<SearchDialogInput />
|
|
111
|
+
<SearchDialogClose />
|
|
112
|
+
</SearchDialogHeader>
|
|
113
|
+
{query.isLoading ? (
|
|
114
|
+
<div className="flex flex-col gap-1.5 p-3">
|
|
115
|
+
<Skeleton className="h-9 w-full" />
|
|
116
|
+
<Skeleton className="h-9 w-5/6" />
|
|
117
|
+
<Skeleton className="h-9 w-4/6" />
|
|
118
|
+
</div>
|
|
119
|
+
) : (
|
|
120
|
+
<SearchDialogList items={items.length ? items : null} />
|
|
121
|
+
)}
|
|
122
|
+
{section && (
|
|
123
|
+
<SearchDialogFooter>
|
|
124
|
+
<SearchTagsList
|
|
125
|
+
tag={scope}
|
|
126
|
+
onTagChange={(value) => setScope((value as Scope) ?? 'all')}
|
|
127
|
+
>
|
|
128
|
+
{dataset && (
|
|
129
|
+
<SearchTagsListItem value="dataset">
|
|
130
|
+
{datasetMeta?.title ?? dataset}
|
|
131
|
+
</SearchTagsListItem>
|
|
132
|
+
)}
|
|
133
|
+
<SearchTagsListItem value="section">
|
|
134
|
+
{t(`sections.${section}`)}
|
|
135
|
+
</SearchTagsListItem>
|
|
136
|
+
<SearchTagsListItem value="all">
|
|
137
|
+
{t('ui.search_everywhere')}
|
|
138
|
+
</SearchTagsListItem>
|
|
139
|
+
</SearchTagsList>
|
|
140
|
+
</SearchDialogFooter>
|
|
141
|
+
)}
|
|
142
|
+
</SearchDialogContent>
|
|
143
|
+
</SearchDialogRoot>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import {
|
|
3
|
+
AnchorProvider,
|
|
4
|
+
ScrollProvider,
|
|
5
|
+
type AnchorProviderProps,
|
|
6
|
+
} from 'fumadocs-core/toc';
|
|
7
|
+
import { useRef, type ComponentProps } from 'react';
|
|
8
|
+
import { TOCContext } from '@terrafirmagreg-team/kit/hooks/toc';
|
|
9
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
10
|
+
import { mergeRefs } from '@terrafirmagreg-team/ui/lib/utils/merge-refs';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
AnchorProvider,
|
|
14
|
+
ScrollProvider,
|
|
15
|
+
TOCItem,
|
|
16
|
+
useActiveAnchor,
|
|
17
|
+
useActiveAnchors,
|
|
18
|
+
useItems,
|
|
19
|
+
useTOC,
|
|
20
|
+
useTOCListener,
|
|
21
|
+
useTOCSelector,
|
|
22
|
+
} from 'fumadocs-core/toc';
|
|
23
|
+
export type {
|
|
24
|
+
AnchorProviderProps,
|
|
25
|
+
ScrollProviderProps,
|
|
26
|
+
TOCItemInfo,
|
|
27
|
+
TOCItemProps,
|
|
28
|
+
TOCItemType,
|
|
29
|
+
TableOfContents,
|
|
30
|
+
} from 'fumadocs-core/toc';
|
|
31
|
+
|
|
32
|
+
export type TOCProviderProps = AnchorProviderProps;
|
|
33
|
+
|
|
34
|
+
export function TOCProvider({ toc, children, ...props }: TOCProviderProps) {
|
|
35
|
+
return (
|
|
36
|
+
<TOCContext value={toc}>
|
|
37
|
+
<AnchorProvider toc={toc} {...props}>
|
|
38
|
+
{children}
|
|
39
|
+
</AnchorProvider>
|
|
40
|
+
</TOCContext>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function TOCScrollArea({
|
|
45
|
+
ref,
|
|
46
|
+
className,
|
|
47
|
+
...props
|
|
48
|
+
}: ComponentProps<'div'>) {
|
|
49
|
+
const viewRef = useRef<HTMLDivElement>(null);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
ref={mergeRefs(viewRef, ref)}
|
|
54
|
+
className={cn(
|
|
55
|
+
'relative min-h-0 text-sm overflow-x-clip overflow-y-auto overscroll-contain [scrollbar-width:none] mask-[linear-gradient(to_bottom,transparent,white_16px,white_calc(100%-16px),transparent)] py-3',
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
{...props}
|
|
59
|
+
>
|
|
60
|
+
<ScrollProvider containerRef={viewRef}>
|
|
61
|
+
{props.children}
|
|
62
|
+
</ScrollProvider>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef, type ComponentProps } from 'react';
|
|
3
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
4
|
+
import { mergeRefs } from '@terrafirmagreg-team/ui/lib/utils/merge-refs';
|
|
5
|
+
import * as Primitive from './base';
|
|
6
|
+
import { TOCEmpty } from './default';
|
|
7
|
+
|
|
8
|
+
export { TOCEmpty };
|
|
9
|
+
|
|
10
|
+
export type TOCItemsProps = ComponentProps<'div'>;
|
|
11
|
+
|
|
12
|
+
export function TOCItems({
|
|
13
|
+
ref,
|
|
14
|
+
className,
|
|
15
|
+
children,
|
|
16
|
+
...props
|
|
17
|
+
}: TOCItemsProps) {
|
|
18
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
const blockRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
const tocInfo = Primitive.useTOC();
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const container = containerRef.current;
|
|
24
|
+
const block = blockRef.current;
|
|
25
|
+
if (!container || !block) return;
|
|
26
|
+
|
|
27
|
+
const positions = new Map<string, [number, number]>();
|
|
28
|
+
let height = 0;
|
|
29
|
+
|
|
30
|
+
const update = (items: Primitive.TOCItemInfo[]) => {
|
|
31
|
+
let top = -1;
|
|
32
|
+
let bottom = -1;
|
|
33
|
+
for (const item of items) {
|
|
34
|
+
if (!item.active) continue;
|
|
35
|
+
const pos = positions.get(item.original.url);
|
|
36
|
+
if (!pos) continue;
|
|
37
|
+
if (top === -1) top = pos[0];
|
|
38
|
+
bottom = pos[1];
|
|
39
|
+
}
|
|
40
|
+
if (top === -1) return;
|
|
41
|
+
block.style.clipPath = `inset(${top}px 0 ${height - bottom}px round 8px)`;
|
|
42
|
+
if (!block.hasAttribute('data-ready'))
|
|
43
|
+
requestAnimationFrame(() => block.setAttribute('data-ready', ''));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const observer = new ResizeObserver(() => {
|
|
47
|
+
positions.clear();
|
|
48
|
+
for (const anchor of container.getElementsByTagName('a')) {
|
|
49
|
+
const href = anchor.getAttribute('href');
|
|
50
|
+
if (href)
|
|
51
|
+
positions.set(href, [
|
|
52
|
+
anchor.offsetTop,
|
|
53
|
+
anchor.offsetTop + anchor.offsetHeight,
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
height = container.clientHeight;
|
|
57
|
+
update(tocInfo.get());
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
observer.observe(container);
|
|
61
|
+
tocInfo.listen(update);
|
|
62
|
+
return () => {
|
|
63
|
+
observer.disconnect();
|
|
64
|
+
tocInfo.unlisten(update);
|
|
65
|
+
};
|
|
66
|
+
}, [tocInfo]);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
ref={mergeRefs(containerRef, ref)}
|
|
71
|
+
className={cn('relative flex flex-col', className)}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
<div
|
|
75
|
+
ref={blockRef}
|
|
76
|
+
className="absolute inset-0 bg-linear-to-r from-primary/10 [clip-path:inset(0_0_100%)] data-ready:transition-[clip-path] duration-300 ease-out"
|
|
77
|
+
/>
|
|
78
|
+
{children}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getItemOffset(depth: number): number {
|
|
84
|
+
if (depth <= 2) return 8;
|
|
85
|
+
if (depth === 3) return 20;
|
|
86
|
+
return 32;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function TOCItem({
|
|
90
|
+
item,
|
|
91
|
+
...props
|
|
92
|
+
}: Primitive.TOCItemProps & { item: Primitive.TOCItemType }) {
|
|
93
|
+
return (
|
|
94
|
+
<Primitive.TOCItem
|
|
95
|
+
href={item.url}
|
|
96
|
+
{...props}
|
|
97
|
+
className={cn(
|
|
98
|
+
'group prose relative py-1 pe-2 text-[0.8125rem]/5 scroll-m-4 text-muted-foreground hover:text-accent-foreground transition-colors wrap-anywhere data-[active=true]:text-primary',
|
|
99
|
+
props.className,
|
|
100
|
+
)}
|
|
101
|
+
style={{ paddingInlineStart: getItemOffset(item.depth), ...props.style }}
|
|
102
|
+
>
|
|
103
|
+
{item._step !== undefined && (
|
|
104
|
+
<span className="inline-flex size-4 me-1.5 items-center justify-center rounded-full align-text-bottom bg-muted text-muted-foreground font-mono font-medium text-[10px] transition-colors group-data-[active=true]:bg-primary group-data-[active=true]:text-primary-foreground">
|
|
105
|
+
{item._step}
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
{item.title}
|
|
109
|
+
</Primitive.TOCItem>
|
|
110
|
+
);
|
|
111
|
+
}
|