diff --git a/apps/documentation/src/components/MdxProvider/MdxTitle/MdxTitle.tsx b/apps/documentation/src/components/MdxProvider/MdxTitle/MdxTitle.tsx
index 6cf75d9a..0f5c040c 100644
--- a/apps/documentation/src/components/MdxProvider/MdxTitle/MdxTitle.tsx
+++ b/apps/documentation/src/components/MdxProvider/MdxTitle/MdxTitle.tsx
@@ -1,4 +1,4 @@
-import type { JSX } from 'react'
+import type { ElementType, JSX, ReactNode } from 'react'
import { Title, type TitleProps } from '@mantine/core'
export default function MdxTitle(props: TitleProps): JSX.Element {
@@ -6,19 +6,39 @@ export default function MdxTitle(props: TitleProps): JSX.Element {
)
}
-export const h = (
- order: 1 | 2 | 3 | 4 | 5 | 6,
-): React.ElementType => {
+function idGen(children: ReactNode): string {
+ if (Array.isArray(children)) {
+ const result = children
+ .map((child) => {
+ if (typeof child === 'string') {
+ return child
+ }
+ if (typeof child === 'object' && child !== null && 'props' in child) {
+ const childWithProps = child as { props?: { children?: ReactNode } }
+ return typeof childWithProps.props?.children === 'string'
+ ? childWithProps.props.children
+ : ''
+ }
+ return ''
+ })
+ .join('')
+
+ return result.toLowerCase().replace(/\s+/g, '-')
+ }
+
+ return typeof children === 'string'
+ ? children.toLowerCase().replace(/\s+/g, '-')
+ : ''
+}
+
+export const h = (order: 1 | 2 | 3 | 4 | 5 | 6): ElementType => {
function render(props: TitleProps): JSX.Element {
return
}
diff --git a/apps/documentation/src/components/MdxWrapper/MdxWrapper.tsx b/apps/documentation/src/components/MdxWrapper/MdxWrapper.tsx
new file mode 100644
index 00000000..c5175047
--- /dev/null
+++ b/apps/documentation/src/components/MdxWrapper/MdxWrapper.tsx
@@ -0,0 +1,25 @@
+import type { JSX, ReactNode } from 'react'
+import { Box, Container } from '@mantine/core'
+
+import TableOfContents from '@/components/TableOfContents'
+
+import EditPage from '../EditPage'
+import MdxProvider from '../MdxProvider'
+
+interface MdxWrapperProps {
+ children: ReactNode
+}
+
+export function MdxWrapper({ children }: MdxWrapperProps): JSX.Element {
+ return (
+
+
+ {children}
+
+
+
+
+
+
+ )
+}
diff --git a/apps/documentation/src/components/MdxWrapper/index.ts b/apps/documentation/src/components/MdxWrapper/index.ts
new file mode 100644
index 00000000..76ac0e44
--- /dev/null
+++ b/apps/documentation/src/components/MdxWrapper/index.ts
@@ -0,0 +1,3 @@
+import { MdxWrapper } from './MdxWrapper'
+
+export default MdxWrapper
diff --git a/apps/documentation/src/components/TableOfContents/TableOfContents.module.css b/apps/documentation/src/components/TableOfContents/TableOfContents.module.css
index 80f23130..86212299 100644
--- a/apps/documentation/src/components/TableOfContents/TableOfContents.module.css
+++ b/apps/documentation/src/components/TableOfContents/TableOfContents.module.css
@@ -1,47 +1,33 @@
-/*
- * Component inspired by: https://github.com/mantinedev/mantine/tree/master/apps/mantine.dev/src/components/TableOfContents
- */
.wrapper {
- padding-left: var(--mantine-spacing-md);
position: sticky;
top: var(--mantine-spacing-xl);
- right: 0;
padding-top: 55px;
- flex: 0 0 calc(var(--docs-table-of-contents-width) - 20px);
- width: 200px;
+ flex: 0 0 calc(250px - 20px);
+ min-width: 220px;
+ max-width: 220px;
+ height: 100vh;
- @mixin rtl {
- padding-left: 0;
- padding-right: var(--mantine-spacing-md);
- right: auto;
- left: 0;
- }
-
- &[data-with-tabs] {
- padding-top: 0;
- top: calc(var(--mantine-spacing-xl) + 60px);
+ /* var(--mantine-breakpoint-md) = 62em */
+ @media (max-width: 62em) {
+ display: none;
}
}
.inner {
- padding-bottom: var(--mantine-spacing-xl);
- padding-left: var(--mantine-spacing-md);
display: flex;
flex-direction: column;
justify-content: space-between;
-
- @mixin rtl {
- padding-left: 0;
- padding-right: var(--mantine-spacing-md);
- }
}
.link {
+ white-space: normal;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
display: block;
border-left: 1px solid transparent;
padding: 8px var(--mantine-spacing-md);
margin-left: -1px;
- padding-left: calc(var(--toc-link-offset) * var(--mantine-spacing-lg));
+ padding-left: calc(1 * var(--mantine-spacing-lg));
border-top-right-radius: var(--mantine-radius-sm);
border-bottom-right-radius: var(--mantine-radius-sm);
@@ -58,10 +44,19 @@
border-right: 1px solid transparent;
margin-left: 0;
margin-right: -1px;
- border-top-left-radius: var(--mantine-radius-sm);
- border-bottom-left-radius: var(--mantine-radius-sm);
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
+ border-radius: var(--mantine-radius-sm) 0 0 var(--mantine-radius-sm);
+ }
+
+ &:hover {
+ border-color: var(--mantine-color-violet-1);
+
+ @mixin light {
+ color: var(--mantine-color-black);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-violet-1);
+ }
}
&[data-active] {
@@ -69,22 +64,14 @@
@mixin light {
color: var(--mantine-color-violet-8);
- background-color: var(--mantine-color-violet-0);
}
@mixin dark {
color: var(--mantine-color-violet-1);
- background-color: #534a6d;
}
}
}
-.header {
- display: flex;
- align-items: center;
- margin-bottom: var(--mantine-spacing-md);
-}
-
.title {
margin-left: var(--mantine-spacing-md);
diff --git a/apps/documentation/src/components/TableOfContents/TableOfContents.tsx b/apps/documentation/src/components/TableOfContents/TableOfContents.tsx
index 57bf3229..09f6b23c 100644
--- a/apps/documentation/src/components/TableOfContents/TableOfContents.tsx
+++ b/apps/documentation/src/components/TableOfContents/TableOfContents.tsx
@@ -1,115 +1,117 @@
-/*
- * Component inspired by: https://github.com/mantinedev/mantine/tree/master/apps/mantine.dev/src/components/TableOfContents
- */
-import type { JSX } from 'react'
-import { useEffect, useRef, useState } from 'react'
-import { useRouter, Link } from 'tuono'
-import { IconList } from '@tabler/icons-react'
-import { Box, rem, ScrollArea, Text } from '@mantine/core'
+import type { JSX, MouseEvent } from 'react'
+import { useRef, useState, useEffect } from 'react'
+import { useRouter } from 'tuono'
+import { Box, Text } from '@mantine/core'
import { getHeadings, type Heading } from './getHeadings'
import classes from './TableOfContents.module.css'
-function getActiveElement(rects: Array): number {
- if (rects.length === 0) {
- return -1
- }
-
- const closest = rects.reduce(
- (acc, item, index) => {
- if (Math.abs(acc.position) < Math.abs(item.y)) {
- return acc
- }
-
- return {
- index,
- position: item.y,
- }
- },
- { index: 0, position: rects[0].y },
+export function TableOfContents(): JSX.Element | null {
+ const [activeHeadingIndex, setActiveHeadingIndex] = useState(
+ null,
)
-
- return closest.index
-}
-
-interface TableOfContentsProps {
- withTabs: boolean
-}
-
-export function TableOfContents({
- withTabs,
-}: TableOfContentsProps): JSX.Element | null {
- const [active, setActive] = useState(0)
const [headings, setHeadings] = useState>([])
- const headingsRef = useRef>([])
+ const headingsRef = useRef>([])
+ const observerRef = useRef(null)
const router = useRouter()
- const filteredHeadings = headings.filter((heading) => heading.depth > 1)
-
- const handleScroll = (): void => {
- setActive(
- getActiveElement(
- headingsRef.current.map((d) => d.getNode().getBoundingClientRect()),
- ),
- )
- }
-
useEffect(() => {
const _headings = getHeadings()
- headingsRef.current = _headings
setHeadings(_headings)
- setActive(
- getActiveElement(
- _headings.map((d) => d.getNode().getBoundingClientRect()),
- ),
+ headingsRef.current = _headings.map((heading) => heading.getNode())
+
+ if (observerRef.current) {
+ observerRef.current.disconnect()
+ }
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ const visibleEntries = entries
+ .filter((entry) => entry.isIntersecting)
+ .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)
+
+ if (visibleEntries.length > 0) {
+ setActiveHeadingIndex(
+ _headings.findIndex((h) => h.id === visibleEntries[0].target.id),
+ )
+ }
+ },
+ {
+ rootMargin: '-50px 0px -80% 0px',
+ threshold: [0.1, 0.5, 1.0],
+ },
)
- window.addEventListener('scroll', handleScroll)
+
+ headingsRef.current.forEach((node) => {
+ observer.observe(node)
+ })
+ observerRef.current = observer
+
+ const handleHashChange = (): void => {
+ setTimeout(() => {
+ observerRef.current?.disconnect()
+ observerRef.current = observer
+ headingsRef.current.forEach((node) => {
+ observer.observe(node)
+ })
+ }, 300)
+ }
+
+ window.addEventListener('hashchange', handleHashChange)
+
return (): void => {
- window.removeEventListener('scroll', handleScroll)
+ observer.disconnect()
+ window.removeEventListener('hashchange', handleHashChange)
}
}, [router.pathname])
- if (filteredHeadings.length === 0) {
+ const handleHeadingClick = (
+ event: MouseEvent,
+ id: string,
+ ): void => {
+ event.preventDefault()
+ const element = document.getElementById(id)
+ if (element) {
+ element.scrollIntoView({
+ behavior: 'instant',
+ block: 'start',
+ })
+ }
+ }
+
+ // Avoid to show it in case of a TODO page
+ if (headings.length === 1) {
return null
}
- const items = filteredHeadings.map((heading, index) => (
-
- {heading.content}
-
- ))
-
return (
-
+
-
-
-
Table of contents
+
+ On this page
+
+
+ {headings.slice(1).map((heading, index) => (
+ {
+ handleHeadingClick(e, heading.id)
+ }}
+ __vars={{ '--toc-link-offset': `${heading.depth - 1}` }}
+ >
+ {heading.content}
+
+ ))}
-
- {items}
-
diff --git a/apps/documentation/src/components/TableOfContents/getHeadings.ts b/apps/documentation/src/components/TableOfContents/getHeadings.ts
index acee9300..54ea9aab 100644
--- a/apps/documentation/src/components/TableOfContents/getHeadings.ts
+++ b/apps/documentation/src/components/TableOfContents/getHeadings.ts
@@ -8,14 +8,27 @@ export interface Heading {
getNode: () => HTMLHeadingElement
}
+function getCleanedText(element: HTMLElement): string {
+ const clone = element.cloneNode(true) as HTMLElement
+
+ clone.querySelectorAll('code, pre').forEach((codeBlock) => {
+ const textNode = document.createTextNode(codeBlock.textContent || '')
+ codeBlock.replaceWith(textNode)
+ })
+
+ return clone.textContent?.trim() || ''
+}
+
function getHeadingsData(headings: Array
): Array {
const result: Array = []
for (const heading of headings) {
- if (heading.id) {
+ const depth = parseInt(heading.getAttribute('data-order') || '1', 10)
+
+ if (depth <= 3 && heading.id) {
result.push({
- depth: parseInt(heading.getAttribute('data-order') || '1', 10),
- content: heading.getAttribute('data-heading') || '',
+ depth,
+ content: getCleanedText(heading),
id: heading.id,
getNode: () =>
document.getElementById(heading.id) as HTMLHeadingElement,
@@ -28,7 +41,6 @@ function getHeadingsData(headings: Array): Array {
export function getHeadings(): Array {
const root = document.getElementById('mdx-root')
- console.log(root)
if (!root) {
return []
diff --git a/apps/documentation/src/routes/__layout.tsx b/apps/documentation/src/routes/__layout.tsx
index 84191ce0..e0892f57 100644
--- a/apps/documentation/src/routes/__layout.tsx
+++ b/apps/documentation/src/routes/__layout.tsx
@@ -6,14 +6,12 @@ import {
createTheme,
MantineProvider,
AppShell,
- Container,
mantineHtmlProps,
type CSSVariablesResolver,
} from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
-import EditPage from '@/components/EditPage'
-import MdxProvider from '@/components/MdxProvider'
+import MdxWrapper from '@/components/MdxWrapper'
import Navbar from '@/components/Navbar'
import Sidebar from '@/components/Sidebar'
@@ -38,8 +36,8 @@ const theme = createTheme({
},
fontSizes: {
// 'xs' | 'sm' | 'md' | 'lg' | 'xl'
- xs: '16px',
- sm: '16px',
+ xs: '14px',
+ sm: '14px',
},
colors: {
dark: [
@@ -125,11 +123,8 @@ export default function RootRoute({ children }: RootRouteProps): JSX.Element {
>
-
-
- {children}
-
-
+
+ {children}