Improve layout
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -7,9 +7,7 @@ export default async function Page({
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
|
||||
const { default: Component } = await import(
|
||||
`@/content/components/${slug}.mdx`
|
||||
);
|
||||
const { default: Component } = await import(`@/content/${slug}.mdx`);
|
||||
|
||||
return <Component />;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,28 @@
|
||||
padding-inline: 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
.contentWrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.contentInner {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.toc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -18,3 +36,11 @@
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.toc {
|
||||
display: block;
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Toolbar } from '@/components/Toolbar';
|
||||
import { StickyHeader } from '../components/StickyHeader/StickyHeader';
|
||||
import { Providers } from './providers';
|
||||
import { CustomTheme } from '@/components/CustomTheme';
|
||||
import { TableOfContents } from '@/components/TableOfContents';
|
||||
import styles from './layout.module.css';
|
||||
|
||||
import '../css/globals.css';
|
||||
@@ -53,9 +54,14 @@ export default async function RootLayout({
|
||||
<Sidebar />
|
||||
{/* <StickyHeader /> */}
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.contentWrapper}>
|
||||
<Toolbar version={packageVersion} />
|
||||
{children}
|
||||
<div className={styles.content}>
|
||||
<div className={styles.contentInner}>{children}</div>
|
||||
<aside className={styles.toc}>
|
||||
<TableOfContents />
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CustomTheme />
|
||||
|
||||
@@ -9,15 +9,24 @@ export interface CodeBlockProps {
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export async function CodeBlock({ lang = 'tsx', title, code }: CodeBlockProps) {
|
||||
const out = await codeToHtml(code || '', {
|
||||
lang: lang,
|
||||
export async function CodeBlock(props: CodeBlockProps) {
|
||||
const { lang = 'tsx', title, code } = props;
|
||||
let out = await codeToHtml(code || '', {
|
||||
lang,
|
||||
transformers: [transformerNotationDiff({ matchAlgorithm: 'v3' })],
|
||||
themes: {
|
||||
light: 'min-light',
|
||||
light: 'github-dark',
|
||||
dark: 'min-dark',
|
||||
},
|
||||
transformers: [transformerNotationDiff({ matchAlgorithm: 'v3' })],
|
||||
});
|
||||
|
||||
// Remove background-color from the pre tag to use our theme colors
|
||||
out = out.replace(
|
||||
/style="([^"]*?)background-color:[^;]+;?([^"]*?)"/g,
|
||||
'style="$1$2"',
|
||||
);
|
||||
// Clean up empty style attributes
|
||||
out = out.replace(/style=""\s?/g, '');
|
||||
|
||||
return <CodeBlockClient out={out} title={title} />;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
.codeBlock {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
overflow-x: auto;
|
||||
font-family: var(--font-mono);
|
||||
background-color: #fff;
|
||||
background-color: var(--code-bg);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
[data-theme-mode='dark'] .codeBlock {
|
||||
background-color: #121212;
|
||||
}
|
||||
|
||||
.title {
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 12px 20px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--primary);
|
||||
color: #fff;
|
||||
font-family: var(--font-regular);
|
||||
font-weight: var(--font-weight-bold);
|
||||
background-color: var(--code-title);
|
||||
}
|
||||
|
||||
.title code {
|
||||
background-color: var(--surface-1);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.2rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--secondary);
|
||||
font-size: 0.8125rem;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.code {
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
}
|
||||
|
||||
.preview {
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
box-shadow: inset 0 0 0 1px var(--border);
|
||||
background-color: var(--bg);
|
||||
background-color: var(--bui-bg);
|
||||
padding: 1px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
.container {
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
max-height: calc(100vh - 48px);
|
||||
overflow-y: auto;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.list::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background-color: var(--border);
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
background-color: var(--primary);
|
||||
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.itemNested {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 6px 16px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
line-height: 1.4;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.itemActive .link {
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for webkit browsers */
|
||||
.container::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.container::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.container::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Responsive: hide on smaller screens */
|
||||
@media (max-width: 1280px) {
|
||||
.container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import styles from './TableOfContents.module.css';
|
||||
|
||||
interface Heading {
|
||||
id: string;
|
||||
text: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
export function TableOfContents() {
|
||||
const [headings, setHeadings] = useState<Heading[]>([]);
|
||||
const [activeId, setActiveId] = useState<string>('');
|
||||
const [indicatorTop, setIndicatorTop] = useState<number>(0);
|
||||
const [indicatorHeight, setIndicatorHeight] = useState<number>(0);
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
// Extract all H2 and H3 headings from the document
|
||||
const elements = Array.from(
|
||||
document.querySelectorAll('h2[id], h3[id]'),
|
||||
) as HTMLHeadingElement[];
|
||||
|
||||
const headingData: Heading[] = elements.map(element => ({
|
||||
id: element.id,
|
||||
text: (element.textContent || '').replace('#', '').trim(),
|
||||
level: parseInt(element.tagName.substring(1)),
|
||||
}));
|
||||
|
||||
setHeadings(headingData);
|
||||
|
||||
// Set initial active heading (first visible heading or first heading)
|
||||
if (headingData.length > 0) {
|
||||
const viewportTop = window.scrollY + 100; // offset for header
|
||||
const visibleHeading = elements.find(element => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.top + window.scrollY >= viewportTop - 200;
|
||||
});
|
||||
setActiveId(visibleHeading?.id || headingData[0].id);
|
||||
}
|
||||
|
||||
// Set up IntersectionObserver to track visible headings
|
||||
const observerOptions = {
|
||||
rootMargin: '-80px 0px -80% 0px',
|
||||
threshold: 1,
|
||||
};
|
||||
|
||||
const observerCallback = (entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveId(entry.target.id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
observerCallback,
|
||||
observerOptions,
|
||||
);
|
||||
|
||||
elements.forEach(element => observer.observe(element));
|
||||
|
||||
return () => {
|
||||
elements.forEach(element => observer.unobserve(element));
|
||||
};
|
||||
}, [pathname]);
|
||||
|
||||
// Update indicator position when activeId changes
|
||||
useEffect(() => {
|
||||
if (activeId) {
|
||||
const activeElement = document.querySelector(
|
||||
`[data-toc-id="${activeId}"]`,
|
||||
) as HTMLElement;
|
||||
if (activeElement) {
|
||||
const list = activeElement.closest('ul');
|
||||
if (list) {
|
||||
const listRect = list.getBoundingClientRect();
|
||||
const elementRect = activeElement.getBoundingClientRect();
|
||||
setIndicatorTop(elementRect.top - listRect.top);
|
||||
setIndicatorHeight(elementRect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [activeId, headings]);
|
||||
|
||||
const handleClick = (id: string) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
// Update URL without scrolling (already handled above)
|
||||
window.history.pushState(null, '', `#${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (headings.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={styles.container}>
|
||||
<ul className={styles.list}>
|
||||
{indicatorHeight > 0 && (
|
||||
<div
|
||||
className={styles.indicator}
|
||||
style={{
|
||||
top: `${indicatorTop}px`,
|
||||
height: `${indicatorHeight}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{headings.map((heading, index) => (
|
||||
<li
|
||||
key={`${heading.id}-${index}`}
|
||||
data-toc-id={heading.id}
|
||||
className={`${styles.item} ${
|
||||
heading.level === 3 ? styles.itemNested : ''
|
||||
} ${activeId === heading.id ? styles.itemActive : ''}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => handleClick(heading.id)}
|
||||
className={styles.link}
|
||||
>
|
||||
{heading.text}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { TableOfContents } from './TableOfContents';
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { classNamePropDefs, stylePropDefs } from '../../utils/propDefs';
|
||||
import type { PropDef } from '../../utils/propDefs';
|
||||
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
|
||||
import type { PropDef } from '@/utils/propDefs';
|
||||
|
||||
export const buttonLinkPropDefs: Record<string, PropDef> = {
|
||||
variant: {
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { classNamePropDefs, stylePropDefs } from '../../utils/propDefs';
|
||||
import type { PropDef } from '../../utils/propDefs';
|
||||
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
|
||||
import type { PropDef } from '@/utils/propDefs';
|
||||
|
||||
export const buttonPropDefs: Record<string, PropDef> = {
|
||||
variant: {
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
classNamePropDefs,
|
||||
stylePropDefs,
|
||||
type PropDef,
|
||||
} from '../../utils/propDefs';
|
||||
} from '@/utils/propDefs';
|
||||
|
||||
export const tablePropDefs: Record<string, PropDef> = {
|
||||
selectionBehavior: {
|
||||
@@ -15,11 +15,15 @@
|
||||
--border: #e5e5e5;
|
||||
--border2: #cdcdcd;
|
||||
--surface-1: #f4f4f4;
|
||||
--code-bg: #3e444f;
|
||||
--code-title: #505865;
|
||||
}
|
||||
|
||||
[data-theme-mode='dark'] {
|
||||
--bg: #000000;
|
||||
--bg-hover: #202020;
|
||||
--code-bg: #202020;
|
||||
--code-title: #292929;
|
||||
--primary: #fff;
|
||||
--secondary: #818181;
|
||||
--action: #202020;
|
||||
|
||||
@@ -49,3 +49,27 @@
|
||||
.a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.headingWithAnchor {
|
||||
position: relative;
|
||||
scroll-margin-top: 80px;
|
||||
}
|
||||
|
||||
.anchorLink {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.anchorHash {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
font-size: 0.8em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.anchorLink:hover .anchorHash {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,61 @@ import Image, { ImageProps } from 'next/image';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import styles from './css/mdx.module.css';
|
||||
|
||||
// Utility function to generate slug from heading text
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^\w\-]+/g, '')
|
||||
.replace(/\-\-+/g, '-')
|
||||
.replace(/^-+/, '')
|
||||
.replace(/-+$/, '');
|
||||
}
|
||||
|
||||
// Component for heading with anchor link
|
||||
function HeadingWithAnchor({
|
||||
level,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
level: number;
|
||||
children: ReactNode;
|
||||
className: string;
|
||||
}) {
|
||||
const text =
|
||||
typeof children === 'string'
|
||||
? children
|
||||
: Array.isArray(children)
|
||||
? children.join('')
|
||||
: '';
|
||||
const id = slugify(text);
|
||||
|
||||
const Tag = `h${level}` as keyof JSX.IntrinsicElements;
|
||||
|
||||
return (
|
||||
<Tag id={id} className={`${className} ${styles.headingWithAnchor}`}>
|
||||
<a href={`#${id}`} className={styles.anchorLink}>
|
||||
{children}
|
||||
<span className={styles.anchorHash}>#</span>
|
||||
</a>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
export const formattedMDXComponents: MDXComponents = {
|
||||
h1: ({ children }) => <h1 className={styles.h1}>{children as ReactNode}</h1>,
|
||||
h2: ({ children }) => <h2 className={styles.h2}>{children as ReactNode}</h2>,
|
||||
h3: ({ children }) => <h3 className={styles.h3}>{children as ReactNode}</h3>,
|
||||
h2: ({ children }) => (
|
||||
<HeadingWithAnchor level={2} className={styles.h2}>
|
||||
{children as ReactNode}
|
||||
</HeadingWithAnchor>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<HeadingWithAnchor level={3} className={styles.h3}>
|
||||
{children as ReactNode}
|
||||
</HeadingWithAnchor>
|
||||
),
|
||||
p: ({ children }) => <p className={styles.p}>{children as ReactNode}</p>,
|
||||
a: ({ children, href }) => (
|
||||
<a href={href} className={styles.a}>
|
||||
|
||||
@@ -125,16 +125,3 @@ export const components: Page[] = [
|
||||
slug: 'visually-hidden',
|
||||
},
|
||||
];
|
||||
|
||||
export type ScreenSize = {
|
||||
title: string;
|
||||
slug: string;
|
||||
width: number;
|
||||
};
|
||||
|
||||
export const screenSizes: ScreenSize[] = [
|
||||
{ title: 'Mobile', slug: 'mobile', width: 390 },
|
||||
{ title: 'Tablet', slug: 'tablet', width: 768 },
|
||||
{ title: 'Desktop', slug: 'desktop', width: 1280 },
|
||||
{ title: 'Wide', slug: 'wide', width: 1600 },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user