@srcroot/ui 0.0.54 → 0.0.56
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/README.md +151 -151
- package/dist/index.d.ts +0 -0
- package/dist/index.js +55 -1
- package/package.json +7 -2
- package/src/registry/analytics/google-analytics.tsx +36 -39
- package/src/registry/analytics/google-tag-manager.tsx +62 -65
- package/src/registry/analytics/meta-pixel.tsx +44 -47
- package/src/registry/analytics/microsoft-clarity.tsx +31 -34
- package/src/registry/analytics/tiktok-pixel.tsx +34 -37
- package/src/registry/lib/utils.ts +0 -0
- package/src/registry/themes/v3/blue.css +157 -157
- package/src/registry/themes/v3/glass.css +153 -153
- package/src/registry/themes/v3/gray.css +157 -157
- package/src/registry/themes/v3/green.css +157 -157
- package/src/registry/themes/v3/neutral.css +157 -157
- package/src/registry/themes/v3/orange.css +157 -157
- package/src/registry/themes/v3/rose.css +157 -157
- package/src/registry/themes/v3/slate.css +157 -157
- package/src/registry/themes/v3/stone.css +157 -157
- package/src/registry/themes/v3/violet.css +186 -186
- package/src/registry/themes/v3/zinc.css +157 -157
- package/src/registry/themes/v4/blue.css +184 -184
- package/src/registry/themes/v4/glass.css +180 -180
- package/src/registry/themes/v4/gray.css +184 -184
- package/src/registry/themes/v4/green.css +184 -184
- package/src/registry/themes/v4/neutral.css +184 -184
- package/src/registry/themes/v4/orange.css +184 -184
- package/src/registry/themes/v4/rose.css +184 -184
- package/src/registry/themes/v4/slate.css +184 -184
- package/src/registry/themes/v4/stone.css +184 -184
- package/src/registry/themes/v4/violet.css +184 -184
- package/src/registry/themes/v4/zinc.css +184 -184
- package/src/registry/ui/accordion.tsx +164 -165
- package/src/registry/ui/alert-dialog.tsx +213 -214
- package/src/registry/ui/alert.tsx +73 -76
- package/src/registry/ui/aspect-ratio.tsx +44 -47
- package/src/registry/ui/avatar.tsx +96 -97
- package/src/registry/ui/badge.tsx +52 -55
- package/src/registry/ui/breadcrumb.tsx +147 -150
- package/src/registry/ui/button-group.tsx +64 -67
- package/src/registry/ui/button.tsx +71 -72
- package/src/registry/ui/calendar.tsx +514 -515
- package/src/registry/ui/card.tsx +88 -91
- package/src/registry/ui/carousel.tsx +214 -214
- package/src/registry/ui/chart.tsx +373 -373
- package/src/registry/ui/chatbot.tsx +86 -13
- package/src/registry/ui/checkbox.tsx +93 -94
- package/src/registry/ui/collapsible.tsx +107 -108
- package/src/registry/ui/combobox.tsx +171 -171
- package/src/registry/ui/command.tsx +300 -300
- package/src/registry/ui/container.tsx +44 -47
- package/src/registry/ui/context-menu.tsx +221 -221
- package/src/registry/ui/date-picker.tsx +228 -228
- package/src/registry/ui/dialog.tsx +269 -270
- package/src/registry/ui/drawer.tsx +10 -4
- package/src/registry/ui/dropdown-menu.tsx +529 -530
- package/src/registry/ui/empty-state.tsx +0 -2
- package/src/registry/ui/file-upload.tsx +0 -0
- package/src/registry/ui/floating-dock.tsx +0 -0
- package/src/registry/ui/form-field.tsx +91 -94
- package/src/registry/ui/google-analytics.tsx +38 -0
- package/src/registry/ui/google-tag-manager.tsx +64 -0
- package/src/registry/ui/hover-card.tsx +223 -223
- package/src/registry/ui/image.tsx +144 -147
- package/src/registry/ui/input-group.tsx +82 -85
- package/src/registry/ui/input.tsx +125 -125
- package/src/registry/ui/kbd.tsx +60 -63
- package/src/registry/ui/label.tsx +36 -37
- package/src/registry/ui/loading-spinner.tsx +108 -111
- package/src/registry/ui/map.tsx +0 -0
- package/src/registry/ui/marquee.tsx +2 -0
- package/src/registry/ui/menubar.tsx +246 -246
- package/src/registry/ui/meta-pixel.tsx +46 -0
- package/src/registry/ui/microsoft-clarity.tsx +33 -0
- package/src/registry/ui/native-select.tsx +49 -52
- package/src/registry/ui/otp-input.tsx +152 -155
- package/src/registry/ui/pagination.tsx +149 -152
- package/src/registry/ui/patterns.tsx +28 -0
- package/src/registry/ui/popover.tsx +226 -227
- package/src/registry/ui/progress.tsx +51 -52
- package/src/registry/ui/radio.tsx +99 -102
- package/src/registry/ui/resizable.tsx +314 -314
- package/src/registry/ui/scroll-animation.tsx +45 -0
- package/src/registry/ui/scroll-area.tsx +121 -122
- package/src/registry/ui/scroll-to-top.tsx +0 -0
- package/src/registry/ui/search.tsx +147 -150
- package/src/registry/ui/select.tsx +292 -293
- package/src/registry/ui/separator.tsx +46 -47
- package/src/registry/ui/sheet.tsx +6 -3
- package/src/registry/ui/sidebar.tsx +628 -628
- package/src/registry/ui/skeleton.tsx +26 -29
- package/src/registry/ui/slider.tsx +196 -197
- package/src/registry/ui/slot.tsx +69 -72
- package/src/registry/ui/star-rating.tsx +131 -134
- package/src/registry/ui/switch.tsx +72 -73
- package/src/registry/ui/table-of-contents.tsx +96 -96
- package/src/registry/ui/table.tsx +138 -139
- package/src/registry/ui/tabs.tsx +124 -125
- package/src/registry/ui/text.tsx +61 -64
- package/src/registry/ui/textarea.tsx +41 -42
- package/src/registry/ui/theme-switcher.tsx +66 -66
- package/src/registry/ui/tiktok-pixel.tsx +36 -0
- package/src/registry/ui/toast.tsx +97 -98
- package/src/registry/ui/toggle-group.tsx +129 -129
- package/src/registry/ui/toggle.tsx +72 -72
- package/src/registry/ui/tooltip.tsx +143 -144
- package/src/registry/ui/whatsapp.tsx +0 -0
|
@@ -1,134 +1,131 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
export { StarRating }
|
|
134
|
-
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
interface StarRatingProps {
|
|
5
|
+
/** Current rating value */
|
|
6
|
+
value?: number
|
|
7
|
+
/** Max number of stars */
|
|
8
|
+
max?: number
|
|
9
|
+
/** Callback when rating changes */
|
|
10
|
+
onValueChange?: (value: number) => void
|
|
11
|
+
/** Whether rating is readonly */
|
|
12
|
+
readonly?: boolean
|
|
13
|
+
/** Size of stars */
|
|
14
|
+
size?: "sm" | "default" | "lg"
|
|
15
|
+
className?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sizeClasses = {
|
|
19
|
+
sm: "h-4 w-4",
|
|
20
|
+
default: "h-5 w-5",
|
|
21
|
+
lg: "h-6 w-6",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Star rating component
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const [rating, setRating] = useState(0)
|
|
29
|
+
* <StarRating value={rating} onValueChange={setRating} />
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* <StarRating value={4.5} readonly />
|
|
33
|
+
*/
|
|
34
|
+
const StarRating = React.forwardRef<HTMLDivElement, StarRatingProps>(
|
|
35
|
+
(
|
|
36
|
+
{
|
|
37
|
+
value = 0,
|
|
38
|
+
max = 5,
|
|
39
|
+
onValueChange,
|
|
40
|
+
readonly = false,
|
|
41
|
+
size = "default",
|
|
42
|
+
className,
|
|
43
|
+
},
|
|
44
|
+
ref
|
|
45
|
+
) => {
|
|
46
|
+
const [hoverValue, setHoverValue] = React.useState<number | null>(null)
|
|
47
|
+
|
|
48
|
+
const displayValue = hoverValue !== null ? hoverValue : value
|
|
49
|
+
|
|
50
|
+
const handleClick = (starValue: number) => {
|
|
51
|
+
if (!readonly) {
|
|
52
|
+
onValueChange?.(starValue)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const handleKeyDown = (e: React.KeyboardEvent, starValue: number) => {
|
|
57
|
+
if (readonly) return
|
|
58
|
+
|
|
59
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
60
|
+
e.preventDefault()
|
|
61
|
+
onValueChange?.(starValue)
|
|
62
|
+
} else if (e.key === "ArrowRight") {
|
|
63
|
+
e.preventDefault()
|
|
64
|
+
onValueChange?.(Math.min(value + 1, max))
|
|
65
|
+
} else if (e.key === "ArrowLeft") {
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
onValueChange?.(Math.max(value - 1, 0))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
ref={ref}
|
|
74
|
+
role="radiogroup"
|
|
75
|
+
aria-label={`Rating: ${value} out of ${max} stars`}
|
|
76
|
+
className={cn("inline-flex gap-0.5", className)}
|
|
77
|
+
onMouseLeave={() => setHoverValue(null)}
|
|
78
|
+
>
|
|
79
|
+
{Array.from({ length: max }).map((_, index) => {
|
|
80
|
+
const starValue = index + 1
|
|
81
|
+
const isFilled = displayValue >= starValue
|
|
82
|
+
const isHalfFilled = !isFilled && displayValue > index && displayValue < starValue
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<button
|
|
86
|
+
key={index}
|
|
87
|
+
type="button"
|
|
88
|
+
role="radio"
|
|
89
|
+
aria-checked={value >= starValue}
|
|
90
|
+
aria-label={`${starValue} star${starValue > 1 ? "s" : ""}`}
|
|
91
|
+
disabled={readonly}
|
|
92
|
+
tabIndex={readonly ? -1 : starValue === Math.ceil(value) || (value === 0 && starValue === 1) ? 0 : -1}
|
|
93
|
+
className={cn(
|
|
94
|
+
"relative focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded",
|
|
95
|
+
!readonly && "cursor-pointer hover:scale-110 transition-transform"
|
|
96
|
+
)}
|
|
97
|
+
onClick={() => handleClick(starValue)}
|
|
98
|
+
onMouseEnter={() => !readonly && setHoverValue(starValue)}
|
|
99
|
+
onKeyDown={(e) => handleKeyDown(e, starValue)}
|
|
100
|
+
>
|
|
101
|
+
{/* Empty star */}
|
|
102
|
+
<svg
|
|
103
|
+
className={cn(sizeClasses[size], "text-muted-foreground/30")}
|
|
104
|
+
fill="currentColor"
|
|
105
|
+
viewBox="0 0 24 24"
|
|
106
|
+
>
|
|
107
|
+
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
|
108
|
+
</svg>
|
|
109
|
+
|
|
110
|
+
{/* Filled star overlay */}
|
|
111
|
+
<svg
|
|
112
|
+
className={cn(
|
|
113
|
+
sizeClasses[size],
|
|
114
|
+
"absolute inset-0 text-yellow-400 transition-opacity",
|
|
115
|
+
isFilled ? "opacity-100" : isHalfFilled ? "opacity-50" : "opacity-0"
|
|
116
|
+
)}
|
|
117
|
+
fill="currentColor"
|
|
118
|
+
viewBox="0 0 24 24"
|
|
119
|
+
>
|
|
120
|
+
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
|
121
|
+
</svg>
|
|
122
|
+
</button>
|
|
123
|
+
)
|
|
124
|
+
})}
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
StarRating.displayName = "StarRating"
|
|
130
|
+
|
|
131
|
+
export { StarRating }
|
|
@@ -1,73 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { cn } from "@/lib/utils"
|
|
5
|
-
|
|
6
|
-
interface SwitchProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> {
|
|
7
|
-
/**
|
|
8
|
-
* Whether the switch is on
|
|
9
|
-
*/
|
|
10
|
-
checked?: boolean
|
|
11
|
-
/**
|
|
12
|
-
* Callback when the switch state changes
|
|
13
|
-
*/
|
|
14
|
-
onCheckedChange?: (checked: boolean) => void
|
|
15
|
-
/**
|
|
16
|
-
* Whether the switch is disabled
|
|
17
|
-
*/
|
|
18
|
-
disabled?: boolean
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Switch/Toggle component with keyboard accessibility
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* const [enabled, setEnabled] = useState(false)
|
|
26
|
-
* <Switch checked={enabled} onCheckedChange={setEnabled} />
|
|
27
|
-
*/
|
|
28
|
-
const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
|
|
29
|
-
({ className, checked = false, onCheckedChange, disabled, ...props }, ref) => {
|
|
30
|
-
const handleClick = () => {
|
|
31
|
-
if (!disabled) {
|
|
32
|
-
onCheckedChange?.(!checked)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
37
|
-
if (e.key === " " || e.key === "Enter") {
|
|
38
|
-
e.preventDefault()
|
|
39
|
-
handleClick()
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<button
|
|
45
|
-
type="button"
|
|
46
|
-
role="switch"
|
|
47
|
-
aria-checked={checked}
|
|
48
|
-
aria-disabled={disabled}
|
|
49
|
-
disabled={disabled}
|
|
50
|
-
ref={ref}
|
|
51
|
-
className={cn(
|
|
52
|
-
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
|
|
53
|
-
checked ? "bg-primary" : "bg-input",
|
|
54
|
-
className
|
|
55
|
-
)}
|
|
56
|
-
onClick={handleClick}
|
|
57
|
-
onKeyDown={handleKeyDown}
|
|
58
|
-
{...props}
|
|
59
|
-
>
|
|
60
|
-
<span
|
|
61
|
-
className={cn(
|
|
62
|
-
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform",
|
|
63
|
-
checked ? "translate-x-4" : "translate-x-0"
|
|
64
|
-
)}
|
|
65
|
-
/>
|
|
66
|
-
</button>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
)
|
|
70
|
-
Switch.displayName = "Switch"
|
|
71
|
-
|
|
72
|
-
export { Switch }
|
|
73
|
-
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
interface SwitchProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> {
|
|
7
|
+
/**
|
|
8
|
+
* Whether the switch is on
|
|
9
|
+
*/
|
|
10
|
+
checked?: boolean
|
|
11
|
+
/**
|
|
12
|
+
* Callback when the switch state changes
|
|
13
|
+
*/
|
|
14
|
+
onCheckedChange?: (checked: boolean) => void
|
|
15
|
+
/**
|
|
16
|
+
* Whether the switch is disabled
|
|
17
|
+
*/
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Switch/Toggle component with keyboard accessibility
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const [enabled, setEnabled] = useState(false)
|
|
26
|
+
* <Switch checked={enabled} onCheckedChange={setEnabled} />
|
|
27
|
+
*/
|
|
28
|
+
const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
|
|
29
|
+
({ className, checked = false, onCheckedChange, disabled, ...props }, ref) => {
|
|
30
|
+
const handleClick = () => {
|
|
31
|
+
if (!disabled) {
|
|
32
|
+
onCheckedChange?.(!checked)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
37
|
+
if (e.key === " " || e.key === "Enter") {
|
|
38
|
+
e.preventDefault()
|
|
39
|
+
handleClick()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
role="switch"
|
|
47
|
+
aria-checked={checked}
|
|
48
|
+
aria-disabled={disabled}
|
|
49
|
+
disabled={disabled}
|
|
50
|
+
ref={ref}
|
|
51
|
+
className={cn(
|
|
52
|
+
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
|
|
53
|
+
checked ? "bg-primary" : "bg-input",
|
|
54
|
+
className
|
|
55
|
+
)}
|
|
56
|
+
onClick={handleClick}
|
|
57
|
+
onKeyDown={handleKeyDown}
|
|
58
|
+
{...props}
|
|
59
|
+
>
|
|
60
|
+
<span
|
|
61
|
+
className={cn(
|
|
62
|
+
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform",
|
|
63
|
+
checked ? "translate-x-4" : "translate-x-0"
|
|
64
|
+
)}
|
|
65
|
+
/>
|
|
66
|
+
</button>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
Switch.displayName = "Switch"
|
|
71
|
+
|
|
72
|
+
export { Switch }
|
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { cn } from "@/lib/utils"
|
|
5
|
-
|
|
6
|
-
export interface HeadingInfo {
|
|
7
|
-
id: string
|
|
8
|
-
text: string
|
|
9
|
-
level: number
|
|
10
|
-
children?: HeadingInfo[]
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface TableOfContentsProps {
|
|
14
|
-
headings?: HeadingInfo[]
|
|
15
|
-
activeSection?: string | null
|
|
16
|
-
className?: string
|
|
17
|
-
onSectionClick?: (id: string) => void
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function TableOfContents({
|
|
21
|
-
headings = [],
|
|
22
|
-
activeSection,
|
|
23
|
-
className,
|
|
24
|
-
onSectionClick,
|
|
25
|
-
}: TableOfContentsProps) {
|
|
26
|
-
|
|
27
|
-
const scrollToHeading = React.useCallback((headingId: string) => {
|
|
28
|
-
if (onSectionClick) {
|
|
29
|
-
onSectionClick(headingId)
|
|
30
|
-
return
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const element = document.getElementById(headingId)
|
|
34
|
-
if (element) {
|
|
35
|
-
const offset = 80
|
|
36
|
-
const elementPosition =
|
|
37
|
-
element.getBoundingClientRect().top + window.pageYOffset
|
|
38
|
-
window.scrollTo({
|
|
39
|
-
top: elementPosition - offset,
|
|
40
|
-
behavior: "smooth",
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
}, [onSectionClick])
|
|
44
|
-
|
|
45
|
-
// Flatten all headings without hierarchy for simplicity
|
|
46
|
-
const flattenedHeadings = React.useMemo(() => {
|
|
47
|
-
const flatten = (headings: HeadingInfo[]): HeadingInfo[] => {
|
|
48
|
-
return headings.flatMap((heading) => [
|
|
49
|
-
heading,
|
|
50
|
-
...(heading.children ? flatten(heading.children) : []),
|
|
51
|
-
])
|
|
52
|
-
}
|
|
53
|
-
return flatten(headings)
|
|
54
|
-
}, [headings])
|
|
55
|
-
|
|
56
|
-
if (flattenedHeadings.length === 0) return null
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div
|
|
60
|
-
className={cn(
|
|
61
|
-
"hidden lg:block sticky top-16 right-0 h-[calc(100vh-4rem)] w-full bg-background justify-self-end lg:max-w-64",
|
|
62
|
-
className
|
|
63
|
-
)}
|
|
64
|
-
>
|
|
65
|
-
<div className="flex flex-col h-full">
|
|
66
|
-
<div className="flex items-center justify-between p-4">
|
|
67
|
-
<span className="font-medium text-foreground text-sm">
|
|
68
|
-
On this page
|
|
69
|
-
</span>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
<nav className="flex-1 overflow-y-auto p-4 scrollbar-thin">
|
|
73
|
-
<div className="space-y-1">
|
|
74
|
-
{flattenedHeadings.map((heading) => (
|
|
75
|
-
<button
|
|
76
|
-
key={heading.id}
|
|
77
|
-
onClick={() => scrollToHeading(heading.id)}
|
|
78
|
-
className={cn(
|
|
79
|
-
"w-full text-left px-2 py-1.5 rounded text-xs transition-colors duration-200",
|
|
80
|
-
activeSection === heading.id
|
|
81
|
-
? "text-primary bg-accent font-medium"
|
|
82
|
-
: "text-muted-foreground hover:text-foreground",
|
|
83
|
-
)}
|
|
84
|
-
style={{
|
|
85
|
-
paddingLeft: `${(heading.level - 1) * 12 + 8}px`,
|
|
86
|
-
}}
|
|
87
|
-
>
|
|
88
|
-
{heading.text}
|
|
89
|
-
</button>
|
|
90
|
-
))}
|
|
91
|
-
</div>
|
|
92
|
-
</nav>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
)
|
|
96
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
export interface HeadingInfo {
|
|
7
|
+
id: string
|
|
8
|
+
text: string
|
|
9
|
+
level: number
|
|
10
|
+
children?: HeadingInfo[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TableOfContentsProps {
|
|
14
|
+
headings?: HeadingInfo[]
|
|
15
|
+
activeSection?: string | null
|
|
16
|
+
className?: string
|
|
17
|
+
onSectionClick?: (id: string) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function TableOfContents({
|
|
21
|
+
headings = [],
|
|
22
|
+
activeSection,
|
|
23
|
+
className,
|
|
24
|
+
onSectionClick,
|
|
25
|
+
}: TableOfContentsProps) {
|
|
26
|
+
|
|
27
|
+
const scrollToHeading = React.useCallback((headingId: string) => {
|
|
28
|
+
if (onSectionClick) {
|
|
29
|
+
onSectionClick(headingId)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const element = document.getElementById(headingId)
|
|
34
|
+
if (element) {
|
|
35
|
+
const offset = 80
|
|
36
|
+
const elementPosition =
|
|
37
|
+
element.getBoundingClientRect().top + window.pageYOffset
|
|
38
|
+
window.scrollTo({
|
|
39
|
+
top: elementPosition - offset,
|
|
40
|
+
behavior: "smooth",
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
}, [onSectionClick])
|
|
44
|
+
|
|
45
|
+
// Flatten all headings without hierarchy for simplicity
|
|
46
|
+
const flattenedHeadings = React.useMemo(() => {
|
|
47
|
+
const flatten = (headings: HeadingInfo[]): HeadingInfo[] => {
|
|
48
|
+
return headings.flatMap((heading) => [
|
|
49
|
+
heading,
|
|
50
|
+
...(heading.children ? flatten(heading.children) : []),
|
|
51
|
+
])
|
|
52
|
+
}
|
|
53
|
+
return flatten(headings)
|
|
54
|
+
}, [headings])
|
|
55
|
+
|
|
56
|
+
if (flattenedHeadings.length === 0) return null
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
className={cn(
|
|
61
|
+
"hidden lg:block sticky top-16 right-0 h-[calc(100vh-4rem)] w-full bg-background justify-self-end lg:max-w-64",
|
|
62
|
+
className
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<div className="flex flex-col h-full">
|
|
66
|
+
<div className="flex items-center justify-between p-4">
|
|
67
|
+
<span className="font-medium text-foreground text-sm">
|
|
68
|
+
On this page
|
|
69
|
+
</span>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<nav className="flex-1 overflow-y-auto p-4 scrollbar-thin">
|
|
73
|
+
<div className="space-y-1">
|
|
74
|
+
{flattenedHeadings.map((heading) => (
|
|
75
|
+
<button
|
|
76
|
+
key={heading.id}
|
|
77
|
+
onClick={() => scrollToHeading(heading.id)}
|
|
78
|
+
className={cn(
|
|
79
|
+
"w-full text-left px-2 py-1.5 rounded text-xs transition-colors duration-200",
|
|
80
|
+
activeSection === heading.id
|
|
81
|
+
? "text-primary bg-accent font-medium"
|
|
82
|
+
: "text-muted-foreground hover:text-foreground",
|
|
83
|
+
)}
|
|
84
|
+
style={{
|
|
85
|
+
paddingLeft: `${(heading.level - 1) * 12 + 8}px`,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{heading.text}
|
|
89
|
+
</button>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
</nav>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|