tekivex-ui 2.2.0 → 2.5.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 (129) hide show
  1. package/dist/TkxForm-BWK4LqY3.js +1461 -0
  2. package/dist/TkxForm-ljQjX7KD.cjs +15 -0
  3. package/dist/charts.cjs +1 -0
  4. package/dist/charts.js +480 -0
  5. package/dist/headless.cjs +1 -0
  6. package/dist/headless.js +258 -0
  7. package/dist/index-BINBzXuY.cjs +2 -0
  8. package/dist/index-eT_U4qB2.js +512 -0
  9. package/dist/index.cjs +63 -14
  10. package/dist/index.d.ts +33 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +11396 -6336
  13. package/dist/src/charts/TkxAreaChart.d.ts +29 -0
  14. package/dist/src/charts/TkxAreaChart.d.ts.map +1 -0
  15. package/dist/src/charts/TkxBarChart.d.ts +28 -0
  16. package/dist/src/charts/TkxBarChart.d.ts.map +1 -0
  17. package/dist/src/charts/TkxDonutChart.d.ts +23 -0
  18. package/dist/src/charts/TkxDonutChart.d.ts.map +1 -0
  19. package/dist/src/charts/TkxLineChart.d.ts +35 -0
  20. package/dist/src/charts/TkxLineChart.d.ts.map +1 -0
  21. package/dist/src/charts/TkxPieChart.d.ts +19 -0
  22. package/dist/src/charts/TkxPieChart.d.ts.map +1 -0
  23. package/dist/src/charts/TkxRadarChart.d.ts +20 -0
  24. package/dist/src/charts/TkxRadarChart.d.ts.map +1 -0
  25. package/dist/src/charts/TkxScatterChart.d.ts +28 -0
  26. package/dist/src/charts/TkxScatterChart.d.ts.map +1 -0
  27. package/dist/src/charts/index.d.ts +15 -0
  28. package/dist/src/charts/index.d.ts.map +1 -0
  29. package/dist/src/charts/shared.d.ts +33 -0
  30. package/dist/src/charts/shared.d.ts.map +1 -0
  31. package/dist/src/components/TkxAffix.d.ts +10 -0
  32. package/dist/src/components/TkxAffix.d.ts.map +1 -0
  33. package/dist/src/components/TkxAnchor.d.ts +14 -0
  34. package/dist/src/components/TkxAnchor.d.ts.map +1 -0
  35. package/dist/src/components/TkxCascader.d.ts +16 -0
  36. package/dist/src/components/TkxCascader.d.ts.map +1 -0
  37. package/dist/src/components/TkxDataGrid.d.ts +19 -1
  38. package/dist/src/components/TkxDataGrid.d.ts.map +1 -1
  39. package/dist/src/components/TkxForm.d.ts +33 -3
  40. package/dist/src/components/TkxForm.d.ts.map +1 -1
  41. package/dist/src/components/TkxImage.d.ts.map +1 -1
  42. package/dist/src/components/TkxList.d.ts +24 -0
  43. package/dist/src/components/TkxList.d.ts.map +1 -0
  44. package/dist/src/components/TkxMentions.d.ts +15 -0
  45. package/dist/src/components/TkxMentions.d.ts.map +1 -0
  46. package/dist/src/components/TkxQRCode.d.ts +11 -0
  47. package/dist/src/components/TkxQRCode.d.ts.map +1 -0
  48. package/dist/src/components/TkxResult.d.ts +11 -0
  49. package/dist/src/components/TkxResult.d.ts.map +1 -0
  50. package/dist/src/components/TkxSegmented.d.ts +16 -0
  51. package/dist/src/components/TkxSegmented.d.ts.map +1 -0
  52. package/dist/src/components/TkxSelect.d.ts +5 -1
  53. package/dist/src/components/TkxSelect.d.ts.map +1 -1
  54. package/dist/src/components/TkxTour.d.ts +15 -0
  55. package/dist/src/components/TkxTour.d.ts.map +1 -0
  56. package/dist/src/components/TkxTypography.d.ts +1 -1
  57. package/dist/src/components/TkxTypography.d.ts.map +1 -1
  58. package/dist/src/components/TkxWatermark.d.ts +12 -0
  59. package/dist/src/components/TkxWatermark.d.ts.map +1 -0
  60. package/dist/src/components/index.d.ts +10 -0
  61. package/dist/src/components/index.d.ts.map +1 -1
  62. package/dist/src/engine/tkx.d.ts +38 -0
  63. package/dist/src/engine/tkx.d.ts.map +1 -1
  64. package/dist/src/headless/index.d.ts +18 -0
  65. package/dist/src/headless/index.d.ts.map +1 -0
  66. package/dist/src/headless/useControllable.d.ts +20 -0
  67. package/dist/src/headless/useControllable.d.ts.map +1 -0
  68. package/dist/src/headless/useDebounce.d.ts +14 -0
  69. package/dist/src/headless/useDebounce.d.ts.map +1 -0
  70. package/dist/src/headless/useDisclosure.d.ts +20 -0
  71. package/dist/src/headless/useDisclosure.d.ts.map +1 -0
  72. package/dist/src/headless/useFormState.d.ts +44 -0
  73. package/dist/src/headless/useFormState.d.ts.map +1 -0
  74. package/dist/src/headless/useIntersectionObserver.d.ts +23 -0
  75. package/dist/src/headless/useIntersectionObserver.d.ts.map +1 -0
  76. package/dist/src/headless/useListSelection.d.ts +29 -0
  77. package/dist/src/headless/useListSelection.d.ts.map +1 -0
  78. package/dist/src/headless/useLocalStorage.d.ts +9 -0
  79. package/dist/src/headless/useLocalStorage.d.ts.map +1 -0
  80. package/dist/src/headless/useMediaQuery.d.ts +21 -0
  81. package/dist/src/headless/useMediaQuery.d.ts.map +1 -0
  82. package/dist/src/headless/useRovingTabIndex.d.ts +33 -0
  83. package/dist/src/headless/useRovingTabIndex.d.ts.map +1 -0
  84. package/dist/src/headless/useThrottle.d.ts +9 -0
  85. package/dist/src/headless/useThrottle.d.ts.map +1 -0
  86. package/dist/src/i18n/index.d.ts +47 -2
  87. package/dist/src/i18n/index.d.ts.map +1 -1
  88. package/dist/src/themes/index.d.ts.map +1 -1
  89. package/package.json +61 -7
  90. package/src/charts/TkxAreaChart.tsx +119 -0
  91. package/src/charts/TkxBarChart.tsx +115 -0
  92. package/src/charts/TkxDonutChart.tsx +145 -0
  93. package/src/charts/TkxLineChart.tsx +135 -0
  94. package/src/charts/TkxPieChart.tsx +82 -0
  95. package/src/charts/TkxRadarChart.tsx +90 -0
  96. package/src/charts/TkxScatterChart.tsx +102 -0
  97. package/src/charts/index.ts +20 -0
  98. package/src/charts/shared.ts +45 -0
  99. package/src/components/TkxAffix.tsx +138 -0
  100. package/src/components/TkxAnchor.tsx +193 -0
  101. package/src/components/TkxCascader.tsx +276 -0
  102. package/src/components/TkxDataGrid.tsx +557 -230
  103. package/src/components/TkxForm.tsx +67 -15
  104. package/src/components/TkxImage.tsx +1 -1
  105. package/src/components/TkxList.tsx +242 -0
  106. package/src/components/TkxMentions.tsx +210 -0
  107. package/src/components/TkxQRCode.tsx +174 -0
  108. package/src/components/TkxResult.tsx +128 -0
  109. package/src/components/TkxSegmented.tsx +169 -0
  110. package/src/components/TkxSelect.tsx +209 -6
  111. package/src/components/TkxTour.tsx +239 -0
  112. package/src/components/TkxTypography.tsx +4 -2
  113. package/src/components/TkxWatermark.tsx +143 -0
  114. package/src/components/index.ts +10 -0
  115. package/src/engine/tkx.ts +122 -3
  116. package/src/headless/index.ts +44 -0
  117. package/src/headless/useControllable.ts +40 -0
  118. package/src/headless/useDebounce.ts +35 -0
  119. package/src/headless/useDisclosure.ts +28 -0
  120. package/src/headless/useFormState.ts +111 -0
  121. package/src/headless/useIntersectionObserver.ts +51 -0
  122. package/src/headless/useListSelection.ts +101 -0
  123. package/src/headless/useLocalStorage.ts +46 -0
  124. package/src/headless/useMediaQuery.ts +37 -0
  125. package/src/headless/useRovingTabIndex.ts +84 -0
  126. package/src/headless/useThrottle.ts +46 -0
  127. package/src/i18n/I18nProvider.tsx +1 -1
  128. package/src/i18n/index.ts +678 -38
  129. package/src/themes/index.ts +31 -4
