@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,265 @@
1
+ 'use client';
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ type ComponentProps,
9
+ type CSSProperties,
10
+ type ReactNode,
11
+ } from 'react';
12
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
13
+ import { mergeRefs } from '@terrafirmagreg-team/ui/lib/utils/merge-refs';
14
+ import * as Primitive from './base';
15
+ import { useTOCItems } from '@terrafirmagreg-team/kit/hooks/toc';
16
+ import { TOCEmpty } from './default';
17
+
18
+ export { TOCEmpty };
19
+
20
+ interface SvgResult {
21
+ content: ReactNode;
22
+ width: number;
23
+ height: number;
24
+ positions: [number, number, number][];
25
+ }
26
+
27
+ export type TOCItemsProps = ComponentProps<'div'>;
28
+
29
+ export function TOCItems({
30
+ ref,
31
+ className,
32
+ children,
33
+ ...props
34
+ }: TOCItemsProps) {
35
+ const containerRef = useRef<HTMLDivElement>(null);
36
+ const items = useTOCItems();
37
+ const [svg, setSvg] = useState<SvgResult | null>(null);
38
+
39
+ const onPrint = useCallback(() => {
40
+ const container = containerRef.current;
41
+ if (!container || container.clientHeight === 0) return;
42
+ if (items.length === 0) {
43
+ setSvg(null);
44
+ return;
45
+ }
46
+
47
+ let w = 0;
48
+ let h = 0;
49
+ let d = '';
50
+ const positions: [number, number, number][] = [];
51
+ const output: ReactNode[] = [];
52
+
53
+ for (let i = 0; i < items.length; i++) {
54
+ const item = items[i];
55
+ const element = container.querySelector<HTMLElement>(`a[href="${item.url}"]`);
56
+ if (!element) continue;
57
+ const styles = getComputedStyle(element);
58
+ const x = getLineOffset(item.depth) + 0.5;
59
+ const top = element.offsetTop + parseFloat(styles.paddingTop);
60
+ const bottom =
61
+ element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom);
62
+
63
+ w = Math.max(x + 8, w);
64
+ h = Math.max(h, bottom);
65
+ if (i === 0) d += ` M${x} ${top} L${x} ${bottom}`;
66
+ else {
67
+ const [, upperBottom, upperX] = i > 0 ? positions[i - 1] : [0, 0, 0];
68
+ d += ` L ${upperX} ${upperBottom} ${x} ${top} L${x} ${bottom}`;
69
+ }
70
+
71
+ if (item._step !== undefined)
72
+ output.push(
73
+ <g key={i} transform={`translate(${x}, ${(top + bottom) / 2})`}>
74
+ <circle cx="0" cy="0" r="8" className="fill-primary" />
75
+ <text
76
+ x="0"
77
+ y="0"
78
+ textAnchor="middle"
79
+ alignmentBaseline="central"
80
+ dominantBaseline="middle"
81
+ className="fill-primary-foreground font-medium text-xs leading-none font-mono rtl:-scale-x-100"
82
+ >
83
+ {item._step}
84
+ </text>
85
+ </g>,
86
+ );
87
+ positions.push([top, bottom, x]);
88
+ }
89
+
90
+ output.unshift(
91
+ <path
92
+ key="path"
93
+ d={d}
94
+ className="stroke-primary"
95
+ strokeWidth="1"
96
+ fill="none"
97
+ />,
98
+ );
99
+
100
+ setSvg({ content: output, width: w, height: h, positions });
101
+ }, [items]);
102
+
103
+ useEffect(() => {
104
+ const container = containerRef.current;
105
+ if (!container) return;
106
+ const observer = new ResizeObserver(onPrint);
107
+ observer.observe(container);
108
+ onPrint();
109
+ return () => {
110
+ observer.unobserve(container);
111
+ };
112
+ }, [onPrint]);
113
+
114
+ return (
115
+ <div
116
+ ref={mergeRefs(containerRef, ref)}
117
+ className={cn('relative flex flex-col', className)}
118
+ {...props}
119
+ >
120
+ {svg && <ThumbTrack computed={svg} />}
121
+ {children}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ function ThumbTrack({ computed }: { computed: SvgResult }) {
127
+ const ref = useRef<HTMLDivElement>(null);
128
+ const tocInfo = Primitive.useTOC();
129
+
130
+ function calculate(items: Primitive.TOCItemInfo[]): CSSProperties {
131
+ const out: Record<string, string> = {};
132
+ const startIdx = items.findIndex((item) => item.active);
133
+ if (startIdx === -1) return out;
134
+ const endIdx = items.findLastIndex((item) => item.active);
135
+ out['--track-top'] = `${computed.positions[startIdx][0]}px`;
136
+ out['--track-bottom'] = `${computed.positions[endIdx][1]}px`;
137
+ return out as CSSProperties;
138
+ }
139
+
140
+ Primitive.useTOCListener((items) => {
141
+ const element = ref.current;
142
+ if (!element) return;
143
+ for (const [k, v] of Object.entries(calculate(items)))
144
+ element.style.setProperty(k, v);
145
+ });
146
+
147
+ return (
148
+ <div
149
+ ref={ref}
150
+ className="absolute top-0 inset-s-0 origin-center rtl:-scale-x-100"
151
+ style={{
152
+ width: computed.width,
153
+ height: computed.height,
154
+ ...calculate(tocInfo.get()),
155
+ }}
156
+ >
157
+ <svg
158
+ xmlns="http://www.w3.org/2000/svg"
159
+ viewBox={`0 0 ${computed.width} ${computed.height}`}
160
+ className="absolute transition-[clip-path]"
161
+ style={{
162
+ width: computed.width,
163
+ height: computed.height,
164
+ clipPath: `polygon(0 var(--track-top,0), 100% var(--track-top,0), 100% var(--track-bottom,0), 0 var(--track-bottom,0))`,
165
+ }}
166
+ >
167
+ {computed.content}
168
+ </svg>
169
+ </div>
170
+ );
171
+ }
172
+
173
+ const BASE = 8;
174
+
175
+ function getItemOffset(depth: number): number {
176
+ if (depth <= 2) return 20;
177
+ if (depth === 3) return 32;
178
+ return 44;
179
+ }
180
+
181
+ function getLineOffset(depth: number): number {
182
+ if (depth <= 2) return BASE;
183
+ if (depth === 3) return 20;
184
+ return 32;
185
+ }
186
+
187
+ export function TOCItem({
188
+ item,
189
+ ...props
190
+ }: Primitive.TOCItemProps & { item: Primitive.TOCItemType }) {
191
+ const items = useTOCItems();
192
+ const { isFirst, isLast, svg } = useMemo(() => {
193
+ const index = items.indexOf(item);
194
+ const isFirst = index === 0;
195
+ const isLast = index === items.length - 1;
196
+ const l1 = getLineOffset(item.depth);
197
+ const l0 = isFirst ? l1 : getLineOffset(items[index - 1].depth);
198
+ const l2 = isLast ? l1 : getLineOffset(items[index + 1].depth);
199
+
200
+ return {
201
+ isFirst,
202
+ isLast,
203
+ svg: (
204
+ <svg
205
+ xmlns="http://www.w3.org/2000/svg"
206
+ className={cn(
207
+ 'absolute -top-1.5 inset-s-0 bottom-0 h-[calc(100%+--spacing(1.5))] -z-1 rtl:-scale-x-100',
208
+ l1 !== l2 && 'h-full bottom-1.5',
209
+ )}
210
+ style={{ width: Math.max(l0, l1) + 9 }}
211
+ >
212
+ {l0 !== l1 && (
213
+ <path
214
+ d={`M ${l0 + 0.5} 0 L ${l0 + 0.5} 0 ${l1 + 0.5} 12`}
215
+ stroke="black"
216
+ strokeWidth="1"
217
+ fill="none"
218
+ className="stroke-foreground/10"
219
+ />
220
+ )}
221
+ <line
222
+ x1={l1 + 0.5}
223
+ y1={l0 === l1 ? '6' : '12'}
224
+ x2={l1 + 0.5}
225
+ y2="100%"
226
+ strokeWidth="1"
227
+ className="stroke-foreground/10"
228
+ />
229
+ {item._step !== undefined && (
230
+ <g transform={`translate(${l1 + 0.5}, ${l1 === l2 ? '3' : '6'})`}>
231
+ <circle cx="0" cy="50%" r="8" className="fill-muted" />
232
+ <text
233
+ x="0"
234
+ y="50%"
235
+ textAnchor="middle"
236
+ alignmentBaseline="central"
237
+ dominantBaseline="middle"
238
+ className="fill-muted-foreground font-medium text-xs leading-none font-mono rtl:-scale-x-100"
239
+ >
240
+ {item._step}
241
+ </text>
242
+ </g>
243
+ )}
244
+ </svg>
245
+ ),
246
+ };
247
+ }, [items, item]);
248
+
249
+ return (
250
+ <Primitive.TOCItem
251
+ href={item.url}
252
+ {...props}
253
+ className={cn(
254
+ 'prose prose-sm relative py-1.5 scroll-m-4 text-muted-foreground hover:text-accent-foreground transition-colors wrap-anywhere data-[active=true]:text-primary',
255
+ isFirst && 'pt-0',
256
+ isLast && 'pb-0',
257
+ props.className,
258
+ )}
259
+ style={{ paddingInlineStart: getItemOffset(item.depth), ...props.style }}
260
+ >
261
+ {svg}
262
+ {item.title}
263
+ </Primitive.TOCItem>
264
+ );
265
+ }
@@ -0,0 +1,333 @@
1
+ 'use client';
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ type ComponentProps,
9
+ type CSSProperties,
10
+ type ReactNode,
11
+ } from 'react';
12
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
13
+ import { mergeRefs } from '@terrafirmagreg-team/ui/lib/utils/merge-refs';
14
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
15
+ import * as Primitive from './base';
16
+ import { useTOCItems } from '@terrafirmagreg-team/kit/hooks/toc';
17
+
18
+ interface SvgResult {
19
+ content: ReactNode;
20
+ width: number;
21
+ height: number;
22
+ d: string;
23
+ itemLineLengths: [number, number][];
24
+ positions: [number, number, number][];
25
+ }
26
+
27
+ export interface TOCItemsProps extends ComponentProps<'div'> {
28
+ thumbBox?: boolean;
29
+ }
30
+
31
+ export function TOCItems({
32
+ ref,
33
+ className,
34
+ thumbBox = true,
35
+ children,
36
+ ...props
37
+ }: TOCItemsProps) {
38
+ const containerRef = useRef<HTMLDivElement>(null);
39
+ const items = useTOCItems();
40
+ const [svg, setSvg] = useState<SvgResult | null>(null);
41
+
42
+ const onPrint = useCallback(() => {
43
+ const container = containerRef.current;
44
+ if (!container || container.clientHeight === 0) return;
45
+ if (items.length === 0) {
46
+ setSvg(null);
47
+ return;
48
+ }
49
+
50
+ let w = 0;
51
+ let h = 0;
52
+ let d = '';
53
+ const positions: [number, number, number][] = [];
54
+ const output: ReactNode[] = [];
55
+
56
+ for (let i = 0; i < items.length; i++) {
57
+ const item = items[i];
58
+ const element = container.querySelector<HTMLElement>(`a[href="${item.url}"]`);
59
+ if (!element) continue;
60
+ const styles = getComputedStyle(element);
61
+ const x = getLineOffset(item.depth) + 0.5;
62
+ const top = element.offsetTop + parseFloat(styles.paddingTop);
63
+ const bottom =
64
+ element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom);
65
+
66
+ w = Math.max(x + 8, w);
67
+ h = Math.max(h, bottom);
68
+ if (i === 0) d += ` M${x} ${top} L${x} ${bottom}`;
69
+ else {
70
+ const [, upperBottom, upperX] = i > 0 ? positions[i - 1] : [0, 0, 0];
71
+ d += ` C ${upperX} ${top - 4} ${x} ${upperBottom + 4} ${x} ${top} L${x} ${bottom}`;
72
+ }
73
+
74
+ if (item._step !== undefined)
75
+ output.push(
76
+ <g key={i} transform={`translate(${x}, ${(top + bottom) / 2})`}>
77
+ <circle cx="0" cy="0" r="8" className="fill-primary" />
78
+ <text
79
+ x="0"
80
+ y="0"
81
+ textAnchor="middle"
82
+ alignmentBaseline="central"
83
+ dominantBaseline="middle"
84
+ className="fill-primary-foreground font-medium text-xs leading-none font-mono rtl:-scale-x-100"
85
+ >
86
+ {item._step}
87
+ </text>
88
+ </g>,
89
+ );
90
+ positions.push([top, bottom, x]);
91
+ }
92
+
93
+ output.unshift(
94
+ <path
95
+ key="path"
96
+ d={d}
97
+ className="stroke-primary"
98
+ strokeWidth="1"
99
+ fill="none"
100
+ />,
101
+ );
102
+
103
+ const itemLineLengths: [number, number][] = [];
104
+ if (thumbBox) {
105
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
106
+ path.setAttribute('d', d);
107
+ const n = path.getTotalLength();
108
+ for (let i = 0; i < positions.length; i++) {
109
+ const [top, bottom] = positions[i];
110
+ let l =
111
+ i > 0 ? itemLineLengths[i - 1][1] + (top - positions[i - 1][1]) : top;
112
+ while (l < n && path.getPointAtLength(l).y < top) l++;
113
+ itemLineLengths.push([l, l + bottom - top]);
114
+ }
115
+ }
116
+
117
+ setSvg({ content: output, width: w, height: h, d, itemLineLengths, positions });
118
+ }, [items, thumbBox]);
119
+
120
+ useEffect(() => {
121
+ const container = containerRef.current;
122
+ if (!container) return;
123
+ const observer = new ResizeObserver(onPrint);
124
+ observer.observe(container);
125
+ onPrint();
126
+ return () => {
127
+ observer.unobserve(container);
128
+ };
129
+ }, [onPrint]);
130
+
131
+ return (
132
+ <div
133
+ ref={mergeRefs(containerRef, ref)}
134
+ className={cn('relative flex flex-col', className)}
135
+ {...props}
136
+ >
137
+ {svg && <ThumbTrack computed={svg} thumbBox={thumbBox} />}
138
+ {children}
139
+ </div>
140
+ );
141
+ }
142
+
143
+ export function TOCEmpty() {
144
+ const t = useTranslations();
145
+ return (
146
+ <div className="rounded-lg border bg-card p-3 text-xs text-muted-foreground">
147
+ {t('ui.no_headings')}
148
+ </div>
149
+ );
150
+ }
151
+
152
+ function ThumbTrack({
153
+ computed,
154
+ thumbBox,
155
+ }: {
156
+ computed: SvgResult;
157
+ thumbBox: boolean;
158
+ }) {
159
+ const ref = useRef<HTMLDivElement>(null);
160
+ const previousRef = useRef<{
161
+ startIdx: number;
162
+ endIdx: number;
163
+ isUp: boolean;
164
+ } | null>(null);
165
+ const tocInfo = Primitive.useTOC();
166
+
167
+ function calculate(
168
+ items: Primitive.TOCItemInfo[],
169
+ previous: { startIdx: number; endIdx: number; isUp: boolean } | null,
170
+ ): [CSSProperties, typeof previous] {
171
+ const out: Record<string, string> = {};
172
+ const startIdx = items.findIndex((item) => item.active);
173
+ if (startIdx === -1) return [out, previous];
174
+ const endIdx = items.findLastIndex((item) => item.active);
175
+
176
+ out['--track-top'] = `${computed.positions[startIdx][0]}px`;
177
+ out['--track-bottom'] = `${computed.positions[endIdx][1]}px`;
178
+
179
+ if (thumbBox) {
180
+ let isUp = false;
181
+ if (previous) {
182
+ isUp =
183
+ previous.startIdx > startIdx ||
184
+ previous.endIdx > endIdx ||
185
+ (previous.startIdx === startIdx &&
186
+ previous.endIdx === endIdx &&
187
+ previous.isUp);
188
+ }
189
+ const next = { startIdx, endIdx, isUp };
190
+ out['--offset-distance'] = isUp
191
+ ? `${computed.itemLineLengths[startIdx][0]}px`
192
+ : `${computed.itemLineLengths[endIdx][1]}px`;
193
+ out['--opacity'] =
194
+ items[isUp ? startIdx : endIdx].original._step !== undefined ? '0' : '1';
195
+ return [out as CSSProperties, next];
196
+ }
197
+ return [out as CSSProperties, previous];
198
+ }
199
+
200
+ Primitive.useTOCListener((items) => {
201
+ const element = ref.current;
202
+ if (!element) return;
203
+ const [style, next] = calculate(items, previousRef.current);
204
+ previousRef.current = next;
205
+ for (const [k, v] of Object.entries(style))
206
+ element.style.setProperty(k, v);
207
+ });
208
+
209
+ return (
210
+ <div
211
+ ref={ref}
212
+ className="absolute top-0 inset-s-0 origin-center rtl:-scale-x-100"
213
+ style={{
214
+ width: computed.width,
215
+ height: computed.height,
216
+ ...calculate(tocInfo.get(), null)[0],
217
+ }}
218
+ >
219
+ <svg
220
+ xmlns="http://www.w3.org/2000/svg"
221
+ viewBox={`0 0 ${computed.width} ${computed.height}`}
222
+ className="absolute transition-[clip-path]"
223
+ style={{
224
+ width: computed.width,
225
+ height: computed.height,
226
+ clipPath: `polygon(0 var(--track-top,0), 100% var(--track-top,0), 100% var(--track-bottom,0), 0 var(--track-bottom,0))`,
227
+ }}
228
+ >
229
+ {computed.content}
230
+ </svg>
231
+ {thumbBox && (
232
+ <div
233
+ className="absolute left-0 size-1 bg-primary rounded-full [offset-distance:var(--offset-distance,0)] opacity-(--opacity,0) transition-[opacity,offset-distance]"
234
+ style={{ offsetPath: `path("${computed.d}")` }}
235
+ />
236
+ )}
237
+ </div>
238
+ );
239
+ }
240
+
241
+ const BASE = 8;
242
+
243
+ function getItemOffset(depth: number): number {
244
+ if (depth <= 2) return 20;
245
+ if (depth === 3) return 32;
246
+ return 44;
247
+ }
248
+
249
+ function getLineOffset(depth: number): number {
250
+ if (depth <= 2) return BASE;
251
+ if (depth === 3) return 16;
252
+ return 24;
253
+ }
254
+
255
+ export function TOCItem({
256
+ item,
257
+ ...props
258
+ }: Primitive.TOCItemProps & { item: Primitive.TOCItemType }) {
259
+ const items = useTOCItems();
260
+ const { isFirst, isLast, svg } = useMemo(() => {
261
+ const index = items.indexOf(item);
262
+ const isFirst = index === 0;
263
+ const isLast = index === items.length - 1;
264
+ const l1 = getLineOffset(item.depth);
265
+ const l0 = isFirst ? l1 : getLineOffset(items[index - 1].depth);
266
+ const l2 = isLast ? l1 : getLineOffset(items[index + 1].depth);
267
+
268
+ return {
269
+ isFirst,
270
+ isLast,
271
+ svg: (
272
+ <svg
273
+ xmlns="http://www.w3.org/2000/svg"
274
+ className={cn(
275
+ 'absolute -top-1.5 inset-s-0 bottom-0 h-[calc(100%+--spacing(1.5))] -z-1 rtl:-scale-x-100',
276
+ l1 !== l2 && 'h-full bottom-1.5',
277
+ )}
278
+ style={{ width: Math.max(l0, l1) + 9 }}
279
+ >
280
+ {l0 !== l1 && (
281
+ <path
282
+ d={`M ${l0 + 0.5} 0 C ${l0 + 0.5} 8 ${l1 + 0.5} 4 ${l1 + 0.5} 12`}
283
+ stroke="black"
284
+ strokeWidth="1"
285
+ fill="none"
286
+ className="stroke-foreground/10"
287
+ />
288
+ )}
289
+ <line
290
+ x1={l1 + 0.5}
291
+ y1={l0 === l1 ? '6' : '12'}
292
+ x2={l1 + 0.5}
293
+ y2="100%"
294
+ strokeWidth="1"
295
+ className="stroke-foreground/10"
296
+ />
297
+ {item._step !== undefined && (
298
+ <g transform={`translate(${l1 + 0.5}, ${l1 === l2 ? '3' : '6'})`}>
299
+ <circle cx="0" cy="50%" r="8" className="fill-muted" />
300
+ <text
301
+ x="0"
302
+ y="50%"
303
+ textAnchor="middle"
304
+ alignmentBaseline="central"
305
+ dominantBaseline="middle"
306
+ className="fill-muted-foreground font-medium text-xs leading-none font-mono rtl:-scale-x-100"
307
+ >
308
+ {item._step}
309
+ </text>
310
+ </g>
311
+ )}
312
+ </svg>
313
+ ),
314
+ };
315
+ }, [items, item]);
316
+
317
+ return (
318
+ <Primitive.TOCItem
319
+ href={item.url}
320
+ {...props}
321
+ className={cn(
322
+ 'prose prose-sm relative py-1.5 scroll-m-4 text-muted-foreground hover:text-accent-foreground transition-colors wrap-anywhere data-[active=true]:text-primary',
323
+ isFirst && 'pt-0',
324
+ isLast && 'pb-0',
325
+ props.className,
326
+ )}
327
+ style={{ paddingInlineStart: getItemOffset(item.depth), ...props.style }}
328
+ >
329
+ {svg}
330
+ {item.title}
331
+ </Primitive.TOCItem>
332
+ );
333
+ }