Skip to content

Commit 17c4110

Browse files
authoredMar 7, 2025
Merge pull request #42 from rasengan-dev/feature/extract-toc-from-mdx
feat: extract toc from mdx
2 parents 5bfbeab + ad4b19b commit 17c4110

19 files changed

+975
-149
lines changed
 

‎packages/rasengan-mdx/src/components/heading.tsx

+15-28
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,15 @@
11
import { createElement, useMemo } from 'react';
22
import { HeadingProps, HeadingProps2 } from '../types/index.js';
3+
import { generateAnchor } from '../utils/extract-toc.js';
34

45
export const Heading = ({ variant }: HeadingProps) => {
56
return ({ children }: HeadingProps2) => {
67
const { text, id } = useMemo(() => {
7-
// Regex pattern to match links in the format [#link text]
8-
const regex = new RegExp('^\\[#.+\\]');
9-
10-
// split the children into an array of strings based on space
11-
const childrenArray = children.split(' [#');
12-
let lastWord = '';
13-
let restOfTheWords = '';
14-
let link = '';
15-
16-
// Check if we have more than one word and if the last word is a link
17-
if (
18-
childrenArray.length > 1 &&
19-
childrenArray.at(-1).includes(']') &&
20-
regex.test(`[#${childrenArray.at(-1)}`)
21-
) {
22-
lastWord = childrenArray.pop();
23-
}
24-
25-
if (lastWord) {
26-
link = lastWord.replace(']', '').split(' ').join('-').toLowerCase();
27-
restOfTheWords = childrenArray[0];
28-
} else {
29-
link = children.split(' ').join('-').toLowerCase();
30-
restOfTheWords = children;
31-
}
8+
const { id, text } = generateAnchor(children);
329

3310
return {
34-
id: variant === 'h1' ? undefined : link,
35-
text: restOfTheWords,
11+
id: variant === 'h1' ? undefined : id,
12+
text,
3613
};
3714
}, [children]);
3815

@@ -42,11 +19,21 @@ export const Heading = ({ variant }: HeadingProps) => {
4219
children: text,
4320
});
4421

22+
const handleClick = (
23+
e: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
24+
id: string
25+
) => {
26+
e.preventDefault();
27+
28+
const element = document.getElementById(id);
29+
element?.scrollIntoView({ behavior: 'smooth' });
30+
};
31+
4532
return (
4633
<div className="ra-heading-wrapper">
4734
{heading}
4835
{id && (
49-
<a href={`#${id}`}>
36+
<a href={`#${id}`} onClick={(e) => handleClick(e, id)}>
5037
<svg
5138
xmlns="http://www.w3.org/2000/svg"
5239
viewBox="0 0 24 24"
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Import statements
22
import { MDXRenderer } from './renderer.js';
33
import { Markdown } from './markdown.js';
4+
import { TableOfContents } from './toc.js';
45

56
// Export statements
6-
export { Markdown, MDXRenderer };
7+
export { Markdown, MDXRenderer, TableOfContents };

‎packages/rasengan-mdx/src/components/renderer.tsx

+26-13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MDXRendererProps } from '../types/index.js';
33
import { CodeBlock } from './codeblock.js';
44
import { Table } from './table.js';
55
import { Heading } from './heading.js';
6+
import TableOfContents from './toc.js';
67

78
/**
89
* Renders an MDX content component with a custom code block component.
@@ -17,19 +18,31 @@ const MDXRenderer = ({
1718
className,
1819
}: MDXRendererProps): React.ReactElement => {
1920
return (
20-
<section className={'rasengan-markdown-body ' + className}>
21-
<MDXContent
22-
components={{
23-
code: CodeBlock,
24-
table: Table,
25-
h1: Heading({ variant: 'h1' }),
26-
h2: Heading({ variant: 'h2' }),
27-
h3: Heading({ variant: 'h3' }),
28-
h4: Heading({ variant: 'h4' }),
29-
h5: Heading({ variant: 'h5' }),
30-
h6: Heading({ variant: 'h6' }),
31-
}}
32-
/>
21+
<section
22+
className={
23+
MDXContent.toc ? 'rasengan-wrapper-with-toc' : 'rasengan-wrapper'
24+
}
25+
>
26+
<section className={'rasengan-markdown-body ' + className}>
27+
<MDXContent
28+
components={{
29+
code: CodeBlock,
30+
table: Table,
31+
h1: Heading({ variant: 'h1' }),
32+
h2: Heading({ variant: 'h2' }),
33+
h3: Heading({ variant: 'h3' }),
34+
h4: Heading({ variant: 'h4' }),
35+
h5: Heading({ variant: 'h5' }),
36+
h6: Heading({ variant: 'h6' }),
37+
}}
38+
/>
39+
</section>
40+
41+
{MDXContent.toc && (
42+
<aside className="rasengan-toc">
43+
<TableOfContents items={MDXContent.toc} />
44+
</aside>
45+
)}
3346
</section>
3447
);
3548
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import React, { useState, useEffect, useRef, useCallback } from 'react';
2+
import { TOCItem } from '../types/index.js';
3+
4+
interface TableOfContentsProps {
5+
items: TOCItem[];
6+
}
7+
8+
export const TableOfContents: React.FC<TableOfContentsProps> = ({ items }) => {
9+
const [activeId, setActiveId] = useState<string | null>(null);
10+
const observerRef = useRef<IntersectionObserver | null>(null);
11+
12+
useEffect(() => {
13+
// Create an Intersection Observer to track which section is in view
14+
const observerOptions = {
15+
root: null,
16+
rootMargin: '0px',
17+
threshold: 0.4, // Trigger when 40% of the section is visible
18+
};
19+
20+
observerRef.current = new IntersectionObserver((entries) => {
21+
entries.forEach((entry) => {
22+
if (entry.isIntersecting) {
23+
const rect = entry.boundingClientRect;
24+
25+
if (rect.top >= 0) {
26+
// Scrolling Down: Set the first intersecting element as active
27+
setActiveId(entry.target.id);
28+
}
29+
}
30+
});
31+
32+
// Sort entries by bottom position when scrolling down
33+
const sortedEntries = entries
34+
.filter((entry) => entry.isIntersecting)
35+
.sort((a, b) => b.boundingClientRect.top - a.boundingClientRect.top);
36+
37+
if (sortedEntries.length > 0) {
38+
setActiveId(sortedEntries[0].target.id);
39+
}
40+
}, observerOptions);
41+
42+
// Observe all sections with IDs matching TOC anchors
43+
items.forEach((item) => {
44+
// Observe items of level 2
45+
const element = document.getElementById(item.anchor.id);
46+
if (element && observerRef.current) {
47+
observerRef.current.observe(element);
48+
}
49+
50+
// Observe items of level 3
51+
item.children.forEach((item) => {
52+
const element = document.getElementById(item.anchor.id);
53+
if (element && observerRef.current) {
54+
observerRef.current.observe(element);
55+
}
56+
});
57+
});
58+
59+
// Cleanup observer on unmount
60+
return () => {
61+
if (observerRef.current) {
62+
observerRef.current.disconnect();
63+
}
64+
};
65+
}, [items]);
66+
67+
const renderTOCItems = useCallback(
68+
(items: TOCItem[]) => {
69+
return items.map((item, index) => (
70+
<React.Fragment key={index}>
71+
<Item item={item} activeId={activeId} onActive={setActiveId} />
72+
{item.children && item.children.length > 0 && (
73+
<div className="toc-item--children">
74+
{renderTOCItems(item.children as TOCItem[])}
75+
</div>
76+
)}
77+
</React.Fragment>
78+
));
79+
},
80+
[items, activeId]
81+
);
82+
83+
return (
84+
<div className="table-of-contents">
85+
<h2 className="title">ON THIS PAGE</h2>
86+
<div className="items-container">{renderTOCItems(items)}</div>
87+
88+
<div
89+
className="w-full h-[500px] mt-10 bg-red-200"
90+
style={{ height: 500 }}
91+
></div>
92+
</div>
93+
);
94+
};
95+
96+
type ItemProps = {
97+
item: TOCItem;
98+
activeId: string;
99+
onActive: (id: string) => void;
100+
};
101+
102+
const Item = ({ item, activeId, onActive }: ItemProps) => {
103+
const handleClick = (
104+
e: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
105+
id: string
106+
) => {
107+
e.preventDefault();
108+
109+
onActive(id);
110+
111+
const element = document.getElementById(id);
112+
element?.scrollIntoView({ behavior: 'smooth' });
113+
};
114+
115+
return (
116+
<div
117+
key={item.anchor.id}
118+
className={`
119+
toc-item
120+
${item.level === 2 ? 'level2' : 'level3'}
121+
${activeId === item.anchor.id ? 'active' : ''}
122+
`}
123+
>
124+
<a
125+
href={`#${item.anchor.id}`}
126+
onClick={(e) => handleClick(e, item.anchor.id)}
127+
>
128+
<div className="toc-item--title">{item.anchor.text}</div>
129+
</a>
130+
</div>
131+
);
132+
};
133+
134+
export default TableOfContents;

‎packages/rasengan-mdx/src/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
* This package is inspired by @mdx-js/rollup to provide a custom implement of the MDX plugin for RasenganJs.
77
*/
88

9+
// Import statements
10+
import { extractTOC } from './utils/extract-toc.js';
11+
// import { defineMDXConfig } from "./utils/define-mdx-config.js";
12+
913
// Export statements
1014
export * from './types/index.js';
1115
export * from './components/index.js';
16+
export { extractTOC };

‎packages/rasengan-mdx/src/styles/rasengan-mdx.css

+184-14
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,28 @@
1414
--border: #eee;
1515
--border-dark: #494c54;
1616
--table-border: #d1d5dc;
17+
18+
/* Size */
19+
--container-2xs: 18rem;
20+
21+
/* Font size */
22+
--text-3xl: 1.875rem; /* 28px */
23+
--text-2xl: 1.5rem; /* 24px */
24+
--text-xl: 1.25rem; /* 20px */
25+
--text-lg: 1.125rem; /* 18px */
26+
--text-md: 1rem; /* 16px */
27+
--text-sm: 0.875rem; /* 14px */
28+
29+
/* Spacing */
30+
--ra-spacing: 0.25; /* 4px */
31+
32+
/* Marging */
33+
--mb-3xl: calc(var(--text-3xl) * var(--ra-spacing));
34+
--mb-2xl: calc(var(--text-2xl) * var(--ra-spacing));
35+
--mb-xl: calc(var(--text-xl) * var(--ra-spacing));
36+
--mb-lg: calc(var(--text-lg) * var(--ra-spacing));
37+
--mb-md: calc(var(--text-md) * var(--ra-spacing));
38+
--mb-sm: calc(var(--text-sm) * var(--ra-spacing));
1739
}
1840

1941
.dark {
@@ -31,11 +53,93 @@
3153
--table-border: #4b5563;
3254
}
3355

56+
.rasengan-wrapper,
57+
.rasengan-wrapper-with-toc {
58+
position: relative;
59+
}
60+
61+
.rasengan-wrapper-with-toc {
62+
margin-left: auto;
63+
margin-right: auto;
64+
display: grid;
65+
width: 100%;
66+
max-width: 42rem; /* 2xl */
67+
grid-template-columns: 1fr;
68+
gap: 2.5rem; /* 40px */
69+
padding-left: 2.5rem;
70+
}
71+
3472
.rasengan-markdown-body {
73+
position: relative;
3574
box-sizing: border-box;
3675
margin: 0;
37-
padding: 0;
3876
font-weight: 400;
77+
padding-left: 1rem; /* px-4 (16px) */
78+
padding-right: 1rem; /* px-4 (16px) */
79+
padding-top: 2.5rem; /* pt-10 (40px) */
80+
padding-bottom: 6rem; /* pb-24 (96px) */
81+
}
82+
83+
/* TOC */
84+
85+
.rasengan-toc > .table-of-contents {
86+
position: sticky;
87+
top: 3.5rem; /* top-14 (56px) */
88+
max-height: calc(100svh - 3.5rem);
89+
/* max-h-[calc(100svh-3.5rem)] */
90+
overflow-x: hidden;
91+
padding-left: 1.5rem; /* px-6 (24px) */
92+
padding-right: 1.5rem; /* px-6 (24px) */
93+
padding-top: 2.5rem; /* pt-10 (40px) */
94+
padding-bottom: 6rem; /* pb-24 (96px) */
95+
}
96+
97+
.rasengan-toc > .table-of-contents .title {
98+
font-size: 0.675rem;
99+
margin-bottom: 1rem;
100+
color: var(--fg-dark);
101+
}
102+
103+
.rasengan-toc > .table-of-contents .items-container {
104+
display: flex;
105+
flex-direction: column;
106+
}
107+
108+
.rasengan-toc > .table-of-contents .toc-item {
109+
cursor: pointer;
110+
padding-left: 0.5rem;
111+
border-left: 1px solid var(--border);
112+
transition: all 0.2s ease;
113+
color: var(--fg);
114+
}
115+
116+
.rasengan-toc > .table-of-contents .toc-item:hover {
117+
border-left-color: var(--fg-accent);
118+
color: var(--fg-accent);
119+
}
120+
121+
.rasengan-toc > .table-of-contents .toc-item.active {
122+
border-left-color: var(--fg-accent);
123+
color: var(--fg-accent);
124+
}
125+
126+
.rasengan-toc > .table-of-contents .toc-item.active .toc-item--title {
127+
font-weight: bold;
128+
}
129+
130+
.rasengan-toc > .table-of-contents .toc-item--title {
131+
font-size: 0.875rem;
132+
/* font-weight: 300; */
133+
padding-top: 0.25rem;
134+
padding-bottom: 0.25rem;
135+
}
136+
137+
.rasengan-toc > .table-of-contents .level2 {
138+
padding-left: 1rem;
139+
}
140+
141+
.rasengan-toc > .table-of-contents .level3 {
142+
padding-left: 2rem;
39143
}
40144

41145
/* Headings */
@@ -46,9 +150,34 @@
46150
justify-content: start;
47151
}
48152

153+
.rasengan-markdown-body .ra-heading-wrapper:has(h1) {
154+
margin-bottom: 1rem;
155+
}
156+
49157
.rasengan-markdown-body .ra-heading-wrapper:has(h2) {
50158
border-bottom: 1px solid var(--border);
51-
margin: 2rem 0;
159+
margin-bottom: 1rem;
160+
margin-top: 2rem;
161+
}
162+
163+
.rasengan-markdown-body .ra-heading-wrapper:has(h3) {
164+
margin-bottom: var(--mb-xl);
165+
margin-top: 2rem;
166+
}
167+
168+
.rasengan-markdown-body .ra-heading-wrapper:has(h4) {
169+
margin-bottom: var(--mb-lg);
170+
margin-top: 2rem;
171+
}
172+
173+
.rasengan-markdown-body .ra-heading-wrapper:has(h5) {
174+
margin-bottom: var(--mb-md);
175+
margin-top: 2rem;
176+
}
177+
178+
.rasengan-markdown-body .ra-heading-wrapper:has(h6) {
179+
margin-bottom: var(--mb-sm);
180+
margin-top: 2rem;
52181
}
53182

54183
.rasengan-markdown-body .ra-heading-wrapper a {
@@ -92,42 +221,45 @@
92221
}
93222

94223
.rasengan-markdown-body h1 {
95-
font-size: 2.5em;
224+
font-size: var(--text-3xl);
96225
color: var(--fg);
97226
font-weight: 700;
98-
margin: 1em 0;
227+
margin: 0;
99228
}
100229

101230
.rasengan-markdown-body h2 {
102-
font-size: 2em;
231+
font-size: var(--text-2xl);
103232
color: var(--fg);
104233
font-weight: 700;
234+
margin-bottom: var(--mb-2xl);
105235
}
106236

107237
.rasengan-markdown-body h3 {
108-
font-size: 1.5em;
238+
font-size: var(--text-xl);
109239
color: var(--fg);
110240
font-weight: 700;
241+
margin-bottom: var(--mb-xl);
111242
}
112243

113244
.rasengan-markdown-body h4 {
114-
font-size: 1.17em;
245+
font-size: var(--text-lg);
115246
color: var(--fg);
116247
font-weight: 700;
248+
margin-bottom: var(--mb-lg);
117249
}
118250

119251
.rasengan-markdown-body h5 {
120-
font-size: 1em;
252+
font-size: var(--text-md);
121253
color: var(--fg);
122254
font-weight: 700;
123-
margin: 0.67em 0;
255+
margin-bottom: var(--mb-md);
124256
}
125257

126258
.rasengan-markdown-body h6 {
127-
font-size: 0.83em;
259+
font-size: var(--text-sm);
128260
color: var(--fg);
129261
font-weight: 700;
130-
margin: 0.67em 0;
262+
margin-bottom: var(--mb-sm);
131263
}
132264

133265
.rasengan-markdown-body h2:hover::after,
@@ -138,9 +270,10 @@
138270
}
139271

