diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 059d704..2341733 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -10,5 +10,6 @@ module.exports = { ], 'prefer-arrow-callback': 'off', 'react/display-name': 'off', + 'tailwindcss/migration-from-tailwind-2': 0, }, } diff --git a/markdown/index.json b/markdown/index.json index 4e411a0..c7c2a45 100644 --- a/markdown/index.json +++ b/markdown/index.json @@ -1,17 +1,19 @@ [ { "paths": [ - "./sections/guide/what-this.md" + "what-this.md" ], + "slug": "guide", "title": "开始" }, { "paths": [ - "./sections/usage/markdown.md", - "./sections/usage/draw.md", - "./sections/usage/custom-component.md", - "./sections/usage/highlight.md" + "custom-component.md", + "draw.md", + "highlight.md", + "markdown.md" ], + "slug": "usage", "title": "使用" } ] \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 571f2e3..3cd26f3 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,7 +1,10 @@ +import { bootstarp } from './plugins/json-watcher.mjs' + +bootstarp() + /** @type {import('next').NextConfig} */ const nextConfig = { output: process.env.NODE_ENV === 'production' ? 'export' : 'standalone', - reactStrictMode: false, webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { config.plugins.push( diff --git a/package.json b/package.json index c0cb14b..827b968 100644 --- a/package.json +++ b/package.json @@ -37,11 +37,13 @@ "@radix-ui/react-tabs": "1.0.4", "@types/katex": "0.16.7", "camelcase-keys": "9.1.3", + "chokidar": "^3.6.0", "clsx": "2.1.1", "colorjs.io": "0.5.0", "devtools-detector": "2.0.17", "foxact": "0.2.33", "framer-motion": "11.2.4", + "globby": "^14.0.1", "immer": "10.1.1", "jotai": "2.8.0", "js-yaml": "4.1.0", diff --git a/plugins/interface.d.ts b/plugins/interface.d.ts new file mode 100644 index 0000000..2ad2d57 --- /dev/null +++ b/plugins/interface.d.ts @@ -0,0 +1,9 @@ +export type DocumentGraph = SingleDocumentTree[] + +export interface SingleDocumentTree { + title: string + paths: (string | PathWithMeta)[] + slug: string +} +interface PathMeta {} +export type PathWithMeta = [string, PathMeta?] diff --git a/plugins/json-watcher.mjs b/plugins/json-watcher.mjs new file mode 100644 index 0000000..fe66979 --- /dev/null +++ b/plugins/json-watcher.mjs @@ -0,0 +1,234 @@ +import { existsSync, writeFileSync } from 'fs' +import { readFile } from 'fs/promises' +import path from 'path' +import { fileURLToPath } from 'url' +import { watch } from 'chokidar' +import { globbySync } from 'globby' + +const __dirname = path.resolve(fileURLToPath(import.meta.url), '..') +const dataJsonPath = path.resolve(__dirname, '../markdown/index.json') +const workdirPath = path.resolve(__dirname, '../markdown/sections') +const pathGlob = workdirPath + '/**/*.md' + +const readFsDataJsonData = async () => { + const hasFile = existsSync(dataJsonPath) + + if (!hasFile) { + createDefaultDataJson() + return readFsDataJsonData() + } + const data = await readFile(dataJsonPath, 'utf8') + + try { + return JSON.parse(data) + } catch { + console.error('JSON parser error.') + return [] + } +} + +/** + * + * @param {import('./interface').DocumentGraph} data + * @returns {Record}} + */ +const parseFsData = (data) => { + if (!Array.isArray(data)) throw new TypeError('exist data json is broken.') + + const parsedMap = {} + + data.forEach((item) => { + parsedMap[item.slug] = { ...item } + + const pathSet = new Set() + parsedMap[item.slug].pathSet = pathSet + + item.paths.forEach((path) => { + pathSet.add(Array.isArray(path) ? path[0] : path) + }) + }) + + return parsedMap +} + +export async function bootstarp() { + const patch = debounce(async () => { + const fsJsonData = await readFsDataJsonData() + + const diffData = compareFsTreeWithExistJsonData(parseFsData(fsJsonData)) + console.log('diff', diffData) + writeFileSync( + dataJsonPath, + JSON.stringify(patchDataJson(diffData, fsJsonData), null, 2), + 'utf8', + ) + }, 800) + await patch() + const watcher = watch(pathGlob) + + watcher.on('add', (path) => { + patch() + }) + watcher.on('unlink', (path) => { + patch() + }) +} + +/** + * + * @param {{add: Record,remove:Record}} diffData + * @param {import('./interface').DocumentGraph} fsJsonData + */ +function patchDataJson(diffData, fsJsonData) { + const { add, remove } = diffData + + const clonedJsonData = [...fsJsonData] + const slugifyJsonMap = clonedJsonData.reduce((acc, cur) => { + acc[cur.slug] = cur + return acc + }, {}) + + for (const [slug, paths] of Object.entries(add)) { + if (!slugifyJsonMap[slug]) { + clonedJsonData.push({ + paths, + slug, + title: slug, + }) + + continue + } + + paths.forEach((path) => { + slugifyJsonMap[slug].paths.push(path) + }) + } + + for (const [slug, paths] of Object.entries(remove)) { + if (!slugifyJsonMap[slug]) continue + + paths.forEach((path) => { + const index = slugifyJsonMap[slug].paths.findIndex( + (_path) => path === _path, + ) + if (index > -1) { + slugifyJsonMap[slug].paths.splice(index, 1) + } + }) + } + + return clonedJsonData +} + +/** + * + * @param {Record}} data + */ +function compareFsTreeWithExistJsonData(data) { + const diffAddPathMap = {} + const diffRemovePathMap = {} + const paths = globbySync(pathGlob, { onlyFiles: true }) + + const slugSetMap = {} + paths.forEach((fullPath) => { + const pathArr = fullPath.replace(workdirPath, '').split('/').filter(Boolean) + + if (pathArr.length < 2) return + const slug = pathArr.shift() + if (!slug) return + slugSetMap[slug] ||= new Set() + + slugSetMap[slug].add(`${pathArr.join('/')}`) + }) + + Object.keys(slugSetMap).map((slug) => { + if (!data[slug]) { + diffAddPathMap[slug] = Array.from(slugSetMap[slug]) + return + } + + slugSetMap[slug].forEach((path) => { + if (data[slug]?.pathSet.has(path)) { + return + } + + diffAddPathMap[slug] ||= [] + diffAddPathMap[slug].push(path) + }) + + data[slug]?.pathSet.forEach((path) => { + if (!slugSetMap[slug]) { + diffRemovePathMap[slug] = Array.from(data[slug].pathSet) + return + } + + if (!slugSetMap[slug].has(path)) { + diffRemovePathMap[slug] ||= [] + diffRemovePathMap[slug].push(path) + } + }) + }) + + return { + add: diffAddPathMap, + remove: diffRemovePathMap, + } +} + +function createDefaultDataJson() { + /** + * @type {import('./interface').DocumentGraph} + */ + const jsonData = [] + /** + * @type {Record} + */ + const slugToListMap = {} + + globbySync(pathGlob, { onlyFiles: true }).map((fullPath) => { + const pathArr = fullPath.replace(workdirPath, '').split('/').filter(Boolean) + + if (pathArr.length < 2) return + const slug = pathArr.shift() + if (!slug) return + + let documentTree = slugToListMap[slug] + + if (!documentTree) { + slugToListMap[slug] = { + slug, + paths: [], + title: slug, + } + documentTree = slugToListMap[slug] + } + + documentTree.paths.push(`${pathArr.join('/')}`) + + return pathArr + }) + + Object.values(slugToListMap).forEach((value) => { + jsonData.push(value) + }) + + writeFileSync(dataJsonPath, JSON.stringify(jsonData, null, 2), 'utf8') +} + +function debounce(fn, wait) { + let callback = fn + let timerId = null + + function debounced() { + let context = this + + let args = arguments + + clearTimeout(timerId) + timerId = setTimeout(function () { + callback.apply(context, args) + }, wait) + } + + return debounced +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39203f2..b59f69b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: camelcase-keys: specifier: 9.1.3 version: 9.1.3 + chokidar: + specifier: ^3.6.0 + version: 3.6.0 clsx: specifier: 2.1.1 version: 2.1.1 @@ -44,6 +47,9 @@ importers: framer-motion: specifier: 11.2.4 version: 11.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + globby: + specifier: ^14.0.1 + version: 14.0.1 immer: specifier: 10.1.1 version: 10.1.1 @@ -1006,6 +1012,10 @@ packages: '@shikijs/transformers@1.5.2': resolution: {integrity: sha512-/Sh64rKOFGMQLCvtHeL1Y7EExdq8LLxcdVkvoGx2aMHsYMOn8DckYl2gYKMHRBu/YUt1C38/Amd1Jdh48tWHgw==} + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + '@stitches/core@1.2.8': resolution: {integrity: sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==} @@ -2091,6 +2101,10 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + globby@14.0.1: + resolution: {integrity: sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==} + engines: {node: '>=18'} + gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} @@ -2833,6 +2847,10 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -3366,6 +3384,10 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -3596,6 +3618,10 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + unified@11.0.4: resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==} @@ -4690,6 +4716,8 @@ snapshots: dependencies: shiki: 1.5.2 + '@sindresorhus/merge-streams@2.3.0': {} + '@stitches/core@1.2.8': {} '@swc/counter@0.1.3': {} @@ -5962,6 +5990,15 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + globby@14.0.1: + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.2 + ignore: 5.3.1 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 + gopd@1.0.1: dependencies: get-intrinsic: 1.2.4 @@ -6854,6 +6891,8 @@ snapshots: path-type@4.0.0: {} + path-type@5.0.0: {} + picocolors@1.0.0: {} picomatch@2.3.1: {} @@ -7405,6 +7444,8 @@ snapshots: slash@3.0.0: {} + slash@5.1.0: {} + slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.1 @@ -7667,6 +7708,8 @@ snapshots: undici-types@5.26.5: {} + unicorn-magic@0.1.0: {} + unified@11.0.4: dependencies: '@types/unist': 3.0.2 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index da4116b..aded820 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -34,7 +34,7 @@ export default async (props: PropsWithChildren) => { - + {props.children} diff --git a/src/components/banner/Banner.tsx b/src/components/banner/Banner.tsx index daf4f91..a814c5b 100644 --- a/src/components/banner/Banner.tsx +++ b/src/components/banner/Banner.tsx @@ -62,7 +62,7 @@ export const Banner: FC<{ {showIcon && ( diff --git a/src/components/button/MotionButton.tsx b/src/components/button/MotionButton.tsx index 42b06a2..b37a77b 100644 --- a/src/components/button/MotionButton.tsx +++ b/src/components/button/MotionButton.tsx @@ -1,25 +1,23 @@ 'use client' -import { forwardRef } from 'react' +import React, { forwardRef } from 'react' import { m } from 'framer-motion' -import type { ForwardRefComponent, HTMLMotionProps } from 'framer-motion' +import type { HTMLMotionProps } from 'framer-motion' -export const MotionButtonBase: ForwardRefComponent< - HTMLButtonElement, - HTMLMotionProps<'button'> -> = forwardRef(({ children, ...rest }, ref) => { - return ( - - {children} - - ) -}) - -MotionButtonBase.displayName = 'MotionButtonBase' +export const MotionButtonBase = forwardRef>( + ({ children, ...rest }, ref) => { + return ( + + {children} + + ) + }, +) diff --git a/src/components/button/StyledButton.tsx b/src/components/button/StyledButton.tsx index 7eed9de..2f0887a 100644 --- a/src/components/button/StyledButton.tsx +++ b/src/components/button/StyledButton.tsx @@ -13,14 +13,14 @@ const variantStyles = tv({ variant: { primary: clsx( 'bg-accent text-zinc-100', - 'active:contrast-125 hover:contrast-[1.10]', + 'hover:contrast-[1.10] active:contrast-125', 'font-semibold', - 'disabled:bg-gray-400 disabled:opacity-30 disabled:dark:bg-gray-800 disabled:dark:text-zinc-50 disabled:cursor-not-allowed', + 'disabled:cursor-not-allowed disabled:bg-gray-400 disabled:opacity-30 disabled:dark:bg-gray-800 disabled:dark:text-zinc-50', 'dark:text-neutral-800', ), secondary: clsx( 'group rounded-full bg-gradient-to-b from-zinc-50/50 to-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur transition dark:from-zinc-900/50 dark:to-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20', - 'disabled:bg-gray-400 disabled:opacity-30 disabled:dark:bg-gray-800 disabled:dark:text-zinc-50 disabled:cursor-not-allowed', + 'disabled:cursor-not-allowed disabled:bg-gray-400 disabled:opacity-30 disabled:dark:bg-gray-800 disabled:dark:text-zinc-50', ), }, }, @@ -75,7 +75,7 @@ const LoadingButtonWrapper: FC = ({ children }) => { {children}
-
+
) diff --git a/src/components/code-highlighter/CodeBlockWrapper.tsx b/src/components/code-highlighter/CodeBlockWrapper.tsx index f859e4f..9d0a5ae 100644 --- a/src/components/code-highlighter/CodeBlockWrapper.tsx +++ b/src/components/code-highlighter/CodeBlockWrapper.tsx @@ -89,9 +89,9 @@ export const CodeBlockWrapper: FC = (props) => {
{!!filename && (
- {filename} + {filename} {langIcon} @@ -102,7 +102,7 @@ export const CodeBlockWrapper: FC = (props) => { {!filename && !!language && (
{langIcon}
@@ -111,20 +111,20 @@ export const CodeBlockWrapper: FC = (props) => { - +
= (props) => { {isOverflow && isCollapsed && (
setIsOpened((v) => !v)} > - - {props.title} - + {props.title}
diff --git a/src/components/excalidraw/Excalidraw.tsx b/src/components/excalidraw/Excalidraw.tsx index edce37a..e6fdf71 100644 --- a/src/components/excalidraw/Excalidraw.tsx +++ b/src/components/excalidraw/Excalidraw.tsx @@ -134,7 +134,7 @@ const ExcalidrawImpl = forwardRef( className={clsxm('relative h-[500px] w-full', className)} > {loading && ( -
+
)} @@ -193,7 +193,7 @@ const ExcalidrawImpl = forwardRef( } }} className={clsxm( - 'absolute bottom-2 right-2 z-10 box-content flex h-5 w-5 rounded-md border p-2 center', + 'absolute bottom-2 right-2 z-10 box-content flex size-5 rounded-md border p-2 center', 'border-zinc-200 bg-base-100 text-zinc-600', 'dark:border-neutral-800 dark:text-zinc-500', )} diff --git a/src/components/fab/FABContainer.tsx b/src/components/fab/FABContainer.tsx index 43846c3..11e75ec 100644 --- a/src/components/fab/FABContainer.tsx +++ b/src/components/fab/FABContainer.tsx @@ -1,3 +1,4 @@ +/* eslint-disable tailwindcss/no-contradicting-classname */ 'use client' import React, { useEffect, useId, useRef } from 'react' @@ -48,7 +49,7 @@ export const FABBase = typescriptHappyForwardRef( exit={{ opacity: 0.3, scale: 0.8 }} className={clsxm( 'mt-2 flex items-center justify-center', - 'h-12 w-12 text-lg md:h-10 md:w-10 md:text-base', + 'size-12 text-lg md:size-10 md:text-base', 'border border-accent outline-accent hover:opacity-100 focus:opacity-100 focus:outline-none', 'rounded-xl border border-zinc-400/20 shadow-lg backdrop-blur-lg dark:border-zinc-500/30 dark:bg-zinc-800/80 dark:text-zinc-200', 'bg-zinc-50/80 shadow-lg dark:bg-neutral-900/80', diff --git a/src/components/header/MobileHeader.tsx b/src/components/header/MobileHeader.tsx index dbcbd5b..a00b540 100644 --- a/src/components/header/MobileHeader.tsx +++ b/src/components/header/MobileHeader.tsx @@ -32,7 +32,7 @@ export const MobileHeader = () => { -
+
diff --git a/src/components/header/internal/HeaderActionButton.tsx b/src/components/header/internal/HeaderActionButton.tsx index f8e5e01..954e98d 100644 --- a/src/components/header/internal/HeaderActionButton.tsx +++ b/src/components/header/internal/HeaderActionButton.tsx @@ -1,15 +1,15 @@ import { forwardRef } from 'react' import clsx from 'clsx' -import type { ForwardRefComponent } from 'framer-motion' +import type { MotionProps } from 'framer-motion' -export const HeaderActionButton: ForwardRefComponent< - HTMLButtonElement, - JSX.IntrinsicElements['button'] -> = forwardRef(({ children, ...rest }, ref) => { +export const HeaderActionButton = forwardRef< + any, + MotionProps & React.JSX.IntrinsicElements['button'] +>(({ children, ...rest }, ref) => { return (