+
+
+ {/* {category && (
{category}
)} */}
- {doc.title &&
{doc.title}
}
- {doc.description &&
{doc.description}
}
+ {doc.title &&
{doc.title}
}
+ {doc.description &&
{doc.description}
}
-
-
-
-
-
-
-
-
- {/* {prev && (
-
-
-
-
-
-
- {prev.category}
-
-
{prev.title}
-
-
-
-
- )} */}
+
-
- {/* {next && (
-
-
-
-
-
- {next.category}
-
-
{next.title}
-
-
-
-
-
- )} */}
+
+
+
+
+
- {/* //
- //
- // {/*
- //
- //
- // {/*
- //
- //
- // */}
- >
+
);
}
diff --git a/site/app/(marketing)/page.tsx b/site/app/(marketing)/page.tsx
index 47948525f..ceadd1994 100644
--- a/site/app/(marketing)/page.tsx
+++ b/site/app/(marketing)/page.tsx
@@ -653,3 +653,5 @@ export default async function HomePage() {
>
);
}
+
+HomePage.bodyClass = 'xxx';
diff --git a/site/app/layout.tsx b/site/app/layout.tsx
index 6c03351cd..e97eea93d 100644
--- a/site/app/layout.tsx
+++ b/site/app/layout.tsx
@@ -62,7 +62,7 @@ export const metadata = {
},
};
-export default function RootLayout({ children }: { children: React.ReactNode; }) {
+export default function RootLayout({ children, bodyClass }: { children: React.ReactNode; bodyClass?: string }) {
return (
@@ -74,6 +74,7 @@ export default function RootLayout({ children }: { children: React.ReactNode; })
)}
+ {bodyClass}
{children}
diff --git a/site/components/TOC.tsx b/site/components/TOC.tsx
new file mode 100644
index 000000000..d9f2359d9
--- /dev/null
+++ b/site/components/TOC.tsx
@@ -0,0 +1,101 @@
+'use client';
+
+import * as React from 'react';
+
+import { TableOfContents } from '@/lib/toc';
+import useMounted from '@/hooks/use-mounted';
+import clsx from 'clsx';
+
+interface TocProps {
+ toc: TableOfContents;
+}
+
+export default function TOC({ toc }: TocProps) {
+ const itemIds = React.useMemo(
+ () =>
+ toc.items
+ ? toc.items
+ .flatMap((item) => [item.url, item?.items?.map((item) => item.url)])
+ .flat()
+ .filter(Boolean)
+ .map((id) => id?.split('#')[1])
+ : [],
+ [toc],
+ );
+ const activeHeading = useActiveItem(itemIds);
+ const mounted = useMounted();
+
+ if (!toc?.items) {
+ return null;
+ }
+
+ return mounted ? (
+
+ ) : null;
+}
+
+function useActiveItem(itemIds: (string | undefined)[]) {
+ const [activeId, setActiveId] = React.useState
('');
+
+ React.useEffect(() => {
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ setActiveId(entry.target.id);
+ }
+ });
+ },
+ { rootMargin: '0% 0% -80% 0%' },
+ );
+
+ itemIds?.forEach((id) => {
+ if (!id) {
+ return;
+ }
+
+ const element = document.getElementById(id);
+ if (element) {
+ observer.observe(element);
+ }
+ });
+
+ return () => {
+ itemIds?.forEach((id) => {
+ if (!id) {
+ return;
+ }
+
+ const element = document.getElementById(id);
+ if (element) {
+ observer.unobserve(element);
+ }
+ });
+ };
+ }, [itemIds]);
+
+ return activeId;
+}
+
+interface TreeProps {
+ tree: TableOfContents;
+ level?: number;
+ activeItem?: string | null;
+}
+
+function Tree({ tree, level = 1, activeItem }: TreeProps) {
+ return tree?.items?.length && level < 3 ? (
+
+ {tree.items.map((item, index) => {
+ return (
+ -
+
+ {item.title}
+
+ {item.items?.length ? : null}
+
+ );
+ })}
+
+ ) : null;
+}
diff --git a/site/components/layout/hero/Ui.tsx b/site/components/layout/hero/Ui.tsx
index 935486944..5d9f6019c 100644
--- a/site/components/layout/hero/Ui.tsx
+++ b/site/components/layout/hero/Ui.tsx
@@ -5,7 +5,7 @@ import { uiDownloadUrl } from '@/config/site';
export default function LayoutHeroUi() {
return (
<>
-
+
Develop beautiful web apps with Tabler
diff --git a/site/hooks/use-mounted.ts b/site/hooks/use-mounted.ts
index 4887de69c..38231bce6 100644
--- a/site/hooks/use-mounted.ts
+++ b/site/hooks/use-mounted.ts
@@ -1,13 +1,13 @@
-import React from "react";
+import React from 'react';
const useMounted = () => {
- const [mounted, setMounted] = React.useState(false)
+ const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
- setMounted(true)
- }, [])
+ setMounted(true);
+ }, []);
- return mounted
-}
+ return mounted;
+};
-export default useMounted
+export default useMounted;
diff --git a/site/hooks/use-on-click-outside.ts b/site/hooks/use-on-click-outside.ts
index 04d906703..ac4dd0cfe 100644
--- a/site/hooks/use-on-click-outside.ts
+++ b/site/hooks/use-on-click-outside.ts
@@ -1,23 +1,19 @@
-import { RefObject } from 'react'
-import useEventListener from './use-event-listener'
+import { RefObject } from 'react';
+import useEventListener from './use-event-listener';
-type Handler = (event: MouseEvent) => void
+type Handler = (event: MouseEvent) => void;
-const useOnClickOutside = (
- ref: RefObject,
- handler: Handler,
- mouseEvent: 'mousedown' | 'mouseup' = 'mousedown',
-): void => {
- useEventListener(mouseEvent, event => {
- const el = ref?.current
+const useOnClickOutside = (ref: RefObject, handler: Handler, mouseEvent: 'mousedown' | 'mouseup' = 'mousedown'): void => {
+ useEventListener(mouseEvent, (event) => {
+ const el = ref?.current;
// Do nothing if clicking ref's element or descendent elements
if (!el || el.contains(event.target as Node)) {
- return
+ return;
}
- handler(event)
- })
-}
+ handler(event);
+ });
+};
-export default useOnClickOutside
\ No newline at end of file
+export default useOnClickOutside;
diff --git a/site/lib/toc.ts b/site/lib/toc.ts
new file mode 100644
index 000000000..ad4d11d85
--- /dev/null
+++ b/site/lib/toc.ts
@@ -0,0 +1,76 @@
+import { toc } from 'mdast-util-toc';
+import { remark } from 'remark';
+import { visit } from 'unist-util-visit';
+
+const textTypes = ['text', 'emphasis', 'strong', 'inlineCode'];
+
+function flattenNode(node: any) {
+ const p: any[] = [];
+ visit(node, (node) => {
+ if (!textTypes.includes(node.type)) {
+ return;
+ }
+ p.push(node.value);
+ });
+ return p.join('');
+}
+
+interface Item {
+ title: string;
+ url: string;
+ items?: Item[];
+}
+
+interface Items {
+ items?: Item[];
+}
+
+function getItems(node, current): Items {
+ if (!node) {
+ return {};
+ }
+
+ if (node.type === 'paragraph') {
+ visit(node, (item) => {
+ if (item.type === 'link') {
+ current.url = item.url;
+ current.title = flattenNode(node);
+ }
+
+ if (item.type === 'text') {
+ current.title = flattenNode(node);
+ }
+ });
+
+ return current;
+ }
+
+ if (node.type === 'list') {
+ current.items = node.children.map((i) => getItems(i, {}));
+
+ return current;
+ } else if (node.type === 'listItem') {
+ const heading = getItems(node.children[0], {});
+
+ if (node.children.length > 1) {
+ getItems(node.children[1], heading);
+ }
+
+ return heading;
+ }
+
+ return {};
+}
+
+const getToc = () => (node, file) => {
+ const table = toc(node);
+ file.data = getItems(table.map, {});
+};
+
+export type TableOfContents = Items;
+
+export async function getTableOfContents(content: string): Promise {
+ const result = await remark().use(getToc).process(content);
+
+ return result.data;
+}
diff --git a/site/package.json b/site/package.json
index 2b9d073e7..0950df377 100644
--- a/site/package.json
+++ b/site/package.json
@@ -32,6 +32,11 @@
"@tabler/icons": "^2.28.0",
"@tabler/icons-png": "^2.25.0",
"@tabler/icons-webfont": "^2.25.0",
+ "@types/debug": "^4.1.8",
+ "@types/eslint": "^8.44.0",
+ "@types/eslint-scope": "^3.7.4",
+ "@types/estree-jsx": "^1.0.0",
+ "@types/extend": "^3.0.1",
"@types/node": "^20.4.2",
"@types/react": "18.2.15",
"@types/react-dom": "18.2.7",
@@ -62,6 +67,8 @@
"mdast-util-mdx-jsx": "^3.0.0",
"mdast-util-mdxjs-esm": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "mdast-util-toc": "^7.0.0",
"mdx": "^0.3.1",
"mdx-annotations": "^0.1.3",
"minimatch": "^9.0.3",
@@ -105,12 +112,6 @@
"typescript": "5.1.6",
"unist-util-visit": "^5.0.0",
"webpack": "^5.88.2",
- "yaml": "^2.3.1",
- "@types/debug": "^4.1.8",
- "@types/eslint": "^8.44.0",
- "@types/eslint-scope": "^3.7.4",
- "@types/estree-jsx": "^1.0.0",
- "@types/extend": "^3.0.1",
- "mdast-util-to-string": "^4.0.0"
+ "yaml": "^2.3.1"
}
}
diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml
index 8a47fe684..d6f7e137a 100644
--- a/site/pnpm-lock.yaml
+++ b/site/pnpm-lock.yaml
@@ -142,6 +142,9 @@ dependencies:
mdast-util-to-string:
specifier: ^4.0.0
version: 4.0.0
+ mdast-util-toc:
+ specifier: ^7.0.0
+ version: 7.0.0
mdx:
specifier: ^0.3.1
version: 0.3.1
@@ -3004,6 +3007,10 @@ packages:
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
dev: false
+ /@types/ungap__structured-clone@0.3.0:
+ resolution: {integrity: sha512-eBWREUhVUGPze+bUW22AgUr05k8u+vETzuYdLYSvWqGTUe0KOf+zVnOB1qER5wMcw8V6D9Ar4DfJmVvD1yu0kQ==}
+ dev: false
+
/@types/unist@2.0.6:
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
dev: false
@@ -3074,6 +3081,10 @@ packages:
eslint-visitor-keys: 3.3.0
dev: false
+ /@ungap/structured-clone@1.2.0:
+ resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
+ dev: false
+
/@vercel/analytics@1.0.1:
resolution: {integrity: sha512-Ux0c9qUfkcPqng3vrR0GTrlQdqNJ2JREn/2ydrVuKwM3RtMfF2mWX31Ijqo1opSjNAq6rK76PwtANw6kl6TAow==}
dev: false
@@ -6100,6 +6111,18 @@ packages:
unist-util-visit: 4.1.2
dev: false
+ /mdast-util-toc@7.0.0:
+ resolution: {integrity: sha512-C28UcSqjmnWuvgT8d97qpaItHKvySqVPAECUzqQ51xuMyNFFJwcFoKW77KoMjtXrclTidLQFDzLUmTmrshRweA==}
+ dependencies:
+ '@types/mdast': 4.0.0
+ '@types/ungap__structured-clone': 0.3.0
+ '@ungap/structured-clone': 1.2.0
+ github-slugger: 2.0.0
+ mdast-util-to-string: 4.0.0
+ unist-util-is: 6.0.0
+ unist-util-visit: 5.0.0
+ dev: false
+
/mdn-data@2.0.28:
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
dev: false
diff --git a/site/styles/_config.scss b/site/styles/_config.scss
index ffa9794b8..1fabdccf5 100644
--- a/site/styles/_config.scss
+++ b/site/styles/_config.scss
@@ -222,6 +222,7 @@ $utilities: (
class: text,
values: map-merge($colors, (
muted: var(--color-muted, #{$color-muted}),
+ headers: var(--color-headers, #{$color-headers}),
reset: inherit
))
),
diff --git a/site/styles/_docs.scss b/site/styles/_docs.scss
index b7baa6c4b..f5b745a84 100644
--- a/site/styles/_docs.scss
+++ b/site/styles/_docs.scss
@@ -8,7 +8,7 @@
height: calc(100vh - #{$header-height});
display: flex;
flex-direction: column;
- width: 15rem;
+ width: 13rem;
&:after {
content: '';
@@ -22,6 +22,18 @@
}
}
+.docs-side-toc {
+ width: 15rem;
+ position: sticky;
+ top: calc(#{$header-height} + 1px);
+ height: calc(100vh - #{$header-height});
+ display: none;
+
+ @include media-breakpoint-up(xl) {
+ display: block;
+ }
+}
+
.docs-menu,
.docs-menu-group,
.docs-menu-submenu {
diff --git a/site/styles/_icons.scss b/site/styles/_icons.scss
index 54d810c0d..de0963326 100644
--- a/site/styles/_icons.scss
+++ b/site/styles/_icons.scss
@@ -34,7 +34,7 @@
.icon-inline {
width: 1.2em;
height: 1.2em;
- stroke-width: 2;
+ stroke-width: 1.5;
stroke: currentColor;
vertical-align: sub;
}
diff --git a/site/styles/_typo.scss b/site/styles/_typo.scss
index cf20b0d28..3d9b55d1f 100644
--- a/site/styles/_typo.scss
+++ b/site/styles/_typo.scss
@@ -88,7 +88,6 @@ h5,
h6,
.h6 {
@extend %headers-common;
- font-weight: $font-weight-normal;
font-size: $font-size-h6;
line-height: $line-height-h6;
margin-bottom: $gap-1;