140272
.rasengan-markdown-body p {
141-
font-size: 1em;
273+
font-size: var(--text-sm);
142274
color: var(--fg);
143-
margin: 1em 0;
275+
margin: var(--mb-sm) 0;
276+
line-height: 2;
144277
}
145278

146279
.rasengan-markdown-body a {
@@ -156,7 +289,7 @@
156289

157290
.rasengan-markdown-body hr {
158291
color: var(--border);
159-
margin: 1em 0;
292+
margin: var(--mb-sm) 0;
160293
}
161294

162295
/* Code blocks */
@@ -379,3 +512,40 @@
379512
.rasengan-markdown-body blockquote p {
380513
margin: 0;
381514
}
515+
516+
/* Media queries */
517+
518+
@media (min-width: 1280px) {
519+
/* xl */
520+
.rasengan-wrapper-with-toc {
521+
max-width: 80rem; /* 5xl */
522+
grid-template-columns: minmax(0, 1fr) var(--container-2xs);
523+
}
524+
525+
.rasengan-markdown-body {
526+
padding-right: 0; /* xl:pr-0 */
527+
max-width: 50rem;
528+
margin: 0 auto;
529+
}
530+
}
531+
532+
@media (max-width: 1279px) {
533+
/* max-xl */
534+
.rasengan-wrapper-with-toc > .rasengan-toc {
535+
display: none;
536+
}
537+
538+
.rasengan-wrapper-with-toc {
539+
display: block;
540+
max-width: 60rem;
541+
padding-left: 0;
542+
}
543+
}
544+
545+
@media (min-width: 640px) {
546+
/* sm */
547+
.rasengan-markdown-body {
548+
padding-left: 1.5rem; /* sm:px-6 (24px) */
549+
padding-right: 1.5rem; /* sm:px-6 (24px) */
550+
}
551+
}

‎packages/rasengan-mdx/src/types/index.ts

+33
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// @ts-ignore
22
import { type Metadata } from 'rasengan';
3+
import React from 'react';
34

45
/**
56
* A React functional component that represents an MDX page.
@@ -13,6 +14,18 @@ export type MDXPageComponent = React.FC<any> & {
1314
path: string;
1415
metadata: Metadata;
1516
};
17+
18+
toc?: Array<TOCItem>;
19+
};
20+
21+
export type TOCItem = {
22+
title: string;
23+
anchor: {
24+
id: string;
25+
text: string;
26+
};
27+
level: 2 | 3;
28+
children: Array<Omit<TOCItem, 'children'>>;
1629
};
1730

1831
/**
@@ -28,6 +41,9 @@ export type ComponentWithTextChildrenProps = {
2841
export type MDXRendererProps = {
2942
children: MDXPageComponent;
3043
className?: string;
44+
45+
// Used to customise the mdx visual aspect
46+
config?: MDXConfigProps;
3147
};
3248

3349
/**
@@ -51,3 +67,20 @@ export type NavigationStructure = {
5167
level: number;
5268
children?: NavigationStructure[];
5369
};
70+
71+
type HeadingConfigProps = { fullText: string; text: string; id: string };
72+
type TOCConfig = (toc: Array<TOCItem>) => React.ReactNode;
73+
74+
type ComponentConfig = {
75+
h1?: (value: HeadingConfigProps) => React.ReactNode;
76+
h2?: (value: HeadingConfigProps) => React.ReactNode;
77+
h3?: (value: HeadingConfigProps) => React.ReactNode;
78+
h4?: (value: HeadingConfigProps) => React.ReactNode;
79+
h5?: (value: HeadingConfigProps) => React.ReactNode;
80+
h6?: (value: HeadingConfigProps) => React.ReactNode;
81+
};
82+
83+
export type MDXConfigProps = {
84+
components?: ComponentConfig;
85+
toc?: TOCConfig;
86+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { MDXConfigProps } from '../types/index.js';
2+
3+
export function defineMDXConfig(config: MDXConfigProps) {
4+
return config;
5+
}
+74-12
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,75 @@
1-
import { remark } from 'remark';
2-
import remarkToc from 'remark-toc';
3-
4-
export async function extractToc(markdown: string) {
5-
const file = await remark()
6-
.use(remarkToc, {
7-
heading: 'structure',
8-
maxDepth: 3,
9-
})
10-
.process(markdown);
11-
12-
return String(file);
1+
import { TOCItem } from '../types/index.js';
2+
3+
/**
4+
* This function is used to extract TOC (Table Of Content) from markdown text
5+
* @param markdown
6+
* @returns
7+
*/
8+
export function extractTOC(markdown: string) {
9+
const lines = markdown.split('\n');
10+
const toc: TOCItem[] = [];
11+
12+
lines.forEach((line: string) => {
13+
const h2Match = line.match(/^## (.+)/); // Titres H2
14+
const h3Match = line.match(/^### (.+)/); // Titres H3
15+
16+
if (h2Match) {
17+
const title = h2Match[1].trim();
18+
const anchor = generateAnchor(title);
19+
toc.push({
20+
title,
21+
anchor,
22+
level: 2,
23+
children: [],
24+
});
25+
} else if (h3Match && toc.length > 0) {
26+
const title = h3Match[1].trim();
27+
const anchor = generateAnchor(title);
28+
toc[toc.length - 1].children.push({
29+
title,
30+
anchor,
31+
level: 3,
32+
});
33+
}
34+
});
35+
36+
return toc;
1337
}
38+
39+
/**
40+
* This function is used to extract the link from a heading text coming from a markdown
41+
* @param title
42+
* @returns
43+
*/
44+
export const generateAnchor = (title: string) => {
45+
// Regex pattern to match links in the format [#link text]
46+
const regex = new RegExp('^\\[#.+\\]');
47+
48+
// split the children into an array of strings based on space
49+
const childrenArray = title.split(' [#');
50+
let lastWord = '';
51+
let remainingWords = '';
52+
let id = '';
53+
54+
// Check if we have more than one word and if the last word is a link
55+
if (
56+
childrenArray.length > 1 &&
57+
childrenArray.at(-1).includes(']') &&
58+
regex.test(`[#${childrenArray.at(-1)}`)
59+
) {
60+
lastWord = childrenArray.pop();
61+
}
62+
63+
if (lastWord) {
64+
id = lastWord.replace(']', '').split(' ').join('-').toLowerCase();
65+
remainingWords = childrenArray[0];
66+
} else {
67+
id = title.split(' ').join('-').toLowerCase();
68+
remainingWords = title;
69+
}
70+
71+
return {
72+
id,
73+
text: remainingWords,
74+
};
75+
};

‎packages/rasengan-mdx/src/utils/plugin.ts

+6-41
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,7 @@ import rehypeStringify from 'rehype-stringify';
55
import remarkParse from 'remark-parse';
66
import remarkRehype from 'remark-rehype';
77
import rehypePrettyCode from 'rehype-pretty-code';
8-
9-
function extractTOC(markdown: string) {
10-
const lines = markdown.split('\n');
11-
const toc = [];
12-
13-
lines.forEach((line: string) => {
14-
const h2Match = line.match(/^## (.+)/); // Titres H2
15-
const h3Match = line.match(/^### (.+)/); // Titres H3
16-
17-
if (h2Match) {
18-
const title = h2Match[1].trim();
19-
const anchor = generateAnchor(title);
20-
toc.push({
21-
title,
22-
anchor,
23-
level: 2,
24-
children: [],
25-
});
26-
} else if (h3Match && toc.length > 0) {
27-
const title = h3Match[1].trim();
28-
const anchor = generateAnchor(title);
29-
toc[toc.length - 1].children.push({
30-
title,
31-
anchor,
32-
level: 3,
33-
});
34-
}
35-
});
36-
37-
return toc;
38-
}
39-
40-
const generateAnchor = (title: string) => {
41-
// Used the implementation inside the useMemo hooks in src/components/heading.tsx instead
42-
43-
return `#${title}`;
44-
};
8+
import { extractTOC } from './extract-toc.js';
459

4610
/**
4711
* A Vite plugin that transforms MDX files into a format that can be used in a RasenganJs application.
@@ -110,10 +74,11 @@ export default async function plugin(): Promise<{
11074

11175
const { content, data: frontmatter } = matter(code);
11276

77+
// Extract the table of content
78+
const isTocVisible =
79+
frontmatter.toc !== undefined ? frontmatter.toc : false;
11380
const toc = extractTOC(content);
11481

115-
console.log({ toc });
116-
11782
// Apply transformation of the mdx file
11883
const result = await mdxInstance.transform(content, id);
11984

@@ -131,14 +96,14 @@ export default async function plugin(): Promise<{
13196
},
13297
};
13398

134-
// console.log(result.code)
135-
13699
return {
137100
code: `
138101
${result.code}
139102
const metadata = ${JSON.stringify(metadata)};
103+
const toc = ${isTocVisible ? JSON.stringify(toc) : undefined};
140104
141105
MDXContent.metadata = metadata;
106+
MDXContent.toc = toc;
142107
`,
143108
map: result.map,
144109
};

‎playground/rasengan-v1-test/package.json

+6-5
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,23 @@
99
"serve": "rasengan-serve ./dist"
1010
},
1111
"dependencies": {
12-
"rasengan": "workspace:*",
1312
"@rasenganjs/image": "workspace:*",
14-
"@rasenganjs/serve": "workspace:*",
1513
"@rasenganjs/mdx": "workspace:*",
14+
"@rasenganjs/serve": "workspace:*",
1615
"@rasenganjs/vercel": "workspace:*",
16+
"@tailwindcss/vite": "^4.*",
17+
"lucide-react": "^0.477.0",
18+
"rasengan": "workspace:*",
1719
"react": "^19.0.0",
1820
"react-dom": "^19.0.0",
19-
"tailwindcss": "^4.*",
20-
"@tailwindcss/vite": "^4.*"
21+
"tailwindcss": "^4.*"
2122
},
2223
"devDependencies": {
2324
"@types/react": "^19.0.0",
2425
"@types/react-dom": "^19.0.0",
2526
"autoprefixer": "^10.4.16",
26-
"postcss": "^8.4.31",
2727
"cross-env": "^7.0.3",
28+
"postcss": "^8.4.31",
2829
"typescript": "^5.1.3",
2930
"vite": "^6.0.11"
3031
}

‎playground/rasengan-v1-test/src/app/app.layout.tsx

+102-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,110 @@
1-
import React from 'react';
1+
import Image from '@rasenganjs/image';
2+
import { BookOpen, Box, LayoutTemplate } from 'lucide-react';
23
import { Outlet, LayoutComponent } from 'rasengan';
34

45
const AppLayout: LayoutComponent = () => {
56
return (
6-
<section className="w-full bg-white font-comfortaa">
7-
<main className="w-full p-4">
8-
<Outlet />
7+
<section className="w-full bg-white font-inter overflow-auto h-screen dark">
8+
<header className="border-b-[1px] border-b-[#222] h-[60px] w-full bg-black fixed top-0 left-0 right-0 z-20 flex items-center justify-between px-2">
9+
<div className="flex items-center gap-2 text-white">
10+
<Image src={'/rasengan.svg'} alt="Logo of rasengan" width={50} />
11+
<span className="text-xl">Rasengan.js</span>
12+
</div>
13+
</header>
14+
15+
<main className="w-full flex bg-black mt-8">
16+
<div className="w-[280px] border-r-[1px] border-r-[#222] text-white">
17+
<aside className="sticky top-8 w-full h-full max-h-[calc(100vh-(var(--spacing)*14.25))] overflow-y-auto pt-[60px] p-4">
18+
<div className="flex flex-col gap-4 text-sm">
19+
<div className="flex items-center gap-4 text-primary">
20+
<BookOpen size={20} />
21+
<span>Documentation</span>
22+
</div>
23+
<div className="flex items-center gap-4 text-white/80">
24+
<Box size={20} />
25+
<span>Packages</span>
26+
</div>
27+
<div className="flex items-center gap-4 text-white/80">
28+
<LayoutTemplate size={20} />
29+
<span>Templates</span>
30+
</div>
31+
</div>
32+
33+
<div className="mt-8">
34+
<span className="font-mono text-[12px] text-white/80">
35+
GETTING STARTED
36+
</span>
37+
38+
<div className="flex flex-col w-full text-sm py-4">
39+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
40+
<span>Installation</span>
41+
</div>
42+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
43+
<span>Project Structure</span>
44+
</div>
45+
</div>
46+
</div>
47+
48+
<div className="mt-8">
49+
<span className="font-mono text-[12px] text-white/80">
50+
CORE CONCEPTS
51+
</span>
52+
53+
<div className="flex flex-col w-full text-sm py-4">
54+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
55+
<span>Routing</span>
56+
</div>
57+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
58+
<span>Rendering</span>
59+
</div>
60+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
61+
<span>Styling</span>
62+
</div>
63+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
64+
<span>Optimizing</span>
65+
</div>
66+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
67+
<span>Configuring</span>
68+
</div>
69+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
70+
<span>Deploying</span>
71+
</div>
72+
</div>
73+
</div>
74+
75+
<div className="mt-8">
76+
<span className="font-mono text-[12px] text-white/80">
77+
API REFERENCE
78+
</span>
79+
80+
<div className="flex flex-col w-full text-sm py-4">
81+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
82+
<span>Components</span>
83+
</div>
84+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
85+
<span>Functions</span>
86+
</div>
87+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
88+
<span>File Conventions</span>
89+
</div>
90+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
91+
<span>rasengan.config.js</span>
92+
</div>
93+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
94+
<span>create-rasengan</span>
95+
</div>
96+
<div className="px-4 py-1 border-l-[1px] border-l-[#333] text-white/80 cursor-pointer hover:text-primary hover:border-l-primary/60 transition-all">
97+
<span>Rasengan CLI</span>
98+
</div>
99+
</div>
100+
</div>
101+
</aside>
102+
</div>
103+
<section className="w-(--main-width) mt-4">
104+
<Outlet />
105+
</section>
9106
</main>
107+
<div className="w-full h-[400px] border-[1px] border-t-[#222] bg-black"></div>
10108
</section>
11109
);
12110
};
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { RouterComponent, defineRouter } from 'rasengan';
22
import AppLayout from '@/app/app.layout';
3-
import Home from '@/app/home.page';
43
import Blog from '@/app/blog.page.mdx';
54
import SubRouter from '@/app/sub-router/sub-router.router';
65

@@ -9,5 +8,5 @@ class AppRouter extends RouterComponent {}
98
export default defineRouter({
109
imports: [SubRouter],
1110
layout: AppLayout,
12-
pages: [Home, Blog],
11+
pages: [Blog],
1312
})(AppRouter);
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,183 @@
11
---
2-
path: /blog
2+
path: /
33
metadata:
44
title: My Blog
55
description: Discover the latest articles on my blog.
6+
toc: true
67
---
78

8-
<div className="max-w-[700px] mx-auto">
9-
# My Blog
9+
# Créer une API REST avec Express.js
1010

11-
Welcome to my blog! Here you can find the latest articles I've written. Enjoy!
11+
Express.js est un framework minimaliste et flexible pour Node.js, idéal pour construire des API REST rapidement et efficacement. Dans cet article, nous allons voir comment créer une API REST basique avec Express.js, incluant les routes de base pour la gestion des données.
1212

13-
## Latest Articles
13+
## Prérequis
1414

15-
- [Article 1](./article-1)
16-
- [Article 2](./article-2)
17-
- [Article 3](./article-3)
15+
Avant de commencer, assurez-vous d'avoir Node.js installé sur votre machine. Si ce n'est pas encore fait, téléchargez-le depuis [nodejs.org](https://nodejs.org/).
1816

19-
### Article 1
17+
## Initialisation du projet
2018

21-
### Article 2
19+
### Création du projet
2220

23-
## Installations
21+
Créez un nouveau dossier pour votre projet et initialisez un projet Node.js :
2422

25-
```bash
26-
- npm install
23+
```sh
24+
mkdir my-api && cd my-api
25+
npm init -y
2726
```
2827

29-
## Usage
28+
### Installation d'Express.js
3029

31-
```tsx {1} title="main.tsx" showLineNumbers /Blog/
32-
import { Blog } from './components/Blog';
30+
Ensuite, installez Express.js :
3331

34-
const App = () => {
35-
return <Blog />;
36-
};
32+
```sh
33+
npm install express
3734
```
3835

39-
### Usage 1
36+
## Création du serveur Express
4037

41-
### Usage 3
38+
### Configuration du serveur
4239

43-
</div>
40+
Créez un fichier `server.js` à la racine de votre projet et ajoutez le code suivant :
41+
42+
```javascript
43+
const express = require('express');
44+
const app = express();
45+
const PORT = process.env.PORT || 3000;
46+
47+
app.use(express.json()); // Middleware pour parser le JSON
48+
49+
app.get('/', (req, res) => {
50+
res.send('Bienvenue sur mon API REST !');
51+
});
52+
53+
app.listen(PORT, () => {
54+
console.log(`Serveur démarré sur http://localhost:${PORT}`);
55+
});
56+
```
57+
58+
### Démarrage du serveur
59+
60+
Lancez votre serveur avec la commande :
61+
62+
```sh
63+
node server.js
64+
```
65+
66+
Vous devriez voir `Serveur démarré sur http://localhost:3000` dans votre terminal et pouvoir accéder à l'URL `http://localhost:3000` depuis votre navigateur.
67+
68+
## Mise en place des routes CRUD
69+
70+
### Création du fichier des routes
71+
72+
Ajoutons maintenant des routes pour gérer une ressource simple, comme des utilisateurs.
73+
74+
Créez un dossier `routes/` et ajoutez un fichier `users.js` :
75+
76+
```javascript
77+
const express = require('express');
78+
const router = express.Router();
79+
80+
// Données fictives
81+
let users = [
82+
{ id: 1, name: 'John Doe' },
83+
{ id: 2, name: 'Jane Doe' },
84+
];
85+
86+
// Lire tous les utilisateurs
87+
router.get('/', (req, res) => {
88+
res.json(users);
89+
});
90+
91+
// Lire un utilisateur par ID
92+
router.get('/:id', (req, res) => {
93+
const user = users.find((u) => u.id === parseInt(req.params.id));
94+
if (!user) return res.status(404).json({ message: 'Utilisateur non trouvé' });
95+
res.json(user);
96+
});
97+
98+
// Créer un utilisateur
99+
router.post('/', (req, res) => {
100+
const newUser = {
101+
id: users.length + 1,
102+
name: req.body.name,
103+
};
104+
users.push(newUser);
105+
res.status(201).json(newUser);
106+
});
107+
108+
// Mettre à jour un utilisateur
109+
router.put('/:id', (req, res) => {
110+
const user = users.find((u) => u.id === parseInt(req.params.id));
111+
if (!user) return res.status(404).json({ message: 'Utilisateur non trouvé' });
112+
user.name = req.body.name;
113+
res.json(user);
114+
});
115+
116+
// Supprimer un utilisateur
117+
router.delete('/:id', (req, res) => {
118+
users = users.filter((u) => u.id !== parseInt(req.params.id));
119+
res.status(204).send();
120+
});
121+
122+
module.exports = router;
123+
```
124+
125+
### Intégration des routes dans le serveur
126+
127+
Puis, modifiez `server.js` pour utiliser ces routes :
128+
129+
```javascript
130+
const express = require('express');
131+
const app = express();
132+
const PORT = process.env.PORT || 3000;
133+
const usersRoutes = require('./routes/users');
134+
135+
app.use(express.json());
136+
app.use('/users', usersRoutes);
137+
138+
app.listen(PORT, () => {
139+
console.log(`Serveur démarré sur http://localhost:${PORT}`);
140+
});
141+
```
142+
143+
## Test des routes
144+
145+
Vous pouvez tester votre API avec [Postman](https://www.postman.com/) ou en utilisant `curl` :
146+
147+
### Tester la récupération des utilisateurs
148+
149+
- Récupérer tous les utilisateurs :
150+
```sh
151+
curl -X GET http://localhost:3000/users
152+
```
153+
- Récupérer un utilisateur par ID :
154+
```sh
155+
curl -X GET http://localhost:3000/users/1
156+
```
157+
158+
### Tester la création d'un utilisateur
159+
160+
- Créer un utilisateur :
161+
```sh
162+
curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name": "Alice"}'
163+
```
164+
165+
### Tester la mise à jour d'un utilisateur
166+
167+
- Mettre à jour un utilisateur :
168+
```sh
169+
curl -X PUT http://localhost:3000/users/1 -H "Content-Type: application/json" -d '{"name": "John Smith"}'
170+
```
171+
172+
### Tester la suppression d'un utilisateur
173+
174+
- Supprimer un utilisateur :
175+
```sh
176+
curl -X DELETE http://localhost:3000/users/1
177+
```
178+
179+
## Conclusion
180+
181+
Vous venez de créer une API REST basique avec Express.js ! Ce guide vous a montré comment configurer un serveur, définir des routes CRUD et tester votre API. Pour aller plus loin, vous pouvez ajouter une base de données avec MongoDB ou PostgreSQL et utiliser des middleware comme `cors` ou `morgan` pour améliorer votre API.
182+
183+
Bon codage ! 🚀

‎playground/rasengan-v1-test/src/app/sub-router/group/contact.page.tsx

+192-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,199 @@
11
import { PageComponent } from 'rasengan';
2+
import { Markdown, TableOfContents, extractTOC } from '@rasenganjs/mdx';
3+
4+
const markdown = `
5+
# Créer une API REST avec Express.js
6+
7+
Express.js est un framework minimaliste et flexible pour Node.js, idéal pour construire des API REST rapidement et efficacement. Dans cet article, nous allons voir comment créer une API REST basique avec Express.js, incluant les routes de base pour la gestion des données.
8+
9+
## Prérequis
10+
11+
Avant de commencer, assurez-vous d'avoir Node.js installé sur votre machine. Si ce n'est pas encore fait, téléchargez-le depuis [nodejs.org](https://nodejs.org/).
12+
13+
## Initialisation du projet
14+
15+
### Création du projet
16+
17+
Créez un nouveau dossier pour votre projet et initialisez un projet Node.js :
18+
19+
~~~sh
20+
mkdir my-api && cd my-api
21+
npm init -y
22+
~~~
23+
24+
### Installation d'Express.js
25+
26+
Ensuite, installez Express.js :
27+
28+
~~~sh
29+
npm install express
30+
~~~
31+
32+
## Création du serveur Express
33+
34+
### Configuration du serveur
35+
36+
Créez un fichier ~server.js~ à la racine de votre projet et ajoutez le code suivant :
37+
38+
~~~javascript
39+
const express = require('express');
40+
const app = express();
41+
const PORT = process.env.PORT || 3000;
42+
43+
app.use(express.json()); // Middleware pour parser le JSON
44+
45+
app.get('/', (req, res) => {
46+
res.send('Bienvenue sur mon API REST !');
47+
});
48+
49+
app.listen(PORT, () => {
50+
console.log(~Serveur démarré sur http://localhost:\${PORT}~);
51+
});
52+
~~~
53+
54+
### Démarrage du serveur
55+
56+
Lancez votre serveur avec la commande :
57+
58+
~~~sh
59+
node server.js
60+
~~~
61+
62+
Vous devriez voir ~Serveur démarré sur http://localhost:3000~ dans votre terminal et pouvoir accéder à l'URL ~http://localhost:3000~ depuis votre navigateur.
63+
64+
## Mise en place des routes CRUD
65+
66+
### Création du fichier des routes
67+
68+
Ajoutons maintenant des routes pour gérer une ressource simple, comme des utilisateurs.
69+
70+
Créez un dossier ~routes/~ et ajoutez un fichier ~users.js~ :
71+
72+
~~~javascript
73+
const express = require('express');
74+
const router = express.Router();
75+
76+
// Données fictives
77+
let users = [
78+
{ id: 1, name: 'John Doe' },
79+
{ id: 2, name: 'Jane Doe' }
80+
];
81+
82+
// Lire tous les utilisateurs
83+
router.get('/', (req, res) => {
84+
res.json(users);
85+
});
86+
87+
// Lire un utilisateur par ID
88+
router.get('/:id', (req, res) => {
89+
const user = users.find(u => u.id === parseInt(req.params.id));
90+
if (!user) return res.status(404).json({ message: 'Utilisateur non trouvé' });
91+
res.json(user);
92+
});
93+
94+
// Créer un utilisateur
95+
router.post('/', (req, res) => {
96+
const newUser = {
97+
id: users.length + 1,
98+
name: req.body.name
99+
};
100+
users.push(newUser);
101+
res.status(201).json(newUser);
102+
});
103+
104+
// Mettre à jour un utilisateur
105+
router.put('/:id', (req, res) => {
106+
const user = users.find(u => u.id === parseInt(req.params.id));
107+
if (!user) return res.status(404).json({ message: 'Utilisateur non trouvé' });
108+
user.name = req.body.name;
109+
res.json(user);
110+
});
111+
112+
// Supprimer un utilisateur
113+
router.delete('/:id', (req, res) => {
114+
users = users.filter(u => u.id !== parseInt(req.params.id));
115+
res.status(204).send();
116+
});
117+
118+
module.exports = router;
119+
~~~
120+
121+
### Intégration des routes dans le serveur
122+
123+
Puis, modifiez ~server.js~ pour utiliser ces routes :
124+
125+
~~~javascript
126+
const express = require('express');
127+
const app = express();
128+
const PORT = process.env.PORT || 3000;
129+
const usersRoutes = require('./routes/users');
130+
131+
app.use(express.json());
132+
app.use('/users', usersRoutes);
133+
134+
app.listen(PORT, () => {
135+
console.log(~Serveur démarré sur http://localhost:\${PORT}~);
136+
});
137+
~~~
138+
139+
## Test des routes
140+
141+
Vous pouvez tester votre API avec [Postman](https://www.postman.com/) ou en utilisant ~curl~ :
142+
143+
### Tester la récupération des utilisateurs
144+
145+
- Récupérer tous les utilisateurs :
146+
~~~sh
147+
curl -X GET http://localhost:3000/users
148+
~~~
149+
- Récupérer un utilisateur par ID :
150+
~~~sh
151+
curl -X GET http://localhost:3000/users/1
152+
~~~
153+
154+
### Tester la création d'un utilisateur
155+
156+
- Créer un utilisateur :
157+
~~~sh
158+
curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name": "Alice"}'
159+
~~~
160+
161+
### Tester la mise à jour d'un utilisateur
162+
163+
- Mettre à jour un utilisateur :
164+
~~~sh
165+
curl -X PUT http://localhost:3000/users/1 -H "Content-Type: application/json" -d '{"name": "John Smith"}'
166+
~~~
167+
168+
### Tester la suppression d'un utilisateur
169+
170+
- Supprimer un utilisateur :
171+
~~~sh
172+
curl -X DELETE http://localhost:3000/users/1
173+
~~~
174+
175+
## Conclusion
176+
177+
Vous venez de créer une API REST basique avec Express.js ! Ce guide vous a montré comment configurer un serveur, définir des routes CRUD et tester votre API. Pour aller plus loin, vous pouvez ajouter une base de données avec MongoDB ou PostgreSQL et utiliser des middleware comme \`cors\` ou \`morgan\` pour améliorer votre API.
178+
179+
Bon codage ! 🚀
180+
`;
2181

3182
export const Contact: PageComponent = () => {
183+
console.log(extractTOC(markdown));
4184
return (
5-
<section>
6-
<h1>Contact page</h1>
185+
<section className="overflow-auto">
186+
{/* <h1>Contact page</h1> */}
187+
188+
<section className="mx-auto grid w-full max-w-3xl grid-cols-1 gap-10 xl:max-w-6xl xl:grid-cols-[minmax(0,1fr)_var(--container-2xs)] h-screen">
189+
<section className="px-0 pt-10 pb-24 sm:px-6 xl:pr-0">
190+
<Markdown content={markdown} />
191+
</section>
192+
193+
<aside className="rasengan-toc max-xl:hidden">
194+
<TableOfContents items={extractTOC(markdown)} />
195+
</aside>
196+
</section>
7197
</section>
8198
);
9199
};

‎playground/rasengan-v1-test/src/app/home.page.tsx ‎playground/rasengan-v1-test/src/app/sub-router/home.page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const Home: PageComponent = (props: any) => {
8787
);
8888
};
8989

90-
Home.path = '/';
90+
Home.path = '/home';
9191
Home.metadata = {
9292
title: 'Home',
9393
description: 'Home page',
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { defineRouter, RouterComponent } from 'rasengan';
22
import { About } from '@/app/sub-router/about.page';
33
import Group1 from './group/index.group';
4+
import Home from './home.page';
45

56
class SubRouter extends RouterComponent {}
67

78
export default defineRouter({
89
imports: [],
9-
pages: [About, Group1],
10-
// useParentLayout: false
10+
pages: [Home, About, Group1],
11+
useParentLayout: false,
1112
})(SubRouter);

‎playground/rasengan-v1-test/src/styles/index.css

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* Load Urbanist and Comfortaa font */
2-
@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@400;500&family=Urbanist:wght@700&display=swap');
2+
@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@200;300;400;500;600&family=Urbanist:wght@100;200;300;400;500;600;700&display=swap');
3+
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
4+
@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
35

46
@import 'tailwindcss';
57

@@ -9,6 +11,12 @@
911

1012
--font-urbanist: Urbanist, sans-serif;
1113
--font-comfortaa: Comfortaa, sans-serif;
14+
--font-inter: Inter, sans-serif;
15+
--font-mono: 'Geist Mono', sans-serif;
16+
}
17+
18+
:root {
19+
--main-width: calc(100% - 280px);
1220
}
1321

1422
body,
@@ -18,5 +26,7 @@ html {
1826
padding: 0;
1927
width: 100vw;
2028
min-height: 100vh;
21-
overflow-x: hidden;
29+
overflow: hidden;
30+
font-family: 'Inter';
31+
/* font-weight: 100; */
2232
}

‎pnpm-lock.yaml

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.