@windrun-huaiin/third-ui 14.3.1 → 14.4.1

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 (54) hide show
  1. package/dist/fuma/fuma-page-genarator.js +3 -4
  2. package/dist/fuma/fuma-page-genarator.mjs +3 -4
  3. package/dist/fuma/mdx/index.d.ts +1 -0
  4. package/dist/fuma/mdx/index.js +5 -0
  5. package/dist/fuma/mdx/index.mjs +1 -0
  6. package/dist/fuma/mdx/toc-clerk-portable.d.ts +23 -0
  7. package/dist/fuma/mdx/toc-clerk-portable.js +358 -0
  8. package/dist/fuma/mdx/toc-clerk-portable.mjs +334 -0
  9. package/dist/main/delayed-img.js +1 -1
  10. package/dist/main/delayed-img.mjs +1 -1
  11. package/dist/main/gallery/gallery-mobile-swiper.js +2 -2
  12. package/dist/main/gallery/gallery-mobile-swiper.mjs +2 -2
  13. package/dist/main/snake-loading-frame.js +3 -3
  14. package/dist/main/snake-loading-frame.mjs +3 -3
  15. package/dist/node_modules/.pnpm/{katex@0.16.38 → katex@0.16.43}/node_modules/katex/dist/katex.js +50 -16
  16. package/dist/node_modules/.pnpm/{katex@0.16.38 → katex@0.16.43}/node_modules/katex/dist/katex.mjs +50 -16
  17. package/dist/node_modules/.pnpm/mermaid@11.12.1/node_modules/mermaid/dist/chunks/mermaid.core/chunk-ABZYJK2D.js +1 -1
  18. package/dist/node_modules/.pnpm/mermaid@11.12.1/node_modules/mermaid/dist/chunks/mermaid.core/chunk-ABZYJK2D.mjs +1 -1
  19. package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/swiper-core.js +2 -1
  20. package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/swiper-core.mjs +2 -1
  21. package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/swiper-react.js +2 -2
  22. package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/swiper-react.mjs +2 -2
  23. package/package.json +1 -1
  24. package/src/fuma/fuma-page-genarator.tsx +11 -5
  25. package/src/fuma/mdx/index.ts +1 -0
  26. package/src/fuma/mdx/toc-clerk-portable.tsx +684 -0
  27. package/src/main/delayed-img.tsx +0 -1
  28. package/src/main/snake-loading-frame.tsx +3 -3
  29. package/dist/node_modules/.pnpm/framer-motion@12.38.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/framer-motion/dist/es/utils/use-animation-frame.js +0 -24
  30. package/dist/node_modules/.pnpm/framer-motion@12.38.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/framer-motion/dist/es/utils/use-animation-frame.mjs +0 -22
  31. package/dist/node_modules/.pnpm/framer-motion@12.38.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/framer-motion/dist/es/value/use-combine-values.js +0 -40
  32. package/dist/node_modules/.pnpm/framer-motion@12.38.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/framer-motion/dist/es/value/use-combine-values.mjs +0 -38
  33. package/dist/node_modules/.pnpm/framer-motion@12.38.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/framer-motion/dist/es/value/use-computed.js +0 -22
  34. package/dist/node_modules/.pnpm/framer-motion@12.38.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/framer-motion/dist/es/value/use-computed.mjs +0 -20
  35. package/dist/node_modules/.pnpm/framer-motion@12.38.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/framer-motion/dist/es/value/use-motion-value.js +0 -41
  36. package/dist/node_modules/.pnpm/framer-motion@12.38.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/framer-motion/dist/es/value/use-motion-value.mjs +0 -39
  37. package/dist/node_modules/.pnpm/framer-motion@12.38.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/framer-motion/dist/es/value/use-transform.js +0 -50
  38. package/dist/node_modules/.pnpm/framer-motion@12.38.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/framer-motion/dist/es/value/use-transform.mjs +0 -48
  39. package/dist/node_modules/.pnpm/motion-dom@12.38.0/node_modules/motion-dom/dist/es/utils/transform.js +0 -16
  40. package/dist/node_modules/.pnpm/motion-dom@12.38.0/node_modules/motion-dom/dist/es/utils/transform.mjs +0 -14
  41. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/modules/pagination.js +0 -0
  42. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/modules/pagination.mjs +0 -0
  43. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/classes-to-selector.js +0 -0
  44. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/classes-to-selector.mjs +0 -0
  45. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/create-element-if-not-defined.js +0 -0
  46. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/create-element-if-not-defined.mjs +0 -0
  47. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/ssr-window.esm.js +0 -0
  48. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/ssr-window.esm.mjs +0 -0
  49. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/update-on-virtual-data.js +0 -0
  50. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/update-on-virtual-data.mjs +0 -0
  51. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/update-swiper.js +0 -0
  52. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/update-swiper.mjs +0 -0
  53. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/utils.js +0 -0
  54. /package/dist/node_modules/.pnpm/{swiper@12.1.2 → swiper@12.1.3}/node_modules/swiper/shared/utils.mjs +0 -0
@@ -0,0 +1,684 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * References most of its code and SVG animation design from
5
+ * https://github.com/fuma-nama/fumadocs/blob/dev/packages/radix-ui/src/components/toc/clerk.tsx
6
+ */
7
+ import * as Primitive from 'fumadocs-core/toc';
8
+ import {
9
+ PageTOC,
10
+ PageTOCPopover,
11
+ PageTOCPopoverContent,
12
+ PageTOCPopoverTrigger,
13
+ PageTOCTitle,
14
+ } from 'fumadocs-ui/layouts/docs/page';
15
+ import {
16
+ type ComponentProps,
17
+ type ReactNode,
18
+ useEffect,
19
+ useLayoutEffect,
20
+ useMemo,
21
+ useRef,
22
+ useState,
23
+ } from 'react';
24
+ import {
25
+ themeIconColor,
26
+ themeSvgIconColor,
27
+ } from '@windrun-huaiin/base-ui/lib';
28
+
29
+ type TOCItemType = Primitive.TOCItemType;
30
+
31
+ type PortableClerkTOCProps = {
32
+ toc: TOCItemType[];
33
+ header?: ReactNode;
34
+ footer?: ReactNode;
35
+ title?: ReactNode;
36
+ emptyLabel?: ReactNode;
37
+ className?: string;
38
+ };
39
+
40
+ type ClerkItemMeta = {
41
+ item: TOCItemType;
42
+ resolvedContent: ReactNode;
43
+ stepNumber: string | null;
44
+ itemPadding: number;
45
+ lineOffset: number;
46
+ };
47
+
48
+ type ClerkItemMeasure = {
49
+ url: string;
50
+ y: number;
51
+ x: number;
52
+ stepNumber: string | null;
53
+ };
54
+
55
+ // Base stroke width for both the inactive rail and the active highlight path.
56
+ const CLERK_PATH_STROKE_WIDTH = 1;
57
+ // Radius of the moving endpoint dot that marks the latest active heading.
58
+ const CLERK_ACTIVE_DOT_RADIUS = 2;
59
+ // Max vertical space reserved for a turn inside the gap between two headings.
60
+ const CLERK_TURN_CURVE_HEIGHT = 12;
61
+ // Multiplier for bezier control points; higher values make the turn rounder.
62
+ const CLERK_TURN_CONTROL_FACTOR = 0.68;
63
+ // Safety margin that keeps turns away from the heading rows themselves.
64
+ const CLERK_TURN_GAP_MARGIN = 7;
65
+ // Shared duration for active rail fade transitions and endpoint dot movement.
66
+ const CLERK_ACTIVE_ANIMATION_DURATION_MS = 300;
67
+ // Easing curve for the active rail and dot; tuned for a slightly delayed, softer motion.
68
+ const CLERK_ACTIVE_ANIMATION_EASING = 'cubic-bezier(0.22, 1, 0.36, 1)';
69
+ // Horizontal gap between the path centerline and the heading text start.
70
+ const CLERK_TEXT_GAP_FROM_PATH = 12;
71
+ // Radius of numbered step badges rendered on top of the path centerline.
72
+ const CLERK_STEP_BADGE_RADIUS = 7;
73
+
74
+ export function PortableClerkTOC({
75
+ toc,
76
+ header,
77
+ footer,
78
+ title,
79
+ emptyLabel = 'No headings',
80
+ className,
81
+ }: PortableClerkTOCProps) {
82
+ return (
83
+ <PageTOC className={className}>
84
+ {header}
85
+ {title ?? <PageTOCTitle />}
86
+ <PortableClerkTOCScrollArea>
87
+ <PortableClerkTOCItems toc={toc} emptyLabel={emptyLabel} />
88
+ </PortableClerkTOCScrollArea>
89
+ {footer}
90
+ </PageTOC>
91
+ );
92
+ }
93
+
94
+ export function PortableClerkTOCPopover({
95
+ toc,
96
+ header,
97
+ footer,
98
+ emptyLabel = 'No headings',
99
+ }: Omit<PortableClerkTOCProps, 'title' | 'className'>) {
100
+ return (
101
+ <PageTOCPopover>
102
+ <PageTOCPopoverTrigger />
103
+ <PageTOCPopoverContent>
104
+ {header}
105
+ <PortableClerkTOCScrollArea>
106
+ <PortableClerkTOCItems toc={toc} emptyLabel={emptyLabel} />
107
+ </PortableClerkTOCScrollArea>
108
+ {footer}
109
+ </PageTOCPopoverContent>
110
+ </PageTOCPopover>
111
+ );
112
+ }
113
+
114
+ export function PortableClerkTOCScrollArea({
115
+ ref,
116
+ className,
117
+ ...props
118
+ }: ComponentProps<'div'>) {
119
+ const viewRef = useRef<HTMLDivElement>(null);
120
+
121
+ return (
122
+ <div
123
+ ref={mergeRefs(viewRef, ref)}
124
+ className={cn(
125
+ 'relative min-h-0 text-sm ms-px overflow-auto [scrollbar-width:none] mask-[linear-gradient(to_bottom,transparent,white_16px,white_calc(100%-16px),transparent)] py-3',
126
+ className,
127
+ )}
128
+ {...props}
129
+ >
130
+ <Primitive.ScrollProvider containerRef={viewRef}>
131
+ {props.children}
132
+ </Primitive.ScrollProvider>
133
+ </div>
134
+ );
135
+ }
136
+
137
+ export function PortableClerkTOCItems({
138
+ toc,
139
+ emptyLabel = 'No headings',
140
+ ref,
141
+ className,
142
+ ...props
143
+ }: ComponentProps<'div'> & { toc: TOCItemType[]; emptyLabel?: ReactNode }) {
144
+ const containerRef = useRef<HTMLDivElement>(null);
145
+ const activeAnchors = Primitive.useActiveAnchors();
146
+ const itemRefs = useRef<(HTMLAnchorElement | null)[]>([]);
147
+ const contentRefs = useRef<(HTMLSpanElement | null)[]>([]);
148
+ const [layout, setLayout] = useState<{
149
+ height: number;
150
+ items: ClerkItemMeasure[];
151
+ }>({
152
+ height: 0,
153
+ items: [],
154
+ });
155
+
156
+ const metas = useMemo(() => toc.map(resolveClerkItem), [toc]);
157
+ const outlinePath = useMemo(
158
+ () => buildOutlinePath(layout.items),
159
+ [layout.items],
160
+ );
161
+ const activeItems = useMemo(
162
+ () => getActiveItems(layout.items, activeAnchors),
163
+ [activeAnchors, layout.items],
164
+ );
165
+ const activePath = useMemo(() => buildOutlinePath(activeItems), [activeItems]);
166
+ const activeEndpoint = useMemo(
167
+ () => getActiveEndpoint(activeItems),
168
+ [activeItems],
169
+ );
170
+
171
+ useLayoutEffect(() => {
172
+ const container = containerRef.current;
173
+ if (!container) return;
174
+
175
+ let frame = 0;
176
+ const updateLayout = () => {
177
+ frame = 0;
178
+ const nextItems = metas.flatMap((meta, index) => {
179
+ const element = itemRefs.current[index];
180
+ const content = contentRefs.current[index];
181
+ if (!element || !content) return [];
182
+
183
+ const y = measureItemLineY(element, content);
184
+
185
+ return [
186
+ {
187
+ url: meta.item.url,
188
+ y,
189
+ x: meta.lineOffset,
190
+ stepNumber: meta.stepNumber,
191
+ },
192
+ ];
193
+ });
194
+
195
+ setLayout((prev) => {
196
+ const next = {
197
+ height: container.clientHeight,
198
+ items: nextItems,
199
+ };
200
+
201
+ if (isSameLayout(prev, next)) return prev;
202
+ return next;
203
+ });
204
+ };
205
+
206
+ const queueUpdate = () => {
207
+ if (frame !== 0) cancelAnimationFrame(frame);
208
+ frame = requestAnimationFrame(updateLayout);
209
+ };
210
+
211
+ queueUpdate();
212
+
213
+ const observer = new ResizeObserver(queueUpdate);
214
+ observer.observe(container);
215
+
216
+ for (const element of itemRefs.current) {
217
+ if (element) observer.observe(element);
218
+ }
219
+
220
+ return () => {
221
+ if (frame !== 0) cancelAnimationFrame(frame);
222
+ observer.disconnect();
223
+ };
224
+ }, [metas]);
225
+
226
+ if (toc.length === 0) {
227
+ return (
228
+ <div className="rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground">
229
+ {emptyLabel}
230
+ </div>
231
+ );
232
+ }
233
+
234
+ return (
235
+ <div
236
+ ref={mergeRefs(containerRef, ref)}
237
+ className={cn('relative flex flex-col', className)}
238
+ {...props}
239
+ >
240
+ <ClerkOutline
241
+ path={outlinePath}
242
+ items={layout.items}
243
+ activePath={activePath}
244
+ activeAnchors={activeAnchors}
245
+ activeEndpoint={activeEndpoint}
246
+ />
247
+ {metas.map((meta, i) => (
248
+ <PortableClerkTOCItem
249
+ key={meta.item.url}
250
+ item={meta.item}
251
+ isActive={activeAnchors.includes(meta.item.url.slice(1))}
252
+ resolvedContent={meta.resolvedContent}
253
+ itemPadding={meta.itemPadding}
254
+ contentRef={(node: HTMLSpanElement | null) => {
255
+ contentRefs.current[i] = node;
256
+ }}
257
+ ref={(node: HTMLAnchorElement | null) => {
258
+ itemRefs.current[i] = node;
259
+ }}
260
+ />
261
+ ))}
262
+ </div>
263
+ );
264
+ }
265
+
266
+ function PortableClerkTOCItem({
267
+ item,
268
+ isActive,
269
+ resolvedContent,
270
+ itemPadding,
271
+ contentRef,
272
+ ref,
273
+ }: {
274
+ item: TOCItemType;
275
+ isActive: boolean;
276
+ resolvedContent: ReactNode;
277
+ itemPadding: number;
278
+ contentRef?: ((node: HTMLSpanElement | null) => void) | null;
279
+ ref?: ((node: HTMLAnchorElement | null) => void) | null;
280
+ }) {
281
+ return (
282
+ <Primitive.TOCItem
283
+ ref={ref}
284
+ href={item.url}
285
+ data-clerk-item=""
286
+ style={{
287
+ paddingInlineStart: itemPadding,
288
+ }}
289
+ className={cn(
290
+ 'prose group relative py-1.5 text-sm transition-colors wrap-anywhere first:pt-0 last:pb-0 hover:text-fd-accent-foreground',
291
+ isActive ? themeIconColor : 'text-fd-muted-foreground',
292
+ )}
293
+ >
294
+ <span ref={contentRef} className="relative z-10">
295
+ {resolvedContent}
296
+ </span>
297
+ </Primitive.TOCItem>
298
+ );
299
+ }
300
+
301
+ function ClerkOutline({
302
+ path,
303
+ items,
304
+ activePath,
305
+ activeAnchors,
306
+ activeEndpoint,
307
+ }: {
308
+ path: string;
309
+ items: ClerkItemMeasure[];
310
+ activePath: string;
311
+ activeAnchors: string[];
312
+ activeEndpoint: { x: number; y: number } | null;
313
+ }) {
314
+ const activeSet = new Set(activeAnchors);
315
+ const [displayPath, setDisplayPath] = useState(activePath);
316
+ const [fadingPath, setFadingPath] = useState<string | null>(null);
317
+
318
+ useEffect(() => {
319
+ if (activePath === displayPath) return;
320
+ if (!displayPath) {
321
+ setDisplayPath(activePath);
322
+ setFadingPath(null);
323
+ return;
324
+ }
325
+
326
+ setFadingPath(displayPath);
327
+ setDisplayPath(activePath);
328
+
329
+ const timeout = window.setTimeout(() => {
330
+ setFadingPath(null);
331
+ }, CLERK_ACTIVE_ANIMATION_DURATION_MS);
332
+
333
+ return () => window.clearTimeout(timeout);
334
+ }, [activePath, displayPath]);
335
+
336
+ const dotTranslate = activeEndpoint
337
+ ? `translate(${activeEndpoint.x - CLERK_ACTIVE_DOT_RADIUS}px, ${
338
+ activeEndpoint.y - CLERK_ACTIVE_DOT_RADIUS
339
+ }px)`
340
+ : undefined;
341
+ const transitionStyle = {
342
+ transitionDuration: `${CLERK_ACTIVE_ANIMATION_DURATION_MS}ms`,
343
+ transitionTimingFunction: CLERK_ACTIVE_ANIMATION_EASING,
344
+ };
345
+
346
+ if (!path) return null;
347
+
348
+ return (
349
+ <>
350
+ <svg
351
+ aria-hidden="true"
352
+ className="pointer-events-none absolute inset-0 z-0 overflow-visible"
353
+ width="100%"
354
+ height="100%"
355
+ >
356
+ <path
357
+ d={path}
358
+ className="stroke-fd-foreground/15"
359
+ fill="none"
360
+ strokeWidth={CLERK_PATH_STROKE_WIDTH}
361
+ strokeLinecap="round"
362
+ strokeLinejoin="round"
363
+ />
364
+ </svg>
365
+ <svg
366
+ aria-hidden="true"
367
+ className="pointer-events-none absolute inset-0 z-0 overflow-visible"
368
+ width="100%"
369
+ height="100%"
370
+ >
371
+ {fadingPath ? (
372
+ <path
373
+ d={fadingPath}
374
+ fill="none"
375
+ strokeWidth={CLERK_PATH_STROKE_WIDTH}
376
+ strokeLinecap="round"
377
+ strokeLinejoin="round"
378
+ stroke={themeSvgIconColor}
379
+ className="transition-opacity"
380
+ style={{
381
+ opacity: 0,
382
+ ...transitionStyle,
383
+ }}
384
+ />
385
+ ) : null}
386
+ {displayPath ? (
387
+ <path
388
+ d={displayPath}
389
+ fill="none"
390
+ strokeWidth={CLERK_PATH_STROKE_WIDTH}
391
+ strokeLinecap="round"
392
+ strokeLinejoin="round"
393
+ stroke={themeSvgIconColor}
394
+ className="transition-opacity"
395
+ style={{
396
+ opacity: 1,
397
+ ...transitionStyle,
398
+ }}
399
+ />
400
+ ) : null}
401
+ </svg>
402
+ <div aria-hidden="true" className="pointer-events-none absolute inset-0 z-0">
403
+ <div
404
+ className="absolute rounded-full transition-transform"
405
+ style={{
406
+ width: CLERK_ACTIVE_DOT_RADIUS * 2,
407
+ height: CLERK_ACTIVE_DOT_RADIUS * 2,
408
+ backgroundColor: themeSvgIconColor,
409
+ transform: dotTranslate,
410
+ opacity: activeEndpoint ? 1 : 0,
411
+ ...transitionStyle,
412
+ }}
413
+ />
414
+ </div>
415
+ <svg
416
+ aria-hidden="true"
417
+ className="pointer-events-none absolute inset-0 z-1 overflow-visible"
418
+ width="100%"
419
+ height="100%"
420
+ >
421
+ {items.map((item) => {
422
+ if (!item.stepNumber) return null;
423
+
424
+ const isActive = activeSet.has(item.url.slice(1));
425
+
426
+ return (
427
+ <g key={item.url} transform={`translate(${item.x}, ${item.y})`}>
428
+ <circle
429
+ r={CLERK_STEP_BADGE_RADIUS}
430
+ fill={isActive ? themeSvgIconColor : undefined}
431
+ className={cn(!isActive && 'fill-black dark:fill-white')}
432
+ />
433
+ <text
434
+ y="0.5"
435
+ textAnchor="middle"
436
+ dominantBaseline="middle"
437
+ className="fill-white text-[9px] font-medium dark:fill-black"
438
+ >
439
+ {item.stepNumber}
440
+ </text>
441
+ </g>
442
+ );
443
+ })}
444
+ </svg>
445
+ </>
446
+ );
447
+ }
448
+
449
+ function getItemOffset(depth: number): number {
450
+ const lineOffset = getLineOffset(depth);
451
+ const badgeRadius = depth === 3 ? CLERK_STEP_BADGE_RADIUS : 0;
452
+ return lineOffset + badgeRadius + CLERK_TEXT_GAP_FROM_PATH;
453
+ }
454
+
455
+ function getLineOffset(depth: number): number {
456
+ return depth >= 3 ? 18 : 6;
457
+ }
458
+
459
+ function getVisualLinePosition(depth: number): number {
460
+ return getLineOffset(depth);
461
+ }
462
+
463
+ function resolveClerkItem(item: TOCItemType): ClerkItemMeta {
464
+ const isH3 = item.depth === 3;
465
+ const rawTitle = typeof item.title === 'string' ? item.title : '';
466
+ const { isStep, displayStep, content } = getStepInfoFromTitle(rawTitle);
467
+ let stepNumber: string | null = isH3 && isStep ? String(displayStep) : null;
468
+ let resolvedContent: ReactNode = item.title;
469
+
470
+ if (isH3 && isStep) {
471
+ resolvedContent = content ?? item.title;
472
+ }
473
+
474
+ if (isH3 && !stepNumber) {
475
+ const urlNum = getDigitsFromUrl(item.url);
476
+ if (urlNum != null) {
477
+ const clamped = Math.max(0, Math.min(19, urlNum));
478
+ stepNumber = String(clamped);
479
+ if (typeof rawTitle === 'string') {
480
+ const match = rawTitle.match(/^(\d+(?:\.\d+)*\.?)\s+(.+)$/);
481
+ if (match?.[2]) {
482
+ resolvedContent = match[2];
483
+ }
484
+ }
485
+ }
486
+ }
487
+
488
+ return {
489
+ item,
490
+ resolvedContent,
491
+ stepNumber,
492
+ itemPadding: getItemOffset(item.depth),
493
+ lineOffset: getVisualLinePosition(item.depth),
494
+ };
495
+ }
496
+
497
+ function buildOutlinePath(items: ClerkItemMeasure[]): string {
498
+ if (items.length === 0) return '';
499
+
500
+ const [first] = items;
501
+ const last = items.at(-1);
502
+ if (!last) return '';
503
+
504
+ let path = `M ${round(first.x)} ${round(first.y)}`;
505
+
506
+ for (let i = 1; i < items.length; i++) {
507
+ path += ` ${buildTurnSegment(items[i - 1], items[i])}`;
508
+ }
509
+
510
+ return path;
511
+ }
512
+
513
+ function buildTurnSegment(
514
+ previous: ClerkItemMeasure,
515
+ current: ClerkItemMeasure,
516
+ ): string {
517
+ if (Math.abs(previous.x - current.x) <= 0.5) {
518
+ return `L ${round(current.x)} ${round(current.y)}`;
519
+ }
520
+
521
+ const distanceY = current.y - previous.y;
522
+ if (distanceY <= 0) {
523
+ return `L ${round(current.x)} ${round(current.y)}`;
524
+ }
525
+
526
+ const gapMidY = previous.y + distanceY / 2;
527
+ const maxCurveHeight = Math.max(distanceY - CLERK_TURN_GAP_MARGIN * 2, 0);
528
+ const curveHeight = Math.min(
529
+ CLERK_TURN_CURVE_HEIGHT,
530
+ Math.max(maxCurveHeight, 0),
531
+ );
532
+ if (curveHeight <= 0.5) {
533
+ return `L ${round(current.x)} ${round(current.y)}`;
534
+ }
535
+
536
+ const turnStartY = gapMidY - curveHeight / 2;
537
+ const turnEndY = gapMidY + curveHeight / 2;
538
+ const controlDelta = curveHeight * CLERK_TURN_CONTROL_FACTOR;
539
+
540
+ return [
541
+ `L ${round(previous.x)} ${round(turnStartY)}`,
542
+ `C ${round(previous.x)} ${round(turnStartY + controlDelta)} ${round(
543
+ current.x,
544
+ )} ${round(turnEndY - controlDelta)} ${round(current.x)} ${round(
545
+ turnEndY,
546
+ )}`,
547
+ `L ${round(current.x)} ${round(current.y)}`,
548
+ ].join(' ');
549
+ }
550
+
551
+ function getActiveItems(
552
+ items: ClerkItemMeasure[],
553
+ activeAnchors: string[],
554
+ ): ClerkItemMeasure[] {
555
+ if (items.length === 0 || activeAnchors.length === 0) return [];
556
+
557
+ return items.filter((item) => activeAnchors.includes(item.url.slice(1)));
558
+ }
559
+
560
+ function getActiveEndpoint(
561
+ items: ClerkItemMeasure[],
562
+ ): { x: number; y: number } | null {
563
+ if (items.length === 0) return null;
564
+
565
+ const last = items.at(-1);
566
+ if (!last) return null;
567
+
568
+ return {
569
+ x: last.x,
570
+ y: last.y,
571
+ };
572
+ }
573
+
574
+ function isSameLayout(
575
+ previous: { height: number; items: ClerkItemMeasure[] },
576
+ next: { height: number; items: ClerkItemMeasure[] },
577
+ ): boolean {
578
+ if (Math.abs(previous.height - next.height) > 0.5) return false;
579
+ if (previous.items.length !== next.items.length) return false;
580
+
581
+ for (let i = 0; i < previous.items.length; i++) {
582
+ const prev = previous.items[i];
583
+ const curr = next.items[i];
584
+ if (!prev || !curr) return false;
585
+ if (prev.url !== curr.url || prev.stepNumber !== curr.stepNumber) {
586
+ return false;
587
+ }
588
+ if (Math.abs(prev.y - curr.y) > 0.5) return false;
589
+ if (Math.abs(prev.x - curr.x) > 0.5) return false;
590
+ }
591
+
592
+ return true;
593
+ }
594
+
595
+ function measureItemLineY(
596
+ element: HTMLAnchorElement,
597
+ content: HTMLSpanElement,
598
+ ): number {
599
+ const anchorRect = element.getBoundingClientRect();
600
+ const lineRects = Array.from(content.getClientRects()).filter(
601
+ (rect) => rect.height > 0,
602
+ );
603
+
604
+ if (lineRects.length > 0) {
605
+ const lastRect = lineRects.at(-1);
606
+ if (lastRect) {
607
+ return (
608
+ element.offsetTop +
609
+ (lastRect.top - anchorRect.top) +
610
+ lastRect.height / 2
611
+ );
612
+ }
613
+ }
614
+
615
+ const styles = getComputedStyle(element);
616
+ const top = element.offsetTop + parseFloat(styles.paddingTop);
617
+ const bottom =
618
+ element.offsetTop +
619
+ element.clientHeight -
620
+ parseFloat(styles.paddingBottom);
621
+ return (top + bottom) / 2;
622
+ }
623
+
624
+ function round(value: number): number {
625
+ return Number(value.toFixed(2));
626
+ }
627
+
628
+ function getDigitsFromUrl(url: string): number | null {
629
+ const match = /^#(\d+)-/.exec(url);
630
+ if (!match) return null;
631
+ const value = Number.parseInt(match[1], 10);
632
+ return Number.isNaN(value) ? null : value;
633
+ }
634
+
635
+ function getStepInfoFromTitle(title: string): {
636
+ isStep: boolean;
637
+ displayStep: number | null;
638
+ content: string | null;
639
+ } {
640
+ const trimmed = title.trim();
641
+ const match = trimmed.match(/^(\d+(?:\.\d+)*\.?)\s+(.+)$/);
642
+ if (!match) return { isStep: false, displayStep: null, content: null };
643
+
644
+ const content = (match[2] ?? '').trim();
645
+ if (content.length === 0) {
646
+ return { isStep: false, displayStep: null, content: null };
647
+ }
648
+
649
+ const numericPart = match[1].replace(/\.$/, '');
650
+ const parts = numericPart.split('.').map((part) => Number.parseInt(part, 10));
651
+ const lastPart = parts.at(-1);
652
+ if (lastPart == null || Number.isNaN(lastPart)) {
653
+ return { isStep: false, displayStep: null, content: null };
654
+ }
655
+
656
+ const clamped = Math.max(0, Math.min(19, lastPart));
657
+ return { isStep: true, displayStep: clamped, content };
658
+ }
659
+
660
+ function cn(...inputs: Array<string | false | null | undefined>): string {
661
+ return inputs.filter(Boolean).join(' ');
662
+ }
663
+
664
+ function mergeRefs<T>(
665
+ ...refs: Array<
666
+ React.Ref<T> | ((instance: T | null) => void) | null | undefined
667
+ >
668
+ ) {
669
+ return (node: T | null) => {
670
+ for (const ref of refs) {
671
+ if (!ref) continue;
672
+ if (typeof ref === 'function') {
673
+ ref(node);
674
+ continue;
675
+ }
676
+
677
+ try {
678
+ (ref as React.RefObject<T | null>).current = node;
679
+ } catch {
680
+ // ignore readonly refs
681
+ }
682
+ }
683
+ };
684
+ }
@@ -51,7 +51,6 @@ export function DelayedImg({
51
51
  shape="rounded-rect"
52
52
  loading
53
53
  themeColor={themeSvgIconColor}
54
- strokeWidth={3}
55
54
  className={cn(
56
55
  "absolute inset-0 rounded-[inherit] border shadow-sm bg-white/70 dark:bg-white/5",
57
56
  themeBgColor,
@@ -32,12 +32,12 @@ const TRACK_COLOR = 'rgba(148, 163, 184, 0.22)';
32
32
  const BODY_LENGTH_RATIO = 0.26;
33
33
  const EXIT_DURATION_MS = 260;
34
34
  const LOOP_DURATION_SECONDS = 1.85;
35
- const DEFAULT_CIRCLE_STROKE = 2;
36
- const DEFAULT_RECT_STROKE = 3;
35
+ const DEFAULT_CIRCLE_STROKE = 0.5;
36
+ const DEFAULT_RECT_STROKE = 1;
37
37
  const MIN_FRAME_SIZE = 2;
38
38
  const MIN_BODY_LENGTH = 24;
39
39
  const MAX_BODY_LENGTH_RATIO = 0.36;
40
- const RECT_MIN_STROKE_WIDTH = 3;
40
+ const RECT_MIN_STROKE_WIDTH = 1;
41
41
 
42
42
  function clampProgress(progress: number): number {
43
43
  if (!Number.isFinite(progress)) {