From f176b1631c2cb914f8b45ce4178e9d478f200621 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Mon, 1 Apr 2024 19:05:33 +0200 Subject: [PATCH 01/23] Typo fixing Co-authored-by: William Oldham --- .../api-reference/components/guider-content-footer.mdx | 6 +++++- .../guider/api-reference/components/guider-header.mdx | 6 +++++- .../guider/api-reference/components/guider-layout.mdx | 8 ++++++-- .../guider/api-reference/components/guider-logo.mdx | 6 +++++- .../api-reference/components/guider-page-footer.mdx | 6 +++++- .../guider/api-reference/components/guider-sidebar.mdx | 6 +++++- .../guider/api-reference/components/guider-toc.mdx | 6 +++++- .../api-reference/functions/create-not-found-page.mdx | 2 +- .../guider/api-reference/functions/create-redirect.mdx | 1 + .../guider/api-reference/functions/use-guider-page.mdx | 10 +++++----- .../docs/guider/api-reference/functions/use-guider.mdx | 4 ++-- apps/docs/pages/docs/guider/guides/advanced/header.mdx | 2 +- .../pages/docs/guider/guides/deploy/github-pages.mdx | 6 +++--- .../pages/docs/guider/writing/components/fields.mdx | 4 ++-- .../docs/pages/docs/guider/writing/components/tabs.mdx | 2 +- .../docs/guider/writing/markdown/making-pages.mdx | 2 +- apps/docs/theme.config.tsx | 4 ++-- 17 files changed, 55 insertions(+), 26 deletions(-) diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-content-footer.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-content-footer.mdx index eb1a768c..bf790e1a 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-content-footer.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-content-footer.mdx @@ -2,8 +2,12 @@ React component that will show the currently configured content footer. -It respects configured `contentFooter` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `contentFooter` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +Will show one of: + - Nothing if the content footer is set to be hidden. + - The default content footer with the configured settings. + - The custom component if the content footer has been overriden in the partials. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-header.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-header.mdx index 276e9275..774ca843 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-header.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-header.mdx @@ -2,8 +2,12 @@ React component that will show the currently configured navigation partial. -It respects configured `navigation` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `navigation` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +Will show one of: + - Nothing if the navigation bar is set to be hidden. + - The default navigation bar with the configured settings. + - The custom component if the navigation bar has been overriden in the partials. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-layout.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-layout.mdx index a77ee0c6..a6d879db 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-layout.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-layout.mdx @@ -2,10 +2,14 @@ React component that will show the currently configured layout partial. -It respects configured `layout` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `layout` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). The primary use of this component is to make a non-MDX page look like an MDX page. Don't use it if you're already in an MDX File. +Will show one of: + - The default layout with the configured settings. + - The custom component if the layout has been overriden in the partials. + ## Example ```tsx @@ -55,7 +59,7 @@ function GuiderLayout(props); - The children of the react component, it will be displayed as the page contents. + The children of the React component, it will be displayed as the page contents. diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-logo.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-logo.mdx index 1dbb8c07..1d918629 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-logo.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-logo.mdx @@ -2,8 +2,12 @@ React component that will show the currently configured logo. -It respects configured `logo` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `logo` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +Will show one of: + - Nothing if the logo is set to be hidden. + - The default logo with the configured settings. + - The custom component if the logo has been overriden in the partials. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-page-footer.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-page-footer.mdx index 28ff6ca4..2fc8c2ac 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-page-footer.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-page-footer.mdx @@ -2,8 +2,12 @@ React component that will show the currently configured page footer. -It respects configured `pageFooter` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `pageFooter` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +Will show one of: + - Nothing if the page footer is set to be hidden. + - The default page footer with the configured settings. + - The custom component if the page footer has been overriden in the partials. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-sidebar.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-sidebar.mdx index cf47f551..282991ff 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-sidebar.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-sidebar.mdx @@ -2,8 +2,12 @@ React component that will show the currently configured sidebar partial. -It respects configured `sidebar` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `sidebar` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +Will show one of: + - Nothing if the sidebar is set to be hidden. + - The default sidebar with the configured settings. + - The custom component if the sidebar has been overriden in the partials. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-toc.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-toc.mdx index f0213840..86a34e57 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-toc.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-toc.mdx @@ -2,8 +2,12 @@ React component that will show the currently configured table of contents. -It respects configured `toc` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `toc` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +Will show one of: + - Nothing if the table of contents is set to be hidden. + - The default table of contents with the configured settings. + - The custom component if the table of contents has been overriden in the partials. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/functions/create-not-found-page.mdx b/apps/docs/pages/docs/guider/api-reference/functions/create-not-found-page.mdx index 125a51d2..187f96a4 100644 --- a/apps/docs/pages/docs/guider/api-reference/functions/create-not-found-page.mdx +++ b/apps/docs/pages/docs/guider/api-reference/functions/create-not-found-page.mdx @@ -5,7 +5,7 @@ This function creates a premade 404 page. You don't have to use this 404 page, it does nothing special. Feel free to make your own. - This has be put in `/pages/404.tsx` to work, you can't choose a different location or it won't work. + This has be put in `/pages/404.tsx` to work, if you use a different location it won't work. diff --git a/apps/docs/pages/docs/guider/api-reference/functions/create-redirect.mdx b/apps/docs/pages/docs/guider/api-reference/functions/create-redirect.mdx index 3091a724..d33c566f 100644 --- a/apps/docs/pages/docs/guider/api-reference/functions/create-redirect.mdx +++ b/apps/docs/pages/docs/guider/api-reference/functions/create-redirect.mdx @@ -2,6 +2,7 @@ This utility function is an easy way to make redirects work in static site generation mode. +When it's used, it creates a dummy page that redirects when loaded. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/functions/use-guider-page.mdx b/apps/docs/pages/docs/guider/api-reference/functions/use-guider-page.mdx index 8421cc75..537efe7a 100644 --- a/apps/docs/pages/docs/guider/api-reference/functions/use-guider-page.mdx +++ b/apps/docs/pages/docs/guider/api-reference/functions/use-guider-page.mdx @@ -59,19 +59,19 @@ function useGuiderPage(): GuiderPageContext; - The site field in the Frontmatter + The site field in the Frontmatter. - The directory field in the Frontmatter + The directory field in the Frontmatter. - The layout field in the Frontmatter + The layout field in the Frontmatter. - The title field in the Frontmatter + The title field in the Frontmatter. - The description field in the Frontmatter + The description field in the Frontmatter. diff --git a/apps/docs/pages/docs/guider/api-reference/functions/use-guider.mdx b/apps/docs/pages/docs/guider/api-reference/functions/use-guider.mdx index 50c68313..12fd8a45 100644 --- a/apps/docs/pages/docs/guider/api-reference/functions/use-guider.mdx +++ b/apps/docs/pages/docs/guider/api-reference/functions/use-guider.mdx @@ -76,7 +76,7 @@ function useGuider(metaConf): GuiderContext; - The ID of the layout + The ID of the layout. The populated layout settings for this layout, read more [about layout settings](../theme/settings.mdx). @@ -95,7 +95,7 @@ function useGuider(metaConf): GuiderContext; - The ID of the directory + The ID of the directory. The ID of the layout that needs to be shown for pages in this directory. diff --git a/apps/docs/pages/docs/guider/guides/advanced/header.mdx b/apps/docs/pages/docs/guider/guides/advanced/header.mdx index 32a4a35b..5f9ffcfc 100644 --- a/apps/docs/pages/docs/guider/guides/advanced/header.mdx +++ b/apps/docs/pages/docs/guider/guides/advanced/header.mdx @@ -6,7 +6,7 @@ Learn about configuring the page header. If you are looking for ways to customise the navigation links in the header, you can [read more here](/docs/guider/guides/config/navigation). -## Github widget +## GitHub widget To showcase your GitHub repository with its stars and forks, you can configure a GitHub widget. diff --git a/apps/docs/pages/docs/guider/guides/deploy/github-pages.mdx b/apps/docs/pages/docs/guider/guides/deploy/github-pages.mdx index 8d11e3f2..fc06a1f2 100644 --- a/apps/docs/pages/docs/guider/guides/deploy/github-pages.mdx +++ b/apps/docs/pages/docs/guider/guides/deploy/github-pages.mdx @@ -1,10 +1,10 @@ -# Github Pages +# GitHub Pages -A Github Action to deploy a Guider documentation site. +A GitHub Action to deploy a Guider documentation site. ## Prerequisites -- Enable Github Pages, go to `Your repo > Settings > Pages` and set `Source` to `Github Actions`. +- Enable GitHub Pages, go to `Your repo > Settings > Pages` and set `Source` to `GitHub Actions`. ## The workflow diff --git a/apps/docs/pages/docs/guider/writing/components/fields.mdx b/apps/docs/pages/docs/guider/writing/components/fields.mdx index 12c4c398..1600fedd 100644 --- a/apps/docs/pages/docs/guider/writing/components/fields.mdx +++ b/apps/docs/pages/docs/guider/writing/components/fields.mdx @@ -48,10 +48,10 @@ You can also use the `{:tsx}` component to group properties A field with a type display, great for documenting types or structures. - The title of the field + The title of the field. - The type of the field + The type of the field. Mark this field as required. diff --git a/apps/docs/pages/docs/guider/writing/components/tabs.mdx b/apps/docs/pages/docs/guider/writing/components/tabs.mdx index 13ae36e8..78c84aa3 100644 --- a/apps/docs/pages/docs/guider/writing/components/tabs.mdx +++ b/apps/docs/pages/docs/guider/writing/components/tabs.mdx @@ -96,7 +96,7 @@ The Tabs component The tab items, must have the same number of items as there are tabs. - The storage key, to store the preferences against. See [this section](#storing-the-tab-choice) for usage. + The storage key, to store the preferences under. See [this section](#storing-the-tab-choice) for usage. The tabs to display. diff --git a/apps/docs/pages/docs/guider/writing/markdown/making-pages.mdx b/apps/docs/pages/docs/guider/writing/markdown/making-pages.mdx index f31eb69c..51649c54 100644 --- a/apps/docs/pages/docs/guider/writing/markdown/making-pages.mdx +++ b/apps/docs/pages/docs/guider/writing/markdown/making-pages.mdx @@ -73,6 +73,6 @@ export default function MyPage() { ## Writing in MD or MDX -Guider uses most of [Github Flavoured Markdown](https://github.github.com/gfm/). If you are familiar with it, you can use everything you know. +Guider uses most of [GitHub Flavoured Markdown](https://github.github.com/gfm/). If you are familiar with it, you can use everything you know. If you aren't familiar, you can refer to [Basic text](/docs/guider/writing/basic-text) to learn how to write in Markdown. diff --git a/apps/docs/theme.config.tsx b/apps/docs/theme.config.tsx index c458016f..bff28cfe 100644 --- a/apps/docs/theme.config.tsx +++ b/apps/docs/theme.config.tsx @@ -46,7 +46,7 @@ const gdWriting = (url: string) => `/docs/guider/writing${url}`; const gdApi = (url: string) => `/docs/guider/api-reference${url}`; const starLinks = [ - link('Github', 'https://github.com/mrjvs/neatojs', { + link('GitHub', 'https://github.com/mrjvs/neatojs', { style: 'star', newTab: true, icon: 'akar-icons:github-fill', @@ -135,7 +135,7 @@ export default defineTheme([ link('Deep-dive concepts', gdGuides('/advanced/deep-dive')), ]), group('Deploying', [ - link('Github Pages', gdGuides('/deploy/github-pages')), + link('GitHub Pages', gdGuides('/deploy/github-pages')), link('Netlify', gdGuides('/deploy/netlify')), link('Vercel', gdGuides('/deploy/vercel')), link('Cloudflare pages', gdGuides('/deploy/cloudflare')), From d0ef080c263a734d72596ed93eb43d4459208442 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Mon, 1 Apr 2024 19:13:23 +0200 Subject: [PATCH 02/23] Fix caps --- apps/docs/pages/docs/guider/guides/deploy/cloudflare.mdx | 2 +- apps/docs/pages/docs/guider/guides/deploy/netlify.mdx | 4 ++-- apps/docs/theme.config.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/docs/pages/docs/guider/guides/deploy/cloudflare.mdx b/apps/docs/pages/docs/guider/guides/deploy/cloudflare.mdx index 5e59e80b..b668baaa 100644 --- a/apps/docs/pages/docs/guider/guides/deploy/cloudflare.mdx +++ b/apps/docs/pages/docs/guider/guides/deploy/cloudflare.mdx @@ -11,7 +11,7 @@ Follow the steps below to deploy a Guider project to Cloudflare Pages. ### Make a new Application - Go to [the cloudflare dashboard](https://dash.cloudflare.com) on under "Workers & Pages" click "Create application". + Go to [the Cloudflare dashboard](https://dash.cloudflare.com) on under "Workers & Pages" click "Create application". ### Select your repository diff --git a/apps/docs/pages/docs/guider/guides/deploy/netlify.mdx b/apps/docs/pages/docs/guider/guides/deploy/netlify.mdx index c77cb980..caecad1a 100644 --- a/apps/docs/pages/docs/guider/guides/deploy/netlify.mdx +++ b/apps/docs/pages/docs/guider/guides/deploy/netlify.mdx @@ -1,4 +1,4 @@ -# Vercel +# Netlify A guide to deploy a Guider project to Netlify. @@ -26,7 +26,7 @@ Follow the steps below to deploy a Guider project to Netlify. ### Deploy - Click the `deploy` button and wait until your site has been deployed. + Click the `Deploy` button and wait until your site has been deployed. Netlify has linked this deploy with your branch, meaning that once new changes are pushed. Netlify will automatically update your site. diff --git a/apps/docs/theme.config.tsx b/apps/docs/theme.config.tsx index bff28cfe..bcd05280 100644 --- a/apps/docs/theme.config.tsx +++ b/apps/docs/theme.config.tsx @@ -138,7 +138,7 @@ export default defineTheme([ link('GitHub Pages', gdGuides('/deploy/github-pages')), link('Netlify', gdGuides('/deploy/netlify')), link('Vercel', gdGuides('/deploy/vercel')), - link('Cloudflare pages', gdGuides('/deploy/cloudflare')), + link('Cloudflare Pages', gdGuides('/deploy/cloudflare')), link('Docker', gdGuides('/deploy/docker')), ]), ], From 95fa981ab62eb7e4b11d75be4add25a25f839df9 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Wed, 3 Apr 2024 21:19:15 +0200 Subject: [PATCH 03/23] Auto enable nextjs image --- .../src/client/components/markdown/image.tsx | 14 ++++++++++++++ .../src/client/components/markdown/index.tsx | 4 ++++ packages/guider/src/index.ts | 4 ++++ 3 files changed, 22 insertions(+) create mode 100644 packages/guider/src/client/components/markdown/image.tsx diff --git a/packages/guider/src/client/components/markdown/image.tsx b/packages/guider/src/client/components/markdown/image.tsx new file mode 100644 index 00000000..69b9852c --- /dev/null +++ b/packages/guider/src/client/components/markdown/image.tsx @@ -0,0 +1,14 @@ +import classNames from 'classnames'; +import Image from 'next/image'; +import type { MarkdownProps } from './types'; + +export function MarkdownImage(props: MarkdownProps) { + return ( + {props.attrs.alt} + ); +} diff --git a/packages/guider/src/client/components/markdown/index.tsx b/packages/guider/src/client/components/markdown/index.tsx index b33b832f..63c8db6f 100644 --- a/packages/guider/src/client/components/markdown/index.tsx +++ b/packages/guider/src/client/components/markdown/index.tsx @@ -13,6 +13,7 @@ import { MarkdownH6, } from './headings'; import { MarkdownHr } from './hr'; +import { MarkdownImage } from './image'; import { MarkdownItalic, MarkdownStrike, MarkdownStrong } from './inline'; import { MarkdownLi, MarkdownOl, MarkdownUl } from './lists'; import { MarkdownLink, MarkdownParagraph } from './paragraph'; @@ -126,5 +127,8 @@ export function useMDXComponents() { ); return
{props.children}
; }, + img(props: ElementProps) { + return ; + }, }; } diff --git a/packages/guider/src/index.ts b/packages/guider/src/index.ts index 49a05cb4..180d10ef 100644 --- a/packages/guider/src/index.ts +++ b/packages/guider/src/index.ts @@ -17,6 +17,10 @@ export function guider(initConfig: GuiderInitConfig) { }); return { ...nextConfig, + images: { + ...(nextConfig.images ?? {}), + unoptimized: nextConfig?.images?.unoptimized ?? true, + }, transpilePackages: [ '@neato/guider', ...(nextConfig.transpilePackages ?? []), From f486d5be4e824f6a593778b1f7b0139b64aa2496 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Wed, 3 Apr 2024 21:42:02 +0200 Subject: [PATCH 04/23] Fix images with basepath --- .../guider/src/client/components/markdown/image.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/guider/src/client/components/markdown/image.tsx b/packages/guider/src/client/components/markdown/image.tsx index 69b9852c..4e591815 100644 --- a/packages/guider/src/client/components/markdown/image.tsx +++ b/packages/guider/src/client/components/markdown/image.tsx @@ -1,14 +1,17 @@ import classNames from 'classnames'; -import Image from 'next/image'; +import { useRouter } from 'next/router'; import type { MarkdownProps } from './types'; export function MarkdownImage(props: MarkdownProps) { + const router = useRouter(); + const srcInput = props.attrs.src; + const src = srcInput?.startsWith('/') ? router.basePath + srcInput : srcInput; + return ( - {props.attrs.alt} ); } From dc21307de159869d0331a0ebe6e4b8209b7c7092 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 6 Apr 2024 18:22:30 +0200 Subject: [PATCH 05/23] API reference common setups --- .../guider/guides/config/common/api-ref.mdx | 69 ++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/apps/docs/pages/docs/guider/guides/config/common/api-ref.mdx b/apps/docs/pages/docs/guider/guides/config/common/api-ref.mdx index e333fd0f..6447a2fd 100644 --- a/apps/docs/pages/docs/guider/guides/config/common/api-ref.mdx +++ b/apps/docs/pages/docs/guider/guides/config/common/api-ref.mdx @@ -1,5 +1,68 @@ # API reference + Docs - -This article is a stub, please help by contributing to the docs. - +A common requirement for expansive documentation sites is having both guides and an API reference. + +API reference is a lot more technical and usually isn't very helpful for beginners, but massively helpful if you know the library already and want to fact check something. + +The guides is almost the opposite: It's very friendly for beginners, but lacks the completeness of an API reference. + + +## Adding the two sections to your project + +The recommended way to add an API reference next to your guides is to use tabs. + +Tabs is a configuration option for a Guider site that allows for easy switching between two contexts. + + + + ### Adding the pages to sub-directories + + First, you have to make sure that your pages are divided in sub-directories. Like so: + - `/pages/guides` All your MDX files or pages for your guides + - `/pages/api` All your MDX files or pages for your API reference + + + ### Configuring your tabs + + Your site configuration needs to be split into two `directory()` instances and need to have the configured tabs. Like so: + + ```tsx title="theme.config.tsx" showLineNumbers + import { directories, link, defineTheme } from "@neato/guider/theme" + + export default defineTheme({ + tabs: [ + link("Guides", "/guides"), + link("API reference", "/api"), + ] + directories: [ + directory('guides', { + sidebar: [ + // your sidebar items + ], + }), + directory('api', { + sidebar: [ + // your sidebar items + ], + }) + ] + }) + ``` + + + ### Creating meta files + + To make your pages tied to your `directory()` instances (and tabs), you'll need to make two meta files for each folder. + + ```json title="/pages/guides/_meta.json" + { + "directory": "guides" + } + ``` + ```json title="/pages/api/_meta.json" + { + "directory": "api" + } + ``` + + From 9c1a29f8b409bae326ae0c058df2b95c50a75b6a Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 6 Apr 2024 18:32:39 +0200 Subject: [PATCH 06/23] Remove blog common setup --- apps/docs/pages/docs/guider/guides/config/common/blog.mdx | 5 ----- apps/docs/theme.config.tsx | 1 - 2 files changed, 6 deletions(-) delete mode 100644 apps/docs/pages/docs/guider/guides/config/common/blog.mdx diff --git a/apps/docs/pages/docs/guider/guides/config/common/blog.mdx b/apps/docs/pages/docs/guider/guides/config/common/blog.mdx deleted file mode 100644 index fd1bbf10..00000000 --- a/apps/docs/pages/docs/guider/guides/config/common/blog.mdx +++ /dev/null @@ -1,5 +0,0 @@ -# Blog posts + Docs - - -This article is a stub, please help by contributing to the docs. - diff --git a/apps/docs/theme.config.tsx b/apps/docs/theme.config.tsx index bcd05280..cd43ec13 100644 --- a/apps/docs/theme.config.tsx +++ b/apps/docs/theme.config.tsx @@ -121,7 +121,6 @@ export default defineTheme([ gdGuides('/config/common/multi-docs'), ), link('API reference + docs', gdGuides('/config/common/api-ref')), - link('Blog posts + docs', gdGuides('/config/common/blog')), ]), ]), group('Advanced', [ From 1063a773527fb8557a535886e97d6d87b875b668 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 6 Apr 2024 18:56:32 +0200 Subject: [PATCH 07/23] Improve landing pages text --- apps/docs/pages/docs/config/index.tsx | 21 ++++++++++++--------- apps/docs/pages/docs/guider/index.tsx | 21 ++++++++++++--------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/apps/docs/pages/docs/config/index.tsx b/apps/docs/pages/docs/config/index.tsx index ace9cd0a..7492ee5b 100644 --- a/apps/docs/pages/docs/config/index.tsx +++ b/apps/docs/pages/docs/config/index.tsx @@ -8,12 +8,12 @@ import { export default function LandingPage() { return ( - + Simple type-safe configuration - Configure with json files or environment variables, without losing - type-safety + Configure your app with JSON files or environment variables without + losing type-safety @@ -23,14 +23,17 @@ export default function LandingPage() { - - Effortlessly create beautiful documentation sites with just markdown. + + Define your config structure using strictly typed and validated + schemas. - - Effortlessly create beautiful documentation sites with just markdown. + + Easily configure applications in the cloud using multiple environment + agnostic sources. - - Effortlessly create beautiful documentation sites with just markdown. + + Supports a wide range of configuration sources including JSON files, + environment variables and CLI arguments. diff --git a/apps/docs/pages/docs/guider/index.tsx b/apps/docs/pages/docs/guider/index.tsx index 2a2b8bad..6d50691e 100644 --- a/apps/docs/pages/docs/guider/index.tsx +++ b/apps/docs/pages/docs/guider/index.tsx @@ -8,15 +8,15 @@ import { export default function LandingPage() { return ( - + Just went out of alpha! Documentation that looks great out of the box - Flexible but beautiful documentation, easy to write and easier to - extend. Exactly what you need everytime. + Flexible but beautiful documentation powered by NextJS — easy to write + and easier to extend. @@ -26,14 +26,17 @@ export default function LandingPage() { - - Effortlessly create beautiful documentation sites with just markdown. + + Effortlessly create beautiful documentation sites using Markdown or + MDX files. - - Effortlessly create beautiful documentation sites with just markdown. + + Guider doesn't make assumptions about your site. Use it by itself or + include it as part of a larger project. - - Effortlessly create beautiful documentation sites with just markdown. + + Comes out the box with a ready-to-go theme, but can easily be made to + look exactly like what you have envisioned. From b57b525f875081e6750d508007c6d0081452ec65 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 7 Apr 2024 02:04:08 +0200 Subject: [PATCH 08/23] Update marketing copy + add movie-web docs --- apps/docs/components/home-card.tsx | 4 ++-- apps/docs/pages/index.tsx | 6 +++--- apps/docs/pages/showcase.tsx | 12 ++++++++++-- apps/docs/public/showcases/movie-web-docs.png | Bin 0 -> 108628 bytes 4 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 apps/docs/public/showcases/movie-web-docs.png diff --git a/apps/docs/components/home-card.tsx b/apps/docs/components/home-card.tsx index 3603a13b..eb744236 100644 --- a/apps/docs/components/home-card.tsx +++ b/apps/docs/components/home-card.tsx @@ -8,8 +8,8 @@ function Card(props: { icon: string; }) { return ( -
- +
+
{props.children}
{props.right}
diff --git a/apps/docs/pages/index.tsx b/apps/docs/pages/index.tsx index 7bb44afb..166d2915 100644 --- a/apps/docs/pages/index.tsx +++ b/apps/docs/pages/index.tsx @@ -15,21 +15,21 @@ export default function LandingPage() { title: '@neato/guider', description: 'Flexible documentation that looks good out of the box.', href: '/docs/guider', - icon: 'mdi:cube-outline', + icon: 'ic:round-menu-book', }, { title: '@neato/config', description: 'NodeJS configuration loader with strict typing and autocomplete.', href: '/docs/config', - icon: 'mdi:cube-outline', + icon: 'material-symbols:settings-heart-rounded', }, ]; return ( - NeatoJS — A collection of JS tools that you will want. + NeatoJS — A collection of libraries made to simplify Tools that follow the philosophy of doing only one thing, and doing it diff --git a/apps/docs/pages/showcase.tsx b/apps/docs/pages/showcase.tsx index cb1ffe94..1884d4dd 100644 --- a/apps/docs/pages/showcase.tsx +++ b/apps/docs/pages/showcase.tsx @@ -5,22 +5,30 @@ import type { ShowcaseTag, ShowcaseType } from 'components/showcase-card'; import { ShowcaseCard, ShowcaseCardContainer } from 'components/showcase-card'; import pretendoImg from 'public/showcases/pretendo.png'; import mwAccountImg from 'public/showcases/movie-web-account.png'; +import mwDocsImg from 'public/showcases/movie-web-docs.png'; const showcases: ShowcaseType[] = [ { title: 'Pretendo', - description: 'Uses Guider for protocol documentation.', + description: 'Uses Guider to document their protocols and libaries.', href: 'https://developer.pretendo.network/', imageUrl: pretendoImg.src, tags: ['guider'], }, { - title: 'movie-web account', + title: 'movie-web backend', description: 'Uses Config for their account service.', href: 'https://github.com/movie-web/backend/', imageUrl: mwAccountImg.src, tags: ['config'], }, + { + title: 'movie-web docs', + description: 'Uses Guider for their documentation', + href: 'https://github.com/movie-web/docs/', + imageUrl: mwDocsImg.src, + tags: ['guider'], + }, ]; export default function ShowcasePage() { diff --git a/apps/docs/public/showcases/movie-web-docs.png b/apps/docs/public/showcases/movie-web-docs.png new file mode 100644 index 0000000000000000000000000000000000000000..05c7217ad5a0e70d373886447109c42911d8c7ad GIT binary patch literal 108628 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

5CK(f81S`vFDrr8CQa?2f(+ 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 lQczG(SsN Date: Sat, 20 Apr 2024 16:13:41 +0200 Subject: [PATCH 09/23] Add break words to the layout --- packages/guider/src/client/partials/layout/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/guider/src/client/partials/layout/layout.tsx b/packages/guider/src/client/partials/layout/layout.tsx index 7216ca07..a9714d61 100644 --- a/packages/guider/src/client/partials/layout/layout.tsx +++ b/packages/guider/src/client/partials/layout/layout.tsx @@ -39,7 +39,7 @@ export function LayoutInternal(props: GuiderLayoutProps) { >

-
+
{props.children}
From 5c9c94c8b9711d58c0c17a7427a103fdd7fcabd6 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Apr 2024 17:31:29 +0200 Subject: [PATCH 10/23] Group breadcrumb --- .../client/components/utils/activelink.tsx | 47 +++++++------ .../guider/src/client/hooks/use-guider.tsx | 3 + .../src/client/partials/layout/breadcrumb.tsx | 12 ++++ .../src/client/partials/layout/layout.tsx | 4 ++ .../src/client/partials/page-end/index.tsx | 10 +++ .../src/client/partials/page-end/page-end.tsx | 17 +++++ .../src/client/utils/navigational-buttons.ts | 67 +++++++++++++++++++ .../guider/src/theme/components/layout.ts | 1 + .../guider/src/theme/components/settings.ts | 3 + 9 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 packages/guider/src/client/partials/layout/breadcrumb.tsx create mode 100644 packages/guider/src/client/partials/page-end/index.tsx create mode 100644 packages/guider/src/client/partials/page-end/page-end.tsx create mode 100644 packages/guider/src/client/utils/navigational-buttons.ts diff --git a/packages/guider/src/client/components/utils/activelink.tsx b/packages/guider/src/client/components/utils/activelink.tsx index eb2832a3..6345ea2e 100644 --- a/packages/guider/src/client/components/utils/activelink.tsx +++ b/packages/guider/src/client/components/utils/activelink.tsx @@ -5,6 +5,33 @@ import { useMemo, useCallback, useState, useEffect } from 'react'; import classNames from 'classnames'; import { usePathname } from 'next/navigation.js'; +export function isRouteAtive( + link: string, + currentPathname: string, + exact?: boolean, +): boolean { + const linkPathname = new URL(link, 'https://example.com').pathname; + const activePathname = new URL(currentPathname, 'https://example.com') + .pathname; + + const linkPathArr = linkPathname.split('/').filter(Boolean); + const activePathArr = activePathname.split('/').filter(Boolean); + + if (exact) { + const exactMatch = linkPathArr.join('/') === activePathArr.join('/'); + return exactMatch; + } + + let matches = true; + for (let i = 0; i < linkPathArr.length; i++) { + if (linkPathArr[i] !== activePathArr[i]) { + matches = false; + } + } + + return matches; +} + export function useAreRoutesActive( ops: { as?: string; @@ -16,25 +43,7 @@ export function useAreRoutesActive( const isActiveCheck = useCallback( (href: string, exact?: boolean) => { - const linkPathname = new URL(href, location.href).pathname; - const activePathname = new URL(pathName, location.href).pathname; - - const linkPathArr = linkPathname.split('/').filter(Boolean); - const activePathArr = activePathname.split('/').filter(Boolean); - - if (exact) { - const exactMatch = linkPathArr.join('/') === activePathArr.join('/'); - return exactMatch; - } - - let matches = true; - for (let i = 0; i < linkPathArr.length; i++) { - if (linkPathArr[i] !== activePathArr[i]) { - matches = false; - } - } - - return matches; + return isRouteAtive(href, pathName, exact); }, [pathName], ); diff --git a/packages/guider/src/client/hooks/use-guider.tsx b/packages/guider/src/client/hooks/use-guider.tsx index 9d3aaa67..a7b2e27a 100644 --- a/packages/guider/src/client/hooks/use-guider.tsx +++ b/packages/guider/src/client/hooks/use-guider.tsx @@ -3,6 +3,7 @@ import { useRouter } from 'next/router.js'; import type { MetaConf, MetaMapItem } from '../../theme'; import { mergeLayoutSettings } from '../../theme/components/layout'; import { sites, metaMap } from '../virtuals'; +import { getCurrentPageContext } from '../utils/navigational-buttons'; export function getPage(pageUrl: string) { const matches = metaMap @@ -28,6 +29,7 @@ export function getGuiderContext(pageUrl: string, pageMeta: MetaConf = {}) { if (!layout) throw new Error('No layout found'); const settings = mergeLayoutSettings(layout.settings, dir.settings); + const navContext = getCurrentPageContext(pageUrl, dir); return { metaMap, @@ -35,6 +37,7 @@ export function getGuiderContext(pageUrl: string, pageMeta: MetaConf = {}) { directory: dir, layout, site, + navContext, }; } diff --git a/packages/guider/src/client/partials/layout/breadcrumb.tsx b/packages/guider/src/client/partials/layout/breadcrumb.tsx new file mode 100644 index 00000000..b2e5d71c --- /dev/null +++ b/packages/guider/src/client/partials/layout/breadcrumb.tsx @@ -0,0 +1,12 @@ +import { useGuiderPage } from '../../hooks/use-guider-page'; + +export function Breadcrumb() { + const { navContext } = useGuiderPage(); + + if (!navContext.current?.group) return null; + return ( +

+ {navContext.current.group} +

+ ); +} diff --git a/packages/guider/src/client/partials/layout/layout.tsx b/packages/guider/src/client/partials/layout/layout.tsx index a9714d61..d52b0f97 100644 --- a/packages/guider/src/client/partials/layout/layout.tsx +++ b/packages/guider/src/client/partials/layout/layout.tsx @@ -6,6 +6,8 @@ import { GuiderToc } from '../toc'; import { GuiderContentFooter } from '../content-footer'; import { GuiderpageFooter } from '../page-footer'; import { useGuiderPage } from '../../hooks/use-guider-page'; +import { GuiderPageEnd } from '../page-end'; +import { Breadcrumb } from './breadcrumb'; export type GuiderLayoutProps = { children?: ReactNode; @@ -40,7 +42,9 @@ export function LayoutInternal(props: GuiderLayoutProps) {
+ {props.children} +
; +} diff --git a/packages/guider/src/client/partials/page-end/page-end.tsx b/packages/guider/src/client/partials/page-end/page-end.tsx new file mode 100644 index 00000000..e6786df4 --- /dev/null +++ b/packages/guider/src/client/partials/page-end/page-end.tsx @@ -0,0 +1,17 @@ +import { useGuiderPage } from '../../hooks/use-guider-page'; + +export function PageEndInternal() { + const { navContext } = useGuiderPage(); + + return ( +
+ {navContext.prev ?
prev: {navContext.prev.item.title}
: null} + {navContext.current ? ( +
+ current: {navContext.current.item.title} ({navContext.current.group}) +
+ ) : null} + {navContext.next ?
next: {navContext.next.item.title}
: null} +
+ ); +} diff --git a/packages/guider/src/client/utils/navigational-buttons.ts b/packages/guider/src/client/utils/navigational-buttons.ts new file mode 100644 index 00000000..6e49d709 --- /dev/null +++ b/packages/guider/src/client/utils/navigational-buttons.ts @@ -0,0 +1,67 @@ +import type { + DirectoryComponent, + LinkComponent, + NestableLinkComponent, +} from 'src/theme'; +import { isRouteAtive } from '../components/utils/activelink'; + +type SidebarLink = LinkComponent | NestableLinkComponent; +type WrappedSidebarLink = { + item: SidebarLink; + group?: string; +}; +type DeepArray = DeepArray[] | T; + +function wrapLink(item: SidebarLink, group?: string): WrappedSidebarLink { + return { + group, + item, + }; +} + +function flattenSidebar(items: DirectoryComponent['sidebar'], group?: string) { + return items + .map((item): DeepArray => { + if (item.type === 'link') return wrapLink(item, group); + if (item.type === 'group') return flattenSidebar(item.items, item.title); + if (item.type === 'nested-link' && item.to) + return [wrapLink(item, group), ...flattenSidebar(item.items, group)]; + if (item.type === 'nested-link' && !item.to) + return flattenSidebar(item.items, group); + return []; + }) + .filter((v) => { + if (Array.isArray(v)) return true; + if (v.item.type === 'link' || v.item.type === 'nested-link') return true; + return false; + }); +} + +export function getCurrentPageContext( + pagePathName: string, + dir: DirectoryComponent, +) { + const navMap = flattenSidebar(dir.sidebar).flat(4) as WrappedSidebarLink[]; + + for (let i = 0; i < navMap.length; i++) { + const item = navMap[i]; + const isActive = isRouteAtive( + item.item.to ?? '/', + pagePathName, + item.item.exact ?? false, + ); + if (!isActive) continue; + + return { + prev: i > 0 ? navMap[i - 1] : null, + current: item, + next: i + 1 < navMap.length ? navMap[i + 1] : null, + }; + } + + return { + prev: null, + current: null, + next: null, + }; +} diff --git a/packages/guider/src/theme/components/layout.ts b/packages/guider/src/theme/components/layout.ts index 27aad9a3..fbd821fa 100644 --- a/packages/guider/src/theme/components/layout.ts +++ b/packages/guider/src/theme/components/layout.ts @@ -54,6 +54,7 @@ const baseLayoutConfig: PopulatedLayoutSettings = { navigationState: true, backgroundPatternState: false, logoState: true, + pageEndState: true, }; function extractState( diff --git a/packages/guider/src/theme/components/settings.ts b/packages/guider/src/theme/components/settings.ts index e17954fe..05499f56 100644 --- a/packages/guider/src/theme/components/settings.ts +++ b/packages/guider/src/theme/components/settings.ts @@ -41,6 +41,7 @@ export type LayoutSettings = { navigation: ToggleablePartial; contentFooter: ToggleablePartial; pageFooter: ToggleablePartial; + pageEnd: ToggleablePartial; logo: ToggleablePartial; pageLayout?: Partial; backgroundPattern: ToggleSetting | BackgroundPatterns; @@ -83,6 +84,7 @@ export type PopulatedLayoutSettings = { navigationState: ToggleSetting; contentFooterState: ToggleSetting; pageFooterState: ToggleSetting; + pageEndState: ToggleSetting; logoState: ToggleSetting; pageLayoutComponent?: Partial; @@ -93,4 +95,5 @@ export type PopulatedLayoutSettings = { contentFooterComponent?: Partial; pageFooterComponent?: Partial; logoComponent?: Partial; + pageEndComponent?: Partial; }; From cd18ed89d3c0cac98619f236262dfc0ce39d0fc2 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Apr 2024 17:40:42 +0200 Subject: [PATCH 11/23] Update documentation on new settings --- .../components/guider-page-end.mdx | 23 +++++++++++++++++++ .../functions/use-guider-page.mdx | 5 ++++ .../api-reference/functions/use-guider.mdx | 15 ++++++++++++ .../guider/api-reference/theme/settings.mdx | 3 +++ .../guides/advanced/customizing-layout.mdx | 1 + apps/docs/theme.config.tsx | 1 + 6 files changed, 48 insertions(+) create mode 100644 apps/docs/pages/docs/guider/api-reference/components/guider-page-end.mdx diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-page-end.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-page-end.mdx new file mode 100644 index 00000000..feca6894 --- /dev/null +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-page-end.mdx @@ -0,0 +1,23 @@ +# `` + +React component that will show the currently configured page end. + +It respects the configured `pageEnd` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). + +Will show one of: + - Nothing if the page end is set to be hidden. + - The default page end with the configured settings. + - The custom component if the page end has been overriden in the partials. + +## Example + +```tsx + +``` + + +## Reference + +```tsx +function GuiderPageEnd(): ReactNode; +``` diff --git a/apps/docs/pages/docs/guider/api-reference/functions/use-guider-page.mdx b/apps/docs/pages/docs/guider/api-reference/functions/use-guider-page.mdx index 537efe7a..3084d915 100644 --- a/apps/docs/pages/docs/guider/api-reference/functions/use-guider-page.mdx +++ b/apps/docs/pages/docs/guider/api-reference/functions/use-guider-page.mdx @@ -50,6 +50,11 @@ function useGuiderPage(): GuiderPageContext; See [the `useGuider()` page](./use-guider.mdx) for more info. + + The navigational context of the page, use it to get the current links in the sidebar. + + See [the `useGuider()` page](./use-guider.mdx) for more info. + Data for the current page content. diff --git a/apps/docs/pages/docs/guider/api-reference/functions/use-guider.mdx b/apps/docs/pages/docs/guider/api-reference/functions/use-guider.mdx index 12fd8a45..d7cafff6 100644 --- a/apps/docs/pages/docs/guider/api-reference/functions/use-guider.mdx +++ b/apps/docs/pages/docs/guider/api-reference/functions/use-guider.mdx @@ -198,5 +198,20 @@ function useGuider(metaConf): GuiderContext; + + The navigational context of the page, use it to get the current links in the sidebar. + + + + The previous page link, according to the order of the sidebar links. It can be null + + + The current page link. It can be null if it isn't present in the sidebar. + + + The next page link, according to the order of the sidebar links. It can be null. + + + diff --git a/apps/docs/pages/docs/guider/api-reference/theme/settings.mdx b/apps/docs/pages/docs/guider/api-reference/theme/settings.mdx index 709dd26a..e2f301d4 100644 --- a/apps/docs/pages/docs/guider/api-reference/theme/settings.mdx +++ b/apps/docs/pages/docs/guider/api-reference/theme/settings.mdx @@ -47,6 +47,9 @@ When you overwrite a partial, you can use `useGuiderPage()` to get the current p The page footer partial. Check [this section](#about-partials) for information on how to use partials. + + The page end partial. Check [this section](#about-partials) for information on how to use partials. + The logo partial. Check [this section](#about-partials) for information on how to use partials. diff --git a/apps/docs/pages/docs/guider/guides/advanced/customizing-layout.mdx b/apps/docs/pages/docs/guider/guides/advanced/customizing-layout.mdx index 3e8a1d8b..ce711eb2 100644 --- a/apps/docs/pages/docs/guider/guides/advanced/customizing-layout.mdx +++ b/apps/docs/pages/docs/guider/guides/advanced/customizing-layout.mdx @@ -11,6 +11,7 @@ Here are all possibilities: - `sidebar`: The sidebar navigation menu. - `navigation`: The header navigation menu. - `contentFooter`: The content footer, placed right below content. + - `pageEnd`: The navigation links below the content, but above the content footer. - `pageFooter`: The page footer, placed at the very bottom of the page. Disabled by default. - `logo`: The logo, shown in the navigation bar and page footer. - `pageLayout`: The entire page, cannot be turned off. diff --git a/apps/docs/theme.config.tsx b/apps/docs/theme.config.tsx index cd43ec13..0e198d66 100644 --- a/apps/docs/theme.config.tsx +++ b/apps/docs/theme.config.tsx @@ -244,6 +244,7 @@ export default defineTheme([ '', gdApi('/components/guider-page-footer'), ), + link('', gdApi('/components/guider-page-end')), ]), ], }), From 79021d7e21537e5afdbed759c1a807176889a43d Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Apr 2024 18:05:00 +0200 Subject: [PATCH 12/23] Styling for page end --- .../src/client/partials/page-end/page-end.tsx | 61 ++++++++++++++++--- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/packages/guider/src/client/partials/page-end/page-end.tsx b/packages/guider/src/client/partials/page-end/page-end.tsx index e6786df4..61add462 100644 --- a/packages/guider/src/client/partials/page-end/page-end.tsx +++ b/packages/guider/src/client/partials/page-end/page-end.tsx @@ -1,17 +1,62 @@ +import Link from 'next/link'; +import classNames from 'classnames'; import { useGuiderPage } from '../../hooks/use-guider-page'; +import { Icon } from '../../components/icon'; + +function PageEndLink(props: { to: string; title: string; flip?: boolean }) { + return ( + + {!props.flip ? ( + + ) : null} + {props.title} + {props.flip ? ( + + ) : null} + + ); +} export function PageEndInternal() { const { navContext } = useGuiderPage(); + const hasContent = navContext.prev || navContext.next; + if (!hasContent) return null; + return ( -
- {navContext.prev ?
prev: {navContext.prev.item.title}
: null} - {navContext.current ? ( -
- current: {navContext.current.item.title} ({navContext.current.group}) -
- ) : null} - {navContext.next ?
next: {navContext.next.item.title}
: null} +
+
+ {navContext.prev ? ( + + ) : null} +
+
+ {navContext.next ? ( + + ) : null} +
); } From 9c40c0d072fbfaca36908d9313b8a57ba51ed57f Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Apr 2024 18:08:28 +0200 Subject: [PATCH 13/23] Clickable "powered by guider" link --- .../partials/content-footer/content-footer.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/guider/src/client/partials/content-footer/content-footer.tsx b/packages/guider/src/client/partials/content-footer/content-footer.tsx index 8fb83475..11500c6a 100644 --- a/packages/guider/src/client/partials/content-footer/content-footer.tsx +++ b/packages/guider/src/client/partials/content-footer/content-footer.tsx @@ -4,6 +4,8 @@ import { useGuiderPage } from '../../hooks/use-guider-page'; import type { SocialTypes } from '../../../theme/components/social'; import { GithubEditLink, useEditLink } from './github-edit-link'; +const guiderDocumentationLink = 'https://neatojs.com/docs/guider'; + const iconMap: Record = { discord: 'ic:twotone-discord', github: 'mdi:github', @@ -44,7 +46,15 @@ export function ContentFooterInternal() { ) : null}
{site.contentFooter?.text ?? copyright}{' '} - Powered by Guider + {' '} + + Powered by Guider +
{editUrl ? ( From 86d7e3a933987076a00464a4737a6de0d518d365 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Apr 2024 18:24:44 +0200 Subject: [PATCH 14/23] Update link styling --- packages/guider/src/client/components/markdown/paragraph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/guider/src/client/components/markdown/paragraph.tsx b/packages/guider/src/client/components/markdown/paragraph.tsx index 2e4251ea..134ec7b5 100644 --- a/packages/guider/src/client/components/markdown/paragraph.tsx +++ b/packages/guider/src/client/components/markdown/paragraph.tsx @@ -20,7 +20,7 @@ export function MarkdownLink(props: MarkdownProps) { {props.children} From 0b1885bf143aea7fd9a36fbf92899c346455ed6e Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Apr 2024 18:33:44 +0200 Subject: [PATCH 15/23] Fix styling for buttons --- packages/guider/src/client/components/public/button.tsx | 6 +++--- packages/guider/src/client/components/public/hero.tsx | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/guider/src/client/components/public/button.tsx b/packages/guider/src/client/components/public/button.tsx index 723983fb..67bb66f4 100644 --- a/packages/guider/src/client/components/public/button.tsx +++ b/packages/guider/src/client/components/public/button.tsx @@ -20,9 +20,9 @@ const buttonStyles = { export function Button(props: ButtonProps) { const classes = classNames( buttonStyles[props.type ?? 'primary'], - 'gd-bg-gradient-to-b gd-text-opacity-90 gd-px-4 gd-py-2 gd-rounded-md gd-border', - 'gd-transition-[background,transform]', - 'active:gd-scale-105', + 'gd-bg-gradient-to-b gd-text-opacity-80 gd-px-7 gd-py-2.5 gd-rounded-xl gd-border', + 'gd-transition-[background-image,transform]', + 'active:gd-scale-105 gd-inline-block gd-text-center', props.className, ); diff --git a/packages/guider/src/client/components/public/hero.tsx b/packages/guider/src/client/components/public/hero.tsx index fea263b8..66a0b62f 100644 --- a/packages/guider/src/client/components/public/hero.tsx +++ b/packages/guider/src/client/components/public/hero.tsx @@ -51,7 +51,11 @@ function Badge(props: { } function Actions(props: { children: React.ReactNode }) { - return
{props.children}
; + return ( +
+ {props.children} +
+ ); } function HeroFunc(props: { children: React.ReactNode }) { From c63b94f4d713112e016c00501e94394149e954e4 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Apr 2024 18:44:38 +0200 Subject: [PATCH 16/23] Add level 3 headings --- packages/guider/src/client/partials/toc/toc.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/guider/src/client/partials/toc/toc.tsx b/packages/guider/src/client/partials/toc/toc.tsx index 0181eb50..5e12efd0 100644 --- a/packages/guider/src/client/partials/toc/toc.tsx +++ b/packages/guider/src/client/partials/toc/toc.tsx @@ -10,7 +10,11 @@ function TocLink(props: { onClick?: () => void; }) { return ( -

+

{ e.preventDefault(); @@ -34,7 +38,7 @@ function TocLink(props: { export function TocInternal() { const ctx = useContext(GuiderLayoutContext); const headings = useMemo( - () => [...(ctx?.headings ?? [])].slice(1).filter((v) => v.depth <= 2), + () => [...(ctx?.headings ?? [])].slice(1).filter((v) => v.depth <= 3), [ctx?.headings], ); const ids = useMemo(() => headings.map((v) => v.data.id), [headings]); From db0611a0e2bfd40c44308ee7780004553dc42fa0 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Apr 2024 18:52:05 +0200 Subject: [PATCH 17/23] Fix list overflow wrapping + fix broken list in docs --- .../getting-started/migration/from-docus.mdx | 26 +++++++++---------- .../src/client/components/markdown/lists.tsx | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/docs/pages/docs/guider/guides/getting-started/migration/from-docus.mdx b/apps/docs/pages/docs/guider/guides/getting-started/migration/from-docus.mdx index 68916054..0d130aeb 100644 --- a/apps/docs/pages/docs/guider/guides/getting-started/migration/from-docus.mdx +++ b/apps/docs/pages/docs/guider/guides/getting-started/migration/from-docus.mdx @@ -33,16 +33,16 @@ Migrating can be a long journey, this guide aims to create a clear set of steps ## Migration steps -1. **Make a guider project:** Make a new guider project by following [this guide](../installation.mdx). -2. **Copy your markdown files over:** Copy over all Markdown files into the pages directory of the new project. - - Remove the numbers prefixed on front of your files and folders. The order is now in the theme config. - - Replace the component directives with MDX React components. - - Update frontmatter to set the layout ID instead of modifying the layout settings directly. See [this page](../../advanced/customizing-layout.mdx). - - Replace title in the frontmatter to a link in the theme config. -3. **Migrate _dir.yml files:** dir files don't exist in Guider. Check these steps: - - title goes in a `group()` in the theme config. - - replace `navigation.redirect` with a `createRedirect()` in a index.tsx - - title template can be set globally, not a per directory basis. -4. **Migrate app.config.ts:** The settings in Guider go into the theme config, along with your sidebar contents. -5. **Migrate vue components to react:** Guider is based in react, so to have custom components work you will need to port them. -6. **Finished:** That should be everything to migrate to Guider, congratulations! + 1. **Make a guider project:** Make a new guider project by following [this guide](../installation.mdx). + 2. **Copy your markdown files over:** Copy over all Markdown files into the pages directory of the new project. + - Remove the numbers prefixed on front of your files and folders. The order is now in the theme config. + - Replace the component directives with MDX React components. + - Update frontmatter to set the layout ID instead of modifying the layout settings directly. See [this page](../../advanced/customizing-layout.mdx). + - Replace title in the frontmatter to a link in the theme config. + 3. **Migrate _dir.yml files:** dir files don't exist in Guider. Check these steps: + - title goes in a `group()` in the theme config. + - replace `navigation.redirect` with a `createRedirect()` in a index.tsx + - title template can be set globally, not a per directory basis. + 4. **Migrate app.config.ts:** The settings in Guider go into the theme config, along with your sidebar contents. + 5. **Migrate vue components to react:** Guider is based in react, so to have custom components work you will need to port them. + 6. **Finished:** That should be everything to migrate to Guider, congratulations! diff --git a/packages/guider/src/client/components/markdown/lists.tsx b/packages/guider/src/client/components/markdown/lists.tsx index b34eb4e1..82b0cd7d 100644 --- a/packages/guider/src/client/components/markdown/lists.tsx +++ b/packages/guider/src/client/components/markdown/lists.tsx @@ -19,7 +19,7 @@ export function MarkdownUl(props: MarkdownProps) { export function MarkdownLi(props: MarkdownProps) { return (

  • - + Date: Sat, 20 Apr 2024 22:21:49 +0200 Subject: [PATCH 18/23] Start of a search UI --- .../src/client/partials/header/header.tsx | 2 + .../client/partials/header/search/button.tsx | 7 +++ .../client/partials/header/search/index.tsx | 59 +++++++++++++++++++ .../client/partials/header/search/modal.tsx | 12 ++++ .../client/partials/header/search/screen.tsx | 17 ++++++ 5 files changed, 97 insertions(+) create mode 100644 packages/guider/src/client/partials/header/search/button.tsx create mode 100644 packages/guider/src/client/partials/header/search/index.tsx create mode 100644 packages/guider/src/client/partials/header/search/modal.tsx create mode 100644 packages/guider/src/client/partials/header/search/screen.tsx diff --git a/packages/guider/src/client/partials/header/header.tsx b/packages/guider/src/client/partials/header/header.tsx index 8b5a9806..35b30187 100644 --- a/packages/guider/src/client/partials/header/header.tsx +++ b/packages/guider/src/client/partials/header/header.tsx @@ -9,6 +9,7 @@ import { HeaderNav } from './nav'; import { HeaderDropdown } from './dropdown'; import { SidebarMobileNav } from './sidebar-mobile-nav'; import { TopMobileNav } from './top-mobile-nav'; +import { HeaderSearch } from './search'; export function HeaderInternal() { const ctx = useContext(GuiderLayoutContext); @@ -51,6 +52,7 @@ export function HeaderInternal() { ) : null} +
  • diff --git a/packages/guider/src/client/partials/header/search/button.tsx b/packages/guider/src/client/partials/header/search/button.tsx new file mode 100644 index 00000000..46a9e04e --- /dev/null +++ b/packages/guider/src/client/partials/header/search/button.tsx @@ -0,0 +1,7 @@ +export function SearchButton(props: { onClick?: () => void }) { + return ( + + ); +} diff --git a/packages/guider/src/client/partials/header/search/index.tsx b/packages/guider/src/client/partials/header/search/index.tsx new file mode 100644 index 00000000..228c3bca --- /dev/null +++ b/packages/guider/src/client/partials/header/search/index.tsx @@ -0,0 +1,59 @@ +import { useEffect, useRef, useState } from 'react'; +import { SearchButton } from './button'; +import { SearchModal } from './modal'; + +function UpdateHead(props: { active?: boolean }) { + useEffect(() => { + if (props.active) + document.body.setAttribute('data-header-search-open', 'true'); + return () => { + document.body.removeAttribute('data-header-search-open'); + }; + }); + return null; +} + +export function HeaderSearch() { + const [open, setOpen] = useState(false); + const openRef = useRef(open); + + useEffect(() => { + openRef.current = open; + }, [open]); + + useEffect(() => { + const listener = (e: KeyboardEvent) => { + if (openRef.current && e.key === 'Escape') { + setOpen(false); + e.preventDefault(); + return; + } + if (e.key === 'k' && e.ctrlKey) { + setOpen(true); + e.preventDefault(); + } + }; + document.addEventListener('keydown', listener); + return () => { + document.removeEventListener('keydown', listener); + }; + }, []); + + return ( +
    + { + setOpen(true); + }} + /> + + {open ? ( + { + setOpen(false); + }} + /> + ) : null} +
    + ); +} diff --git a/packages/guider/src/client/partials/header/search/modal.tsx b/packages/guider/src/client/partials/header/search/modal.tsx new file mode 100644 index 00000000..e52d363f --- /dev/null +++ b/packages/guider/src/client/partials/header/search/modal.tsx @@ -0,0 +1,12 @@ +import { SearchScreen } from './screen'; + +export function SearchModal(props: { onClose?: () => void }) { + return ( +
    +
    +
    + +
    +
    + ); +} diff --git a/packages/guider/src/client/partials/header/search/screen.tsx b/packages/guider/src/client/partials/header/search/screen.tsx new file mode 100644 index 00000000..992ddad6 --- /dev/null +++ b/packages/guider/src/client/partials/header/search/screen.tsx @@ -0,0 +1,17 @@ +import { useState } from 'react'; + +export function SearchScreen() { + const [input, setInput] = useState(''); + return ( +
    + { + setInput(e.target.value); + }} + type="text" + autoFocus + /> +
    + ); +} From 11926ccfb117fddcbba7476458474fdea8d05cd7 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 21 Apr 2024 14:35:54 +0200 Subject: [PATCH 19/23] Add search index asset file generation --- packages/guider/package.json | 1 + packages/guider/src/index.ts | 3 + packages/guider/src/webpack/loader/index.ts | 2 +- .../guider/src/webpack/loader/md-loader.ts | 11 ++- .../guider/src/webpack/plugin/collector.ts | 4 +- packages/guider/src/webpack/search/index.ts | 84 +++++++++++++++++++ packages/guider/tsup.config.ts | 2 +- pnpm-lock.yaml | 7 ++ 8 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 packages/guider/src/webpack/search/index.ts diff --git a/packages/guider/package.json b/packages/guider/package.json index 5c6f35c3..af8eaac7 100644 --- a/packages/guider/package.json +++ b/packages/guider/package.json @@ -99,6 +99,7 @@ "classnames": "^2.5.1", "color": "^4.2.3", "extra-watch-webpack-plugin": "^1.0.3", + "flexsearch": "^0.7.43", "git-url-parse": "^14.0.0", "glob": "^10.3.10", "gray-matter": "^4.0.3", diff --git a/packages/guider/src/index.ts b/packages/guider/src/index.ts index 180d10ef..9b0bf35c 100644 --- a/packages/guider/src/index.ts +++ b/packages/guider/src/index.ts @@ -2,6 +2,7 @@ import type { NextConfig } from 'next'; import ExtraWatchWebpackPlugin from 'extra-watch-webpack-plugin'; import { GuiderPlugin } from './webpack/plugin/plugin'; import type { GuiderInitConfig } from './types'; +import { GuiderSearchPlugin } from './webpack/search'; export { getGuiderPluginCache } from './webpack/plugin/plugin'; @@ -10,6 +11,7 @@ export function guider(initConfig: GuiderInitConfig) { ...initConfig, }; const guiderPlugin = new GuiderPlugin(guiderConfig); + const searchPlugin = new GuiderSearchPlugin(); function withGuider(nextConfig: NextConfig = {}): NextConfig { const extraWatchers = new ExtraWatchWebpackPlugin({ @@ -32,6 +34,7 @@ export function guider(initConfig: GuiderInitConfig) { webpack(config, options) { if (!config.plugins) config.plugins = []; config.plugins.push(guiderPlugin); + config.plugins.push(searchPlugin); config.plugins.push(extraWatchers); config.module.rules.push({ diff --git a/packages/guider/src/webpack/loader/index.ts b/packages/guider/src/webpack/loader/index.ts index 3a119026..bb1b1209 100644 --- a/packages/guider/src/webpack/loader/index.ts +++ b/packages/guider/src/webpack/loader/index.ts @@ -23,7 +23,7 @@ async function loader( if (directories.pagesDir) context.addContextDependency(directories.pagesDir); if (type === 'virtual') return virtualLoader(getGuiderPluginCache()); - if (type === 'mdx') return mdLoader(source); + if (type === 'mdx') return (await mdLoader(source)).script; throw new Error(`Loader used with incorrect type (${type})`); } diff --git a/packages/guider/src/webpack/loader/md-loader.ts b/packages/guider/src/webpack/loader/md-loader.ts index 82995566..444ab725 100644 --- a/packages/guider/src/webpack/loader/md-loader.ts +++ b/packages/guider/src/webpack/loader/md-loader.ts @@ -18,7 +18,7 @@ import { const EXPORT_FOOTER = 'export default '; -export async function mdLoader(source: string): Promise { +export async function mdLoader(source: string) { const meta = grayMatter(source); const file = await compile(source, { jsx: true, @@ -98,7 +98,7 @@ export async function mdLoader(source: string): Promise { excerpt: file.data.excerpt, }; - return ` + const script = ` import { createMdxPage } from "@neato/guider/client"; ${finalMdxCode} @@ -110,4 +110,11 @@ export async function mdLoader(source: string): Promise { export default createMdxPage(__guiderPageOptions); `; + + return { + script, + searchData: { + excerpt: file.data.excerpt, + }, + }; } diff --git a/packages/guider/src/webpack/plugin/collector.ts b/packages/guider/src/webpack/plugin/collector.ts index 4c1ed078..fd399896 100644 --- a/packages/guider/src/webpack/plugin/collector.ts +++ b/packages/guider/src/webpack/plugin/collector.ts @@ -35,9 +35,7 @@ function normalizePathSeparator(path: string): string { return path.replace(pathSeparatorRegex, '/'); } -async function filePathToSitePath( - filePath: string, -): Promise { +async function filePathToSitePath(filePath: string): Promise { let strippedPath = dirname(relative('./pages', filePath)); const fileContents = await readFile(filePath, 'utf-8'); const parsedContents = JSON.parse(fileContents); diff --git a/packages/guider/src/webpack/search/index.ts b/packages/guider/src/webpack/search/index.ts new file mode 100644 index 00000000..f5d38f5c --- /dev/null +++ b/packages/guider/src/webpack/search/index.ts @@ -0,0 +1,84 @@ +import { relative, sep } from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import webpack from 'webpack'; +import type { Compiler } from 'webpack'; +import { glob } from 'glob'; +import { mdLoader } from '../loader/md-loader'; + +const pluginName = 'GuiderSearchPlugin'; +const defaultKey = 'default'; + +const pathSeparatorRegex = RegExp(`\\${sep}`, 'g'); +function normalizePathSeparator(path: string): string { + return path.replace(pathSeparatorRegex, '/'); +} + +async function filePathToPageData(filePath: string) { + let strippedPath = relative('./pages', filePath).replace(/.mdx?$/g, ''); + const fileContents = await readFile(filePath, 'utf-8'); + + strippedPath = normalizePathSeparator(strippedPath); + + return { + sitePath: `/${strippedPath}`, + fileContents, + }; +} + +function generateChecksum(str) { + return createHash('md5').update(str, 'utf8').digest('hex'); +} + +const cache: Record = {}; + +export class GuiderSearchPlugin { + apply(compiler: Compiler) { + compiler.hooks.make.tap(pluginName, (compilation) => { + compilation.hooks.processAssets.tapAsync( + { + name: pluginName, + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + }, + async (assets, callback) => { + try { + const dataBuckets: Record = {}; + + const mdxPages = await glob('pages/**/*.{mdx,md}'); + const pageData = await Promise.all( + mdxPages.map(async (filePath) => { + const sitePathData = await filePathToPageData(filePath); + const hash = generateChecksum(sitePathData.fileContents); + const key = `${filePath}-${hash}`; + const compiled = + cache[key] ?? (await mdLoader(sitePathData.fileContents)); + cache[key] = compiled; + return { + searchData: compiled.searchData, + sitePath: sitePathData.sitePath, + }; + }), + ); + + for (const page of pageData) { + const key = defaultKey; + console.log(page.sitePath); + dataBuckets[key] ??= {}; + dataBuckets[key][page.sitePath] = page.searchData; + } + + for (const [fileKey, content] of Object.entries(dataBuckets)) { + assets[`static/chunks/guider-data-${fileKey}.json`] = + new webpack.sources.RawSource(JSON.stringify(content)); + } + } catch (err) { + callback(err as Error); + return; + } + + callback(); + }, + ); + }); + } +} diff --git a/packages/guider/tsup.config.ts b/packages/guider/tsup.config.ts index a5d1e8a3..8c0461c3 100644 --- a/packages/guider/tsup.config.ts +++ b/packages/guider/tsup.config.ts @@ -8,7 +8,7 @@ export default defineConfig([ outExtension: () => ({ js: '.js', dts: '.d.ts' }), dts: true, bundle: true, - external: ['@neato/guider', '@neato/guider/shim.guider.virtual'], + external: ['@neato/guider', '@neato/guider/shim.guider.virtual', 'webpack'], }, { name: 'guider-loader', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b65a689..f5347f6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,6 +188,9 @@ importers: extra-watch-webpack-plugin: specifier: ^1.0.3 version: 1.0.3 + flexsearch: + specifier: ^0.7.43 + version: 0.7.43 git-url-parse: specifier: ^14.0.0 version: 14.0.0 @@ -3102,6 +3105,10 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true + /flexsearch@0.7.43: + resolution: {integrity: sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==} + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: From d30c54103952072e7cab77dbd5c66f82d642be31 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 21 Apr 2024 23:35:58 +0200 Subject: [PATCH 20/23] Add basic search UI --- .../client/partials/header/search/button.tsx | 15 +- .../client/partials/header/search/content.ts | 141 ++++++++++++++++++ .../client/partials/header/search/index.tsx | 4 + .../client/partials/header/search/modal.tsx | 7 +- .../client/partials/header/search/screen.tsx | 85 +++++++++-- packages/guider/src/webpack/search/index.ts | 3 +- 6 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 packages/guider/src/client/partials/header/search/content.ts diff --git a/packages/guider/src/client/partials/header/search/button.tsx b/packages/guider/src/client/partials/header/search/button.tsx index 46a9e04e..7bf197ca 100644 --- a/packages/guider/src/client/partials/header/search/button.tsx +++ b/packages/guider/src/client/partials/header/search/button.tsx @@ -1,7 +1,18 @@ +import { Icon } from '../../../components/icon'; + export function SearchButton(props: { onClick?: () => void }) { return ( - ); } diff --git a/packages/guider/src/client/partials/header/search/content.ts b/packages/guider/src/client/partials/header/search/content.ts new file mode 100644 index 00000000..2f892d41 --- /dev/null +++ b/packages/guider/src/client/partials/header/search/content.ts @@ -0,0 +1,141 @@ +import FlexSearch from 'flexsearch'; +import { useRouter } from 'next/router'; +import { useCallback, useEffect, useState } from 'react'; + +type ContentDocument = { + mainDoc: FlexSearch.Document< + { + id: number; + url: string; + excerpt: string; + }, + never[] + >; +}; + +type SearchData = Record< + string, + { + excerpt: string; + } +>; + +export type SearchResult = { + id: string; + type: 'page' | 'section'; + title: string; + content: string; + url: string; +}; + +let contentPromise: Promise | null = null; +let contentDocument: ContentDocument | null = null; + +function loadDocument(basePath: string, key: string): Promise { + if (contentPromise) return contentPromise; + const promise = fetchDocument(basePath, key); + contentPromise = promise; + return promise; +} + +async function fetchDocument(basePath: string, key: string) { + const res = await fetch( + `${basePath}/_next/static/chunks/guider-data-${key}.json`, + ); + const searchData = (await res.json()) as SearchData; + const searchDocument: ContentDocument['mainDoc'] = new FlexSearch.Document({ + cache: 100, + tokenize: 'full', + document: { + id: 'id', + index: 'excerpt', + store: ['excerpt', 'url'], + }, + context: { + resolution: 9, + depth: 2, + bidirectional: true, + }, + }); + + let pageId = 0; + for (const [url, data] of Object.entries(searchData)) { + pageId++; + searchDocument.add({ + id: pageId, + url, + excerpt: data.excerpt, + }); + } + + contentDocument = { + mainDoc: searchDocument, + }; +} + +export function usePreloadSearch(key: string) { + const { basePath } = useRouter(); + useEffect(() => { + void loadDocument(basePath, key); + }, [basePath, key]); +} + +export function useSearch(key: string) { + const { basePath } = useRouter(); + const [query, setQuery] = useState(''); + const [resultLoading, setResultLoading] = useState(false); + const [resultLoadingError, setResultLoadingError] = useState(false); + const [results, setResults] = useState(null); + + const doSearch = useCallback( + async (contentKey: string, searchQuery: string) => { + setResultLoading(true); + setResultLoadingError(false); + try { + await loadDocument(basePath, contentKey); + const doc = contentDocument; + if (!doc) throw new Error('Doc not loaded'); + const docResults = + doc.mainDoc.search(searchQuery, 5, { + enrich: true, + suggest: true, + })?.[0]?.result ?? []; + setResults( + docResults.map((res): SearchResult => { + return { + type: 'page', + id: res.id.toString(), + title: res.doc.excerpt, + content: res.doc.excerpt, + url: res.doc.url, + }; + }), + ); + } catch (err) { + // eslint-disable-next-line no-console -- needs to be logged to console + console.error('Failed to search', err); + setResultLoadingError(true); + setResultLoading(false); + return; + } + setResultLoading(false); + }, + [basePath], + ); + + const setQueryAndSearch = useCallback( + (data: string) => { + setQuery(data); + void doSearch(key, data); + }, + [key, doSearch], + ); + + return { + query, + setQuery: setQueryAndSearch, + results, + loading: resultLoading, + error: resultLoadingError, + }; +} diff --git a/packages/guider/src/client/partials/header/search/index.tsx b/packages/guider/src/client/partials/header/search/index.tsx index 228c3bca..d73852f0 100644 --- a/packages/guider/src/client/partials/header/search/index.tsx +++ b/packages/guider/src/client/partials/header/search/index.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import { SearchButton } from './button'; import { SearchModal } from './modal'; +import { usePreloadSearch } from './content'; function UpdateHead(props: { active?: boolean }) { useEffect(() => { @@ -16,6 +17,8 @@ function UpdateHead(props: { active?: boolean }) { export function HeaderSearch() { const [open, setOpen] = useState(false); const openRef = useRef(open); + const searchKey = 'default'; + usePreloadSearch(searchKey); useEffect(() => { openRef.current = open; @@ -49,6 +52,7 @@ export function HeaderSearch() { {open ? ( { setOpen(false); }} diff --git a/packages/guider/src/client/partials/header/search/modal.tsx b/packages/guider/src/client/partials/header/search/modal.tsx index e52d363f..f3bf902b 100644 --- a/packages/guider/src/client/partials/header/search/modal.tsx +++ b/packages/guider/src/client/partials/header/search/modal.tsx @@ -1,11 +1,14 @@ import { SearchScreen } from './screen'; -export function SearchModal(props: { onClose?: () => void }) { +export function SearchModal(props: { + onClose?: () => void; + searchKey: string; +}) { return (
    - +
    ); diff --git a/packages/guider/src/client/partials/header/search/screen.tsx b/packages/guider/src/client/partials/header/search/screen.tsx index 992ddad6..42fb6d03 100644 --- a/packages/guider/src/client/partials/header/search/screen.tsx +++ b/packages/guider/src/client/partials/header/search/screen.tsx @@ -1,17 +1,78 @@ -import { useState } from 'react'; +import Link from 'next/link'; +import { Icon } from '../../../components/icon'; +import type { SearchResult } from './content'; +import { useSearch } from './content'; -export function SearchScreen() { - const [input, setInput] = useState(''); +const iconMap = { + page: 'tabler:file-filled', + section: 'majesticons:text', +} as const; + +function SearchResults(props: { results: SearchResult[] }) { + return ( +
    + {props.results.map((v) => { + return ( +
    + +
    + +
    +
    +

    + {v.title} +

    +

    + {v.content} +

    +
    + +
    + ); + })} +
    + ); +} + +export function SearchScreen(props: { searchKey: string }) { + const { error, loading, results, query, setQuery } = useSearch( + props.searchKey, + ); return ( -
    - { - setInput(e.target.value); - }} - type="text" - autoFocus - /> +
    +
    + { + setQuery(e.target.value); + }} + type="text" + autoFocus + /> +
    + +
    +
    +
    + {loading ? ( +

    Loading...

    + ) : error ? ( +

    Failed to load search results

    + ) : results && results.length === 0 ? ( +

    No results

    + ) : results ? ( + + ) : null} +
    ); } diff --git a/packages/guider/src/webpack/search/index.ts b/packages/guider/src/webpack/search/index.ts index f5d38f5c..9ae5e255 100644 --- a/packages/guider/src/webpack/search/index.ts +++ b/packages/guider/src/webpack/search/index.ts @@ -26,7 +26,7 @@ async function filePathToPageData(filePath: string) { }; } -function generateChecksum(str) { +function generateChecksum(str: string) { return createHash('md5').update(str, 'utf8').digest('hex'); } @@ -62,7 +62,6 @@ export class GuiderSearchPlugin { for (const page of pageData) { const key = defaultKey; - console.log(page.sitePath); dataBuckets[key] ??= {}; dataBuckets[key][page.sitePath] = page.searchData; } From c3eed6b4bec0c33ac548eb7bde1b5a687f26a07b Mon Sep 17 00:00:00 2001 From: mrjvs Date: Mon, 22 Apr 2024 22:41:29 +0200 Subject: [PATCH 21/23] Added proper search content + finalize search UI Co-authored-by: William Oldham --- packages/guider/package.json | 5 + .../client/partials/header/search/content.ts | 35 ++-- .../client/partials/header/search/index.tsx | 2 +- .../client/partials/header/search/modal.tsx | 2 +- .../client/partials/header/search/screen.tsx | 174 +++++++++++++----- .../guider/src/webpack/loader/md-loader.ts | 72 +++++++- pnpm-lock.yaml | 15 ++ 7 files changed, 239 insertions(+), 66 deletions(-) diff --git a/packages/guider/package.json b/packages/guider/package.json index af8eaac7..2b830dd6 100644 --- a/packages/guider/package.json +++ b/packages/guider/package.json @@ -75,6 +75,7 @@ "@types/color": "^3.0.6", "@types/extra-watch-webpack-plugin": "^1.0.6", "@types/git-url-parse": "^9.0.3", + "@types/mdast": "^4.0.3", "@types/react": "18.2.56", "@types/webpack": "^5.28.5", "autoprefixer": "^10.4.17", @@ -94,6 +95,7 @@ "@mdx-js/mdx": "^3.0.1", "@shikijs/transformers": "^1.1.7", "@theguild/remark-npm2yarn": "^0.3.0", + "@types/unist": "^3.0.2", "@vcarl/remark-headings": "^0.1.0", "approximate-number": "^2.1.1", "classnames": "^2.5.1", @@ -103,6 +105,7 @@ "git-url-parse": "^14.0.0", "glob": "^10.3.10", "gray-matter": "^4.0.3", + "mdast-util-phrasing": "^4.1.0", "next-seo": "^6.5.0", "react-helmet-async": "^2.0.4", "rehype-extract-excerpt": "^0.3.1", @@ -114,6 +117,8 @@ "shiki": "^1.1.7", "tailwindcss-themer": "^4.0.0", "type-fest": "^4.10.3", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0", "webpack-virtual-modules": "^0.6.1", "zustand": "^4.5.2" } diff --git a/packages/guider/src/client/partials/header/search/content.ts b/packages/guider/src/client/partials/header/search/content.ts index 2f892d41..8b305f40 100644 --- a/packages/guider/src/client/partials/header/search/content.ts +++ b/packages/guider/src/client/partials/header/search/content.ts @@ -7,7 +7,8 @@ type ContentDocument = { { id: number; url: string; - excerpt: string; + title: string; + content: string; }, never[] >; @@ -16,13 +17,16 @@ type ContentDocument = { type SearchData = Record< string, { - excerpt: string; + sections: { + heading?: { id: string; depth: number; text: string }; + content: string; + }[]; } >; export type SearchResult = { id: string; - type: 'page' | 'section'; + type: 'section'; title: string; content: string; url: string; @@ -48,8 +52,8 @@ async function fetchDocument(basePath: string, key: string) { tokenize: 'full', document: { id: 'id', - index: 'excerpt', - store: ['excerpt', 'url'], + index: ['title', 'content'], + store: ['content', 'url', 'title'], }, context: { resolution: 9, @@ -60,12 +64,15 @@ async function fetchDocument(basePath: string, key: string) { let pageId = 0; for (const [url, data] of Object.entries(searchData)) { - pageId++; - searchDocument.add({ - id: pageId, - url, - excerpt: data.excerpt, - }); + for (const section of data.sections) { + pageId++; + searchDocument.add({ + id: pageId, + title: section.heading?.text ?? '', + url: url + (section.heading ? `#${section.heading.id}` : ''), + content: section.content, + }); + } } contentDocument = { @@ -103,10 +110,10 @@ export function useSearch(key: string) { setResults( docResults.map((res): SearchResult => { return { - type: 'page', + type: 'section', id: res.id.toString(), - title: res.doc.excerpt, - content: res.doc.excerpt, + title: res.doc.title, + content: res.doc.content, url: res.doc.url, }; }), diff --git a/packages/guider/src/client/partials/header/search/index.tsx b/packages/guider/src/client/partials/header/search/index.tsx index d73852f0..1b0afac0 100644 --- a/packages/guider/src/client/partials/header/search/index.tsx +++ b/packages/guider/src/client/partials/header/search/index.tsx @@ -43,7 +43,7 @@ export function HeaderSearch() { }, []); return ( -
    +
    { setOpen(true); diff --git a/packages/guider/src/client/partials/header/search/modal.tsx b/packages/guider/src/client/partials/header/search/modal.tsx index f3bf902b..ee163370 100644 --- a/packages/guider/src/client/partials/header/search/modal.tsx +++ b/packages/guider/src/client/partials/header/search/modal.tsx @@ -8,7 +8,7 @@ export function SearchModal(props: {
    - +
    ); diff --git a/packages/guider/src/client/partials/header/search/screen.tsx b/packages/guider/src/client/partials/header/search/screen.tsx index 42fb6d03..31a00a30 100644 --- a/packages/guider/src/client/partials/header/search/screen.tsx +++ b/packages/guider/src/client/partials/header/search/screen.tsx @@ -1,4 +1,8 @@ import Link from 'next/link'; +import { Combobox } from '@headlessui/react'; +import { useRouter } from 'next/router'; +import { useCallback } from 'react'; +import classNames from 'classnames'; import { Icon } from '../../../components/icon'; import type { SearchResult } from './content'; import { useSearch } from './content'; @@ -8,71 +12,143 @@ const iconMap = { section: 'majesticons:text', } as const; +function SearchMessage(props: { title: string; text: string; icon: string }) { + return ( +
    +
    + +
    +

    + {props.title} +

    +

    {props.text}

    +
    + ); +} + function SearchResults(props: { results: SearchResult[] }) { return ( -
    +
    {props.results.map((v) => { return ( -
    - -
    - -
    -
    -

    - {v.title} -

    -

    - {v.content} -

    -
    - -
    + + {({ active }) => ( + +
    + +
    +
    +

    + {v.title} +

    +

    + {v.content} +

    +
    +
    + +
    + + )} +
    ); })}
    ); } -export function SearchScreen(props: { searchKey: string }) { +export function SearchScreen(props: { + searchKey: string; + onClose?: () => void; +}) { + const router = useRouter(); const { error, loading, results, query, setQuery } = useSearch( props.searchKey, ); + + const onChange = useCallback( + (e?: SearchResult) => { + if (e) void router.push(e.url); + props.onClose?.(); + }, + [router, props.onClose], + ); + return ( -
    -
    - { - setQuery(e.target.value); - }} - type="text" - autoFocus - /> -
    - +
    +
    + { + setQuery(e.target.value); + }} + type="text" + autoFocus /> +
    + +
    + + {loading ? ( +

    + Loading... +

    + ) : error ? ( + + ) : query.length > 0 && results && results.length === 0 ? ( + + ) : query.length > 0 && results ? ( + + ) : null} +
    -
    - {loading ? ( -

    Loading...

    - ) : error ? ( -

    Failed to load search results

    - ) : results && results.length === 0 ? ( -

    No results

    - ) : results ? ( - - ) : null} -
    -
    + ); } diff --git a/packages/guider/src/webpack/loader/md-loader.ts b/packages/guider/src/webpack/loader/md-loader.ts index 444ab725..741c98a3 100644 --- a/packages/guider/src/webpack/loader/md-loader.ts +++ b/packages/guider/src/webpack/loader/md-loader.ts @@ -8,6 +8,10 @@ import rehypeExtractExcerpt from 'rehype-extract-excerpt'; import remarkLinkRewrite from 'remark-link-rewrite'; import { remarkNpm2Yarn } from '@theguild/remark-npm2yarn'; import remarkGfm from 'remark-gfm'; +import { SKIP, visit } from 'unist-util-visit'; +import type { Heading, HeadingData, Root } from 'mdast'; +import { phrasing } from 'mdast-util-phrasing'; +import type { VFileWithOutput } from 'unified'; import { transformerNotationDiff, transformerNotationHighlight, @@ -18,6 +22,11 @@ import { const EXPORT_FOOTER = 'export default '; +interface Section { + heading?: { id: string; depth: Heading['depth']; text: string }; + content: string; +} + export async function mdLoader(source: string) { const meta = grayMatter(source); const file = await compile(source, { @@ -66,6 +75,67 @@ export async function mdLoader(source: string) { }, }, ], + () => { + return (root: Root, vFile: VFileWithOutput) => { + const sections: Section[] = []; + + let currentSection: Section | undefined; + let previousParentNode: any; + + visit(root, (node, _, parent) => { + if (node.type === 'heading') { + if (currentSection) { + sections.push(currentSection); + } + + const heading = node; + const id = (heading.data as HeadingData & { id: string })?.id; + const depth = heading.depth; + let text = ''; + visit(heading, ['text', 'inlineCode'], (hChild: any) => { + text += hChild.value; + }); + + currentSection = { + heading: { + id, + depth, + text, + }, + content: '', + }; + + return SKIP; + } + + const types = ['text', 'inlineCode']; + + if (types.includes(node.type)) { + if (!currentSection) { + currentSection = { + heading: undefined, + content: '', + }; + } + + if ( + previousParentNode && + previousParentNode !== parent && + !phrasing(previousParentNode) + ) { + currentSection.content += ' '; + } + currentSection.content += (node as any).value; + + previousParentNode = parent; + } + }); + + if (currentSection) sections.push(currentSection); + + vFile.data = { ...vFile.data, sections }; + }; + }, ], rehypePlugins: [ rehypeExtractExcerpt, @@ -114,7 +184,7 @@ export async function mdLoader(source: string) { return { script, searchData: { - excerpt: file.data.excerpt, + sections: file.data.sections, }, }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5347f6c..6c130bb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,6 +173,9 @@ importers: '@theguild/remark-npm2yarn': specifier: ^0.3.0 version: 0.3.0 + '@types/unist': + specifier: ^3.0.2 + version: 3.0.2 '@vcarl/remark-headings': specifier: ^0.1.0 version: 0.1.0 @@ -200,6 +203,9 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 + mdast-util-phrasing: + specifier: ^4.1.0 + version: 4.1.0 next-seo: specifier: ^6.5.0 version: 6.5.0(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) @@ -233,6 +239,12 @@ importers: type-fest: specifier: ^4.10.3 version: 4.10.3 + unified: + specifier: ^11.0.4 + version: 11.0.4 + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 webpack-virtual-modules: specifier: ^0.6.1 version: 0.6.1 @@ -258,6 +270,9 @@ importers: '@types/git-url-parse': specifier: ^9.0.3 version: 9.0.3 + '@types/mdast': + specifier: ^4.0.3 + version: 4.0.3 '@types/react': specifier: 18.2.56 version: 18.2.56 From 228085cf418b580329fa68598c4664edae282954 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Mon, 22 Apr 2024 22:54:47 +0200 Subject: [PATCH 22/23] animations + mobile friendly --- .../client/partials/header/search/button.tsx | 8 +++-- .../client/partials/header/search/index.tsx | 21 +++++++------ .../client/partials/header/search/modal.tsx | 31 ++++++++++++++++--- .../client/partials/header/search/screen.tsx | 2 +- packages/guider/src/styles/global.css | 2 +- 5 files changed, 46 insertions(+), 18 deletions(-) diff --git a/packages/guider/src/client/partials/header/search/button.tsx b/packages/guider/src/client/partials/header/search/button.tsx index 7bf197ca..c59eeac0 100644 --- a/packages/guider/src/client/partials/header/search/button.tsx +++ b/packages/guider/src/client/partials/header/search/button.tsx @@ -5,14 +5,16 @@ export function SearchButton(props: { onClick?: () => void }) { ); } diff --git a/packages/guider/src/client/partials/header/search/index.tsx b/packages/guider/src/client/partials/header/search/index.tsx index 1b0afac0..a2aea67e 100644 --- a/packages/guider/src/client/partials/header/search/index.tsx +++ b/packages/guider/src/client/partials/header/search/index.tsx @@ -1,4 +1,5 @@ -import { useEffect, useRef, useState } from 'react'; +import { Fragment, useEffect, useRef, useState } from 'react'; +import { Transition } from '@headlessui/react'; import { SearchButton } from './button'; import { SearchModal } from './modal'; import { usePreloadSearch } from './content'; @@ -50,14 +51,16 @@ export function HeaderSearch() { }} /> - {open ? ( - { - setOpen(false); - }} - /> - ) : null} + +
    + { + setOpen(false); + }} + /> +
    +
    ); } diff --git a/packages/guider/src/client/partials/header/search/modal.tsx b/packages/guider/src/client/partials/header/search/modal.tsx index ee163370..723237cf 100644 --- a/packages/guider/src/client/partials/header/search/modal.tsx +++ b/packages/guider/src/client/partials/header/search/modal.tsx @@ -1,3 +1,5 @@ +import { Fragment } from 'react'; +import { Transition } from '@headlessui/react'; import { SearchScreen } from './screen'; export function SearchModal(props: { @@ -5,11 +7,32 @@ export function SearchModal(props: { searchKey: string; }) { return ( -
    +
    + +
    +
    -
    - -
    + +
    + +
    +
    ); } diff --git a/packages/guider/src/client/partials/header/search/screen.tsx b/packages/guider/src/client/partials/header/search/screen.tsx index 31a00a30..8d063f4d 100644 --- a/packages/guider/src/client/partials/header/search/screen.tsx +++ b/packages/guider/src/client/partials/header/search/screen.tsx @@ -28,7 +28,7 @@ function SearchMessage(props: { title: string; text: string; icon: string }) { function SearchResults(props: { results: SearchResult[] }) { return ( -
    +
    {props.results.map((v) => { return ( diff --git a/packages/guider/src/styles/global.css b/packages/guider/src/styles/global.css index 852ede6c..7c89a2c6 100644 --- a/packages/guider/src/styles/global.css +++ b/packages/guider/src/styles/global.css @@ -16,7 +16,7 @@ body[data-header-dropdown-open] { } } -body[data-stop-overflow="true"] { +body[data-stop-overflow="true"], body[data-header-search-open="true"] { @apply gd-overflow-hidden; scrollbar-gutter: stable; } From 4f0c9361e0a6a747b1ea6b86252acc6e515dadb1 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Mon, 22 Apr 2024 22:58:49 +0200 Subject: [PATCH 23/23] Bump version --- packages/guider/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/guider/package.json b/packages/guider/package.json index 2b830dd6..19fc003d 100644 --- a/packages/guider/package.json +++ b/packages/guider/package.json @@ -1,6 +1,6 @@ { "name": "@neato/guider", - "version": "0.1.5", + "version": "1.0.0", "description": "Beautiful documentation sites, without all the hassle", "main": "./dist/index.js", "type": "module",