mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 06:39:09 +03:00
* test fuma toc * make toc work * ok toc * ui-patterns toc * fix types * cleanup * fix overflow issues * cleanup * fix loading w * Update apps/docs/components/GuidesTableOfContents.tsx * fix prettier * format * install deps * target ES2018 * tweak regex * update docs test snapshots * fix useSubscribeTocRerender
152 lines
4.0 KiB
TypeScript
152 lines
4.0 KiB
TypeScript
'use client'
|
|
|
|
import type { TOCItemType } from './server/get-toc'
|
|
import * as Primitive from './toc-primitive'
|
|
import { type ComponentProps, Fragment, type HTMLAttributes, type ReactNode, useRef } from 'react'
|
|
import { TocThumb } from './toc-thumb'
|
|
import { cn, ScrollArea, ScrollViewport } from 'ui'
|
|
import { removeAnchor } from 'ui/src/components/CustomHTMLElements/CustomHTMLElements.utils'
|
|
|
|
export interface TOCProps {
|
|
/**
|
|
* Custom content in TOC container, before the main TOC
|
|
*/
|
|
header?: ReactNode
|
|
/**
|
|
* Custom content in TOC container, after the main TOC
|
|
*/
|
|
footer?: ReactNode
|
|
children: ReactNode
|
|
}
|
|
|
|
export function Toc(props: HTMLAttributes<HTMLDivElement>) {
|
|
return (
|
|
<div
|
|
id="toc"
|
|
{...props}
|
|
className={cn('sticky top-[--header-height] h-fit max-md:hidden', props.className)}
|
|
style={
|
|
{
|
|
...props.style,
|
|
} as object
|
|
}
|
|
>
|
|
<div className="flex h-fit w-[--toc-width] max-w-full flex-col gap-3 pe-4">
|
|
{props.children}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function TOCScrollArea({
|
|
isMenu,
|
|
...props
|
|
}: ComponentProps<typeof ScrollArea> & { isMenu?: boolean }) {
|
|
const viewRef = useRef<HTMLDivElement>(null)
|
|
|
|
return (
|
|
<ScrollArea {...props} className={cn('flex flex-col ps-px', props.className)}>
|
|
<Primitive.ScrollProvider containerRef={viewRef}>
|
|
<ScrollViewport
|
|
className={cn('relative min-h-0 text-sm', isMenu && 'mt-2 mb-4 mx-4 md:mx-6')}
|
|
ref={viewRef}
|
|
>
|
|
{props.children}
|
|
</ScrollViewport>
|
|
</Primitive.ScrollProvider>
|
|
</ScrollArea>
|
|
)
|
|
}
|
|
|
|
export function TOCItems({
|
|
items,
|
|
showTrack = false,
|
|
}: {
|
|
items: TOCItemType[]
|
|
showTrack?: boolean
|
|
}) {
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
|
|
if (items.length === 0) return null
|
|
|
|
return (
|
|
<>
|
|
<TocThumb
|
|
containerRef={containerRef}
|
|
className="absolute start-0 mt-[--toc-top] h-[--toc-height] w-px bg-foreground transition-all"
|
|
/>
|
|
<div
|
|
ref={containerRef}
|
|
className={cn(
|
|
'list-none text-[0.8rem] flex flex-col pl-[calc(0.75rem+5px)] border-foreground/10',
|
|
showTrack && 'border-s'
|
|
)}
|
|
>
|
|
{items.map((item) => (
|
|
<TOCItem key={item.url} item={item} />
|
|
))}
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const formatSlug = (slug: string) => {
|
|
// [Joshen] We will still provide support for headers declared like this:
|
|
// ## REST API {#rest-api-overview}
|
|
// At least for now, this was a docusaurus thing.
|
|
if (slug.includes('#')) return slug.split('#')[1]
|
|
return slug
|
|
}
|
|
|
|
function formatTOCHeader(content: string) {
|
|
let insideInlineCode = false
|
|
const res: Array<{ type: 'text'; value: string } | { type: 'code'; value: string }> = []
|
|
|
|
for (const x of content) {
|
|
if (x === '`') {
|
|
if (!insideInlineCode) {
|
|
insideInlineCode = true
|
|
res.push({ type: 'code', value: '' })
|
|
} else {
|
|
insideInlineCode = false
|
|
}
|
|
} else {
|
|
if (insideInlineCode) {
|
|
res[res.length - 1].value += x
|
|
} else {
|
|
if (res.length === 0 || res[res.length - 1].type === 'code') {
|
|
res.push({ type: 'text', value: x })
|
|
} else {
|
|
res[res.length - 1].value += x
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
function TOCItem({ item }: { item: TOCItemType }) {
|
|
return (
|
|
<Primitive.TOCItem
|
|
href={`#${formatSlug(item.url)}`}
|
|
className={cn(
|
|
'text-foreground-lighter hover:text-brand-link transition-colors py-1 [overflow-wrap:anywhere] first:pt-0 last:pb-0 data-[active=true]:text-foreground',
|
|
item.depth <= 2 && 'ps-3',
|
|
item.depth === 3 && 'ps-6',
|
|
item.depth >= 4 && 'ps-8'
|
|
)}
|
|
>
|
|
{formatTOCHeader(removeAnchor(item.title)).map((x, index) => (
|
|
<Fragment key={index}>
|
|
{x.type === 'code' ? (
|
|
<code className="text-xs border rounded bg-muted">{x.value}</code>
|
|
) : (
|
|
x.value
|
|
)}
|
|
</Fragment>
|
|
))}
|
|
</Primitive.TOCItem>
|
|
)
|
|
}
|