126 lines
No EOL
3.8 KiB
TypeScript
126 lines
No EOL
3.8 KiB
TypeScript
'use client'
|
|
|
|
import { colors, effects } from '@/lib/theme'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface TypeLinkProps {
|
|
type: string
|
|
className?: string
|
|
availableTypeIds?: Set<string>
|
|
}
|
|
|
|
/**
|
|
* Parses a type string and converts type references into clickable links
|
|
* that scroll to the corresponding type definition in the documentation.
|
|
*
|
|
* Supports:
|
|
* - Simple types: Domain, User, etc.
|
|
* - Generic types: Array<Domain>, Promise<User>
|
|
* - Union types: string | number
|
|
* - Complex types: Record<string, Domain>
|
|
*/
|
|
export default function TypeLink({ type, className, availableTypeIds }: TypeLinkProps) {
|
|
const parseTypeString = (typeStr: string): React.ReactNode[] => {
|
|
const parts: React.ReactNode[] = []
|
|
let currentIndex = 0
|
|
|
|
const typeNamePattern = /\b([A-Z][a-zA-Z0-9]*)\b/g
|
|
const builtInTypes = new Set([
|
|
'string', 'number', 'boolean', 'void', 'null', 'undefined', 'any', 'unknown',
|
|
'never', 'object', 'symbol', 'bigint', 'Array', 'Promise', 'Record', 'Partial',
|
|
'Required', 'Readonly', 'Pick', 'Omit', 'Exclude', 'Extract', 'NonNullable',
|
|
'ReturnType', 'InstanceType', 'ThisType', 'Parameters', 'ConstructorParameters',
|
|
'Date', 'Error', 'RegExp', 'Map', 'Set', 'WeakMap', 'WeakSet', 'Function',
|
|
'ReadonlyArray', 'String', 'Number', 'Boolean', 'Symbol', 'Object'
|
|
])
|
|
|
|
let match: RegExpExecArray | null
|
|
|
|
while ((match = typeNamePattern.exec(typeStr)) !== null) {
|
|
const typeName = match[1]
|
|
const matchStart = match.index
|
|
const matchEnd = typeNamePattern.lastIndex
|
|
|
|
if (matchStart > currentIndex) {
|
|
parts.push(
|
|
<span key={`text-${currentIndex}`}>
|
|
{typeStr.substring(currentIndex, matchStart)}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
if (builtInTypes.has(typeName)) {
|
|
parts.push(
|
|
<span key={`builtin-${matchStart}`}>
|
|
{typeName}
|
|
</span>
|
|
)
|
|
} else {
|
|
// Check if this type exists in the documentation
|
|
const typeExists = availableTypeIds?.has(typeName) ?? false
|
|
|
|
if (typeExists) {
|
|
parts.push(
|
|
<button
|
|
key={`link-${matchStart}`}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
const targetId = typeName
|
|
const element = document.getElementById(targetId)
|
|
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
|
|
element.classList.add('ring-2', 'ring-blue-400', 'ring-offset-2', 'ring-offset-gray-900')
|
|
setTimeout(() => {
|
|
element.classList.remove('ring-2', 'ring-blue-400', 'ring-offset-2', 'ring-offset-gray-900')
|
|
}, 2000)
|
|
}
|
|
}}
|
|
className={cn(
|
|
'hover:underline cursor-pointer',
|
|
effects.transitions.colors
|
|
)}
|
|
style={{
|
|
color: colors.accents.link,
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.color = colors.accents.linkHover
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.color = colors.accents.link
|
|
}}
|
|
>
|
|
{typeName}
|
|
</button>
|
|
)
|
|
} else {
|
|
// Type doesn't exist in docs, render as plain text
|
|
parts.push(
|
|
<span key={`text-${matchStart}`}>
|
|
{typeName}
|
|
</span>
|
|
)
|
|
}
|
|
}
|
|
|
|
currentIndex = matchEnd
|
|
}
|
|
|
|
if (currentIndex < typeStr.length) {
|
|
parts.push(
|
|
<span key={`text-${currentIndex}`}>
|
|
{typeStr.substring(currentIndex)}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
return parts
|
|
}
|
|
|
|
return (
|
|
<span className={cn('font-mono', className)}>
|
|
{parseTypeString(type)}
|
|
</span>
|
|
)
|
|
} |