@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,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
+ }