@@ -0,0 +1,102 @@
1
+ import {
2
+ ResponsiveContainer,
3
+ ScatterChart,
4
+ Scatter,
5
+ XAxis,
6
+ YAxis,
7
+ ZAxis,
8
+ CartesianGrid,
9
+ Tooltip,
10
+ Legend,
11
+ } from 'recharts';
12
+ import { useTheme } from '../themes';
13
+ import { getDefaultColors, tooltipStyle, DEFAULT_MARGIN, type ChartMargin } from './shared';
14
+
15
+ export interface TkxScatterSeries {
16
+ data: { x: number; y: number; z?: number; label?: string }[];
17
+ name?: string;
18
+ color?: string;
19
+ }
20
+
21
+ export interface TkxScatterChartProps {
22
+ series: TkxScatterSeries[];
23
+ height?: number;
24
+ margin?: ChartMargin;
25
+ showGrid?: boolean;
26
+ showLegend?: boolean;
27
+ showTooltip?: boolean;
28
+ xLabel?: string;
29
+ yLabel?: string;
30
+ xTickFormatter?: (value: any) => string;
31
+ yTickFormatter?: (value: any) => string;
32
+ /** Z-axis range (bubble size). Default: [20, 400] */
33
+ zRange?: [number, number];
34
+ ariaLabel?: string;
35
+ }
36
+
37
+ export function TkxScatterChart({
38
+ series,
39
+ height = 300,
40
+ margin = DEFAULT_MARGIN,
41
+ showGrid = true,
42
+ showLegend = true,
43
+ showTooltip = true,
44
+ xTickFormatter,
45
+ yTickFormatter,
46
+ zRange = [20, 400],
47
+ ariaLabel = 'Scatter chart',
48
+ }: TkxScatterChartProps) {
49
+ const theme = useTheme();
50
+ const colors = getDefaultColors(theme);
51
+ const tt = tooltipStyle(theme);
52
+ const tickStyle = { fill: theme.textMuted, fontSize: 12 };
53
+
54
+ return (
55
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%', height }}>
56
+ <ResponsiveContainer width="100%" height="100%">
57
+ <ScatterChart margin={margin}>
58
+ {showGrid && (
59
+ <CartesianGrid strokeDasharray="3 3" stroke={theme.border} />
60
+ )}
61
+ <XAxis
62
+ type="number"
63
+ dataKey="x"
64
+ tick={tickStyle}
65
+ axisLine={{ stroke: theme.border }}
66
+ tickLine={false}
67
+ tickFormatter={xTickFormatter}
68
+ />
69
+ <YAxis
70
+ type="number"
71
+ dataKey="y"
72
+ tick={tickStyle}
73
+ axisLine={false}
74
+ tickLine={false}
75
+ tickFormatter={yTickFormatter}
76
+ />
77
+ <ZAxis type="number" dataKey="z" range={zRange} />
78
+ {showTooltip && (
79
+ <Tooltip
80
+ contentStyle={tt.contentStyle}
81
+ labelStyle={tt.labelStyle}
82
+ itemStyle={tt.itemStyle}
83
+ cursor={{ strokeDasharray: '3 3' }}
84
+ />
85
+ )}
86
+ {showLegend && (
87
+ <Legend wrapperStyle={{ fontSize: 13, color: theme.textMuted, paddingTop: 8 }} />
88
+ )}
89
+ {series.map((s, i) => (
90
+ <Scatter
91
+ key={s.name ?? i}
92
+ name={s.name}
93
+ data={s.data}
94
+ fill={s.color ?? colors[i % colors.length]}
95
+ fillOpacity={0.8}
96
+ />
97
+ ))}
98
+ </ScatterChart>
99
+ </ResponsiveContainer>
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,20 @@
1
+ // ── @tekivex/ui/charts ───────────────────────────────────────────────────────
2
+ // Theme-aware chart components built on Recharts.
3
+ // Import from '@tekivex/ui/charts' — zero impact on the main bundle.
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+
6
+ export { TkxAreaChart } from './TkxAreaChart';
7
+ export { TkxBarChart } from './TkxBarChart';
8
+ export { TkxLineChart } from './TkxLineChart';
9
+ export { TkxPieChart } from './TkxPieChart';
10
+ export { TkxDonutChart } from './TkxDonutChart';
11
+ export { TkxScatterChart } from './TkxScatterChart';
12
+ export { TkxRadarChart } from './TkxRadarChart';
13
+
14
+ export type { TkxAreaChartProps } from './TkxAreaChart';
15
+ export type { TkxBarChartProps } from './TkxBarChart';
16
+ export type { TkxLineChartProps } from './TkxLineChart';
17
+ export type { TkxPieChartProps } from './TkxPieChart';
18
+ export type { TkxDonutChartProps } from './TkxDonutChart';
19
+ export type { TkxScatterChartProps } from './TkxScatterChart';
20
+ export type { TkxRadarChartProps } from './TkxRadarChart';
@@ -0,0 +1,45 @@
1
+ // ── Shared chart utilities ───────────────────────────────────────────────────
2
+
3
+ import type { ThemeTokens } from '../themes';
4
+
5
+ /** Default palette: uses theme tokens then falls back to categorical colors */
6
+ export function getDefaultColors(theme: ThemeTokens): string[] {
7
+ return [
8
+ theme.primary,
9
+ theme.secondary,
10
+ theme.info,
11
+ theme.success,
12
+ theme.warning,
13
+ theme.danger,
14
+ '#a855f7',
15
+ '#f97316',
16
+ '#14b8a6',
17
+ '#e879f9',
18
+ ];
19
+ }
20
+
21
+ export interface ChartMargin {
22
+ top?: number;
23
+ right?: number;
24
+ bottom?: number;
25
+ left?: number;
26
+ }
27
+
28
+ export const DEFAULT_MARGIN: ChartMargin = { top: 10, right: 30, bottom: 10, left: 0 };
29
+
30
+ /** Common recharts tooltip style that matches TekiVex theme */
31
+ export function tooltipStyle(theme: ThemeTokens) {
32
+ return {
33
+ contentStyle: {
34
+ backgroundColor: theme.surface,
35
+ border: `1px solid ${theme.border}`,
36
+ borderRadius: 8,
37
+ boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
38
+ color: theme.text,
39
+ fontSize: 13,
40
+ },
41
+ labelStyle: { color: theme.textMuted, fontWeight: 600, marginBottom: 4 },
42
+ itemStyle: { color: theme.text },
43
+ cursor: { fill: `${theme.primary}15` },
44
+ };
45
+ }
@@ -0,0 +1,138 @@
1
+ import { type ReactNode, useState, useRef, useEffect, useCallback, useMemo } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { sanitizeString } from '../engine/security';
4
+ import { useReducedMotion } from '../hooks';
5
+ import { tkx } from '../engine/tkx';
6
+
7
+ // ── Types ────────────────────────────────────────────────────────────────────
8
+
9
+ export interface TkxAffixProps {
10
+ children: ReactNode;
11
+ offsetTop?: number;
12
+ offsetBottom?: number;
13
+ onChange?: (affixed: boolean) => void;
14
+ target?: () => HTMLElement | Window;
15
+ }
16
+
17
+ // ── Helpers ──────────────────────────────────────────────────────────────────
18
+
19
+ function getScrollTarget(target?: () => HTMLElement | Window): HTMLElement | Window {
20
+ if (target) {
21
+ try {
22
+ return target();
23
+ } catch {
24
+ return window;
25
+ }
26
+ }
27
+ return window;
28
+ }
29
+
30
+ function getTargetRect(el: HTMLElement | Window): { top: number; bottom: number } {
31
+ if (el instanceof Window) {
32
+ return { top: 0, bottom: el.innerHeight };
33
+ }
34
+ const rect = el.getBoundingClientRect();
35
+ return { top: rect.top, bottom: rect.bottom };
36
+ }
37
+
38
+ // ── Component ────────────────────────────────────────────────────────────────
39
+
40
+ export function TkxAffix({
41
+ children,
42
+ offsetTop,
43
+ offsetBottom,
44
+ onChange,
45
+ target,
46
+ }: TkxAffixProps) {
47
+ const theme = useTheme();
48
+ const reducedMotion = useReducedMotion();
49
+ const placeholderRef = useRef<HTMLDivElement>(null);
50
+ const fixedRef = useRef<HTMLDivElement>(null);
51
+ const [affixed, setAffixed] = useState(false);
52
+ const [placeholderSize, setPlaceholderSize] = useState({ width: 0, height: 0 });
53
+ const prevAffixed = useRef(false);
54
+
55
+ // Sanitize any debug info strings for security
56
+ const _labelSafe = useMemo(() => sanitizeString('Sticky content'), []);
57
+
58
+ const checkPosition = useCallback(() => {
59
+ const el = placeholderRef.current;
60
+ if (!el) return;
61
+
62
+ const rect = el.getBoundingClientRect();
63
+ const targetRect = getTargetRect(getScrollTarget(target));
64
+ let shouldAffix = false;
65
+
66
+ if (offsetTop !== undefined) {
67
+ // Affix when element scrolls past the top offset
68
+ shouldAffix = rect.top - targetRect.top <= offsetTop;
69
+ } else if (offsetBottom !== undefined) {
70
+ // Affix when element scrolls past the bottom offset
71
+ shouldAffix = targetRect.bottom - rect.bottom <= offsetBottom;
72
+ }
73
+
74
+ if (shouldAffix !== prevAffixed.current) {
75
+ prevAffixed.current = shouldAffix;
76
+ setAffixed(shouldAffix);
77
+ setPlaceholderSize({
78
+ width: rect.width,
79
+ height: rect.height,
80
+ });
81
+ onChange?.(shouldAffix);
82
+ }
83
+ }, [offsetTop, offsetBottom, onChange, target]);
84
+
85
+ // Attach scroll and resize listeners
86
+ useEffect(() => {
87
+ const scrollTarget = getScrollTarget(target);
88
+
89
+ checkPosition();
90
+ scrollTarget.addEventListener('scroll', checkPosition, { passive: true });
91
+ window.addEventListener('resize', checkPosition);
92
+
93
+ return () => {
94
+ scrollTarget.removeEventListener('scroll', checkPosition);
95
+ window.removeEventListener('resize', checkPosition);
96
+ };
97
+ }, [checkPosition, target]);
98
+
99
+ // Build the fixed positioning styles
100
+ const fixedStyle: React.CSSProperties = affixed
101
+ ? {
102
+ position: 'fixed',
103
+ zIndex: 100,
104
+ ...(offsetTop !== undefined ? { top: offsetTop } : {}),
105
+ ...(offsetBottom !== undefined ? { bottom: offsetBottom } : {}),
106
+ width: placeholderSize.width || undefined,
107
+ transition: reducedMotion ? 'none' : 'box-shadow 0.2s ease',
108
+ boxShadow: `0 2px 8px ${theme.border}`,
109
+ }
110
+ : {};
111
+
112
+ // Placeholder keeps the document flow intact when element is fixed
113
+ const placeholderStyle: React.CSSProperties = affixed
114
+ ? {
115
+ height: placeholderSize.height,
116
+ width: placeholderSize.width,
117
+ }
118
+ : {};
119
+
120
+ return (
121
+ <>
122
+ <div
123
+ ref={placeholderRef}
124
+ aria-hidden={affixed}
125
+ style={placeholderStyle}
126
+ />
127
+ <div
128
+ ref={fixedRef}
129
+ role="region"
130
+ aria-label={_labelSafe}
131
+ className={tkx(affixed ? 'tkx-affixed' : '')}
132
+ style={fixedStyle}
133
+ >
134
+ {children}
135
+ </div>
136
+ </>
137
+ );
138
+ }
@@ -0,0 +1,193 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { useTheme } from '../themes';
3
+ import { sanitizeString } from '../engine/security';
4
+ import { useReducedMotion } from '../hooks';
5
+ import { tkx } from '../engine/tkx';
6
+
7
+ // ── Types ────────────────────────────────────────────────────────────────────
8
+
9
+ export interface AnchorLink {
10
+ key: string;
11
+ href: string;
12
+ title: string;
13
+ children?: AnchorLink[];
14
+ }
15
+
16
+ export interface TkxAnchorProps {
17
+ items: AnchorLink[];
18
+ offsetTop?: number;
19
+ getCurrentAnchor?: (activeLink: string) => string;
20
+ onChange?: (currentLink: string) => void;
21
+ }
22
+
23
+ // ── Helpers ──────────────────────────────────────────────────────────────────
24
+
25
+ function flattenLinks(items: AnchorLink[]): AnchorLink[] {
26
+ const result: AnchorLink[] = [];
27
+ for (const item of items) {
28
+ result.push(item);
29
+ if (item.children) result.push(...flattenLinks(item.children));
30
+ }
31
+ return result;
32
+ }
33
+
34
+ function getAnchorId(href: string): string {
35
+ return href.startsWith('#') ? href.slice(1) : href;
36
+ }
37
+
38
+ // ── Link Item ────────────────────────────────────────────────────────────────
39
+
40
+ function AnchorItem({
41
+ item,
42
+ activeKey,
43
+ depth,
44
+ onClick,
45
+ theme,
46
+ reducedMotion,
47
+ }: {
48
+ item: AnchorLink;
49
+ activeKey: string;
50
+ depth: number;
51
+ onClick: (href: string) => void;
52
+ theme: ReturnType<typeof useTheme>;
53
+ reducedMotion: boolean;
54
+ }) {
55
+ const isActive = item.key === activeKey;
56
+ const safeTitle = sanitizeString(item.title);
57
+
58
+ return (
59
+ <li role="none" style={{ listStyle: 'none' }}>
60
+ <a
61
+ role="treeitem"
62
+ aria-current={isActive ? 'location' : undefined}
63
+ href={item.href}
64
+ onClick={(e) => {
65
+ e.preventDefault();
66
+ onClick(item.href);
67
+ }}
68
+ className={tkx('block text-sm py-1 no-underline')}
69
+ style={{
70
+ paddingLeft: 12 + depth * 16,
71
+ color: isActive ? theme.primary : theme.textMuted,
72
+ fontWeight: isActive ? 600 : 400,
73
+ borderLeft: `2px solid ${isActive ? theme.primary : 'transparent'}`,
74
+ transition: reducedMotion ? 'none' : 'color 0.15s ease, border-color 0.15s ease',
75
+ }}
76
+ >
77
+ {safeTitle}
78
+ </a>
79
+ {item.children && item.children.length > 0 && (
80
+ <ul role="group" style={{ margin: 0, padding: 0 }}>
81
+ {item.children.map((child) => (
82
+ <AnchorItem
83
+ key={child.key}
84
+ item={child}
85
+ activeKey={activeKey}
86
+ depth={depth + 1}
87
+ onClick={onClick}
88
+ theme={theme}
89
+ reducedMotion={reducedMotion}
90
+ />
91
+ ))}
92
+ </ul>
93
+ )}
94
+ </li>
95
+ );
96
+ }
97
+
98
+ // ── Component ────────────────────────────────────────────────────────────────
99
+
100
+ export function TkxAnchor({
101
+ items,
102
+ offsetTop = 0,
103
+ getCurrentAnchor,
104
+ onChange,
105
+ }: TkxAnchorProps) {
106
+ const theme = useTheme();
107
+ const reducedMotion = useReducedMotion();
108
+ const [activeKey, setActiveKey] = useState(items[0]?.key ?? '');
109
+ const isScrolling = useRef(false);
110
+
111
+ const flat = flattenLinks(items);
112
+
113
+ const updateActive = useCallback(() => {
114
+ if (isScrolling.current) return;
115
+
116
+ let current = flat[0]?.key ?? '';
117
+ for (const link of flat) {
118
+ const id = getAnchorId(link.href);
119
+ const el = document.getElementById(id);
120
+ if (el) {
121
+ const top = el.getBoundingClientRect().top;
122
+ if (top <= offsetTop + 10) {
123
+ current = link.key;
124
+ }
125
+ }
126
+ }
127
+
128
+ if (getCurrentAnchor) {
129
+ current = getCurrentAnchor(current);
130
+ }
131
+
132
+ if (current !== activeKey) {
133
+ setActiveKey(current);
134
+ onChange?.(current);
135
+ }
136
+ }, [flat, offsetTop, getCurrentAnchor, activeKey, onChange]);
137
+
138
+ useEffect(() => {
139
+ window.addEventListener('scroll', updateActive, { passive: true });
140
+ updateActive();
141
+ return () => window.removeEventListener('scroll', updateActive);
142
+ }, [updateActive]);
143
+
144
+ const handleClick = useCallback(
145
+ (href: string) => {
146
+ const id = getAnchorId(href);
147
+ const el = document.getElementById(id);
148
+ if (!el) return;
149
+
150
+ isScrolling.current = true;
151
+ const top = el.getBoundingClientRect().top + window.scrollY - offsetTop;
152
+ window.scrollTo({ top, behavior: reducedMotion ? 'auto' : 'smooth' });
153
+
154
+ // Find the link by href and set active
155
+ const link = flat.find((l) => l.href === href);
156
+ if (link) {
157
+ setActiveKey(link.key);
158
+ onChange?.(link.key);
159
+ }
160
+
161
+ setTimeout(() => {
162
+ isScrolling.current = false;
163
+ }, 800);
164
+ },
165
+ [flat, offsetTop, reducedMotion, onChange],
166
+ );
167
+
168
+ return (
169
+ <nav
170
+ role="tree"
171
+ aria-label="Anchor navigation"
172
+ className={tkx('font-sans')}
173
+ style={{
174
+ borderLeft: `1px solid ${theme.border}`,
175
+ padding: '4px 0',
176
+ }}
177
+ >
178
+ <ul role="group" style={{ margin: 0, padding: 0 }}>
179
+ {items.map((item) => (
180
+ <AnchorItem
181
+ key={item.key}
182
+ item={item}
183
+ activeKey={activeKey}
184
+ depth={0}
185
+ onClick={handleClick}
186
+ theme={theme}
187
+ reducedMotion={reducedMotion}
188
+ />
189
+ ))}
190
+ </ul>
191
+ </nav>
192
+ );
193
+ }