diff --git a/packages/freesewing.dev/components/header.js b/packages/freesewing.dev/components/header.js
index ddabd24548f..aebcf39f258 100644
--- a/packages/freesewing.dev/components/header.js
+++ b/packages/freesewing.dev/components/header.js
@@ -55,7 +55,7 @@ const Header = ({ app, setSearch }) => {
text-neutral-content bg-transparent
border border-transparent
hover:bg-transparent hover:border-base-100
- sm:hidden
+ md:hidden
h-12
`}
onClick={app.togglePrimaryMenu}>
@@ -79,14 +79,14 @@ const Header = ({ app, setSearch }) => {
)
}
-
-
diff --git a/packages/freesewing.dev/components/layouts/docs.js b/packages/freesewing.dev/components/layouts/docs.js
new file mode 100644
index 00000000000..366176ee904
--- /dev/null
+++ b/packages/freesewing.dev/components/layouts/docs.js
@@ -0,0 +1,150 @@
+import React from 'react'
+import { useState } from 'react'
+import { useRouter } from 'next/router'
+import Link from 'next/link'
+// Shared components
+import Logo from 'shared/components/logos/freesewing.js'
+import PrimaryNavigation from 'shared/components/navigation/primary'
+import get from 'lodash.get'
+import Right from 'shared/components/icons/right.js'
+import Left from 'shared/components/icons/left.js'
+// Site components
+import Header from 'site/components/header'
+import Footer from 'site/components/footer'
+import Search from 'site/components/search'
+
+const PageTitle = ({ app, slug, title }) => {
+ if (title) return {title}
+ if (slug) return {get(app.navigation, slug.split('/')).__title}
+
+ return FIXME: This page has no title
+}
+
+const Breadcrumbs = ({ app, slug=false, title }) => {
+ if (!slug) return null
+ const crumbs = []
+ const chunks = slug.split('/')
+ for (const i in chunks) {
+ const j = parseInt(i)+parseInt(1)
+ const page = get(app.navigation, chunks.slice(0,j))
+ if (page) crumbs.push([page.__linktitle, '/'+chunks.slice(0,j).join('/'), (j < chunks.length)])
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {crumbs.map(crumb => (
+
+ »
+
+ {crumb[2]
+ ? (
+
+
+ {crumb[0]}
+
+
+ )
+ : {crumb[0]}
+ }
+
+
+ ))}
+
+ )
+}
+
+const DefaultLayout = ({
+ app,
+ title=false,
+ children=[],
+ search,
+ setSearch,
+ noSearch=false,
+ workbench=false,
+ AltMenu=null,
+}) => {
+ const startNavigation = () => {
+ app.startLoading()
+ // Force close of menu on mobile if it is open
+ if (app.primaryNavigation) app.setPrimaryNavigation(false)
+ // Force close of search modal if it is open
+ if (search) setSearch(false)
+ }
+
+ const router = useRouter()
+ router.events?.on('routeChangeStart', startNavigation)
+ router.events?.on('routeChangeComplete', () => app.stopLoading())
+ const slug = router.asPath.slice(1)
+ const [collapsePrimaryNav, setCollapsePrimaryNav] = useState(workbench || false)
+ const [collapseAltMenu, setCollapseAltMenu] = useState(false)
+
+ return (
+
+
+
+
+
+
+
+
+
+ {title && (
+
+ )}
+ {children}
+
+
+
+
+
+
+ {!noSearch && search && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+ )
+}
+
+export default DefaultLayout
diff --git a/packages/freesewing.dev/components/wrappers/page.js b/packages/freesewing.dev/components/wrappers/page.js
new file mode 100644
index 00000000000..478875b4d39
--- /dev/null
+++ b/packages/freesewing.dev/components/wrappers/page.js
@@ -0,0 +1,65 @@
+import React, { useState, useEffect } from 'react'
+import { useSwipeable } from 'react-swipeable'
+import { useRouter } from 'next/router'
+import { useHotkeys } from 'react-hotkeys-hook'
+// Layouts components
+import Docs from 'site/components/layouts/docs'
+
+const layouts = {
+ docs: Docs,
+}
+
+/* This component should wrap all page content */
+const PageWrapper= ({
+ title="FIXME: No title set",
+ noSearch=false,
+ app=false,
+ layout=false,
+ children=[]
+}) => {
+
+ const swipeHandlers = useSwipeable({
+ onSwipedLeft: evt => (app.primaryMenu) ? app.setPrimaryMenu(false) : null,
+ onSwipedRight: evt => (app.primaryMenu) ? null : app.setPrimaryMenu(true),
+ trackMouse: true
+ })
+
+ const router = useRouter()
+ const slug = router.asPath.slice(1)
+
+ useEffect(() => app.setSlug(slug), [slug])
+
+ // Trigger search with Ctrl+k
+ useHotkeys('ctrl+k', (evt) => {
+ evt.preventDefault()
+ setSearch(true)
+ })
+
+ const [search, setSearch] = useState(false)
+
+ const childProps = {
+ app: app,
+ title: title,
+ search, setSearch, toggleSearch: () => setSearch(!search),
+ noSearch: noSearch,
+ }
+
+ const Layout = layouts[layout]
+
+ return (
+
+ {layout
+ ? {children}
+ : children
+ }
+
+ )
+}
+
+export default PageWrapper
+
diff --git a/packages/freesewing.dev/pages/[...mdxslug].js b/packages/freesewing.dev/pages/[...mdxslug].js
index 9a898f1bd98..64138aec636 100644
--- a/packages/freesewing.dev/pages/[...mdxslug].js
+++ b/packages/freesewing.dev/pages/[...mdxslug].js
@@ -1,8 +1,9 @@
-import Page from 'shared/components/wrappers/page.js'
+import Page from 'site/components/wrappers/page.js'
import useApp from 'site/hooks/useApp.js'
import mdxMeta from 'site/prebuild/mdx.en.js'
import mdxLoader from 'shared/mdx/loader'
import MdxWrapper from 'shared/components/wrappers/mdx'
+import TocWrapper from 'shared/components/wrappers/toc'
import Head from 'next/head'
import HelpUs from 'site/components/help-us.js'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@@ -22,7 +23,7 @@ const MdxPage = props => {
* active state
*/
return (
-
+
@@ -36,8 +37,17 @@ const MdxPage = props => {
-
-
+
+ {props.toc && (
+
+
+
+ )}
+
+
+
+
+
)
}
@@ -61,11 +71,12 @@ export default MdxPage
*/
export async function getStaticProps({ params, locale }) {
- const { mdx, intro } = await mdxLoader('en', 'dev', params.mdxslug.join('/'))
+ const { mdx, intro, toc } = await mdxLoader('en', 'dev', params.mdxslug.join('/'))
return {
props: {
mdx,
+ toc,
intro: intro.join(' '),
page: {
slug: params.mdxslug.join('/'),
diff --git a/packages/freesewing.shared/components/icons/docs.js b/packages/freesewing.shared/components/icons/docs.js
index 65053ce741b..89e7f8bfba1 100644
--- a/packages/freesewing.shared/components/icons/docs.js
+++ b/packages/freesewing.shared/components/icons/docs.js
@@ -1,6 +1,6 @@
/* Sourced from heroicons.com - Thanks guys! */
-const Docs = () => (
-
+const Docs = ({ className="h-6 w-6" }) => (
+
)
diff --git a/packages/freesewing.shared/components/icons/guide.js b/packages/freesewing.shared/components/icons/guide.js
index 910bacf2127..0456eaa3768 100644
--- a/packages/freesewing.shared/components/icons/guide.js
+++ b/packages/freesewing.shared/components/icons/guide.js
@@ -1,5 +1,5 @@
-const Guides = () => (
-
+const Guides = ({ className="h-6 w-6" }) => (
+
)
diff --git a/packages/freesewing.shared/components/icons/help.js b/packages/freesewing.shared/components/icons/help.js
index 92d7723d790..edf0c6e6bd3 100644
--- a/packages/freesewing.shared/components/icons/help.js
+++ b/packages/freesewing.shared/components/icons/help.js
@@ -1,6 +1,6 @@
/* Sourced from heroicons.com - Thanks guys! */
-const Help = () => (
-
+const Help = ({ className="w-6 h-6" }) => (
+
)
diff --git a/packages/freesewing.shared/components/icons/rss.js b/packages/freesewing.shared/components/icons/rss.js
index 0b5b77e7532..cf3c03fca90 100644
--- a/packages/freesewing.shared/components/icons/rss.js
+++ b/packages/freesewing.shared/components/icons/rss.js
@@ -1,6 +1,6 @@
/* Sourced from heroicons.com - Thanks guys! */
-const Rss = () => (
-
+const Rss = ({ className='h-6 w-6' }) => (
+
)
diff --git a/packages/freesewing.shared/components/icons/tutorial.js b/packages/freesewing.shared/components/icons/tutorial.js
index ea49dcfa350..3f642bc80dc 100644
--- a/packages/freesewing.shared/components/icons/tutorial.js
+++ b/packages/freesewing.shared/components/icons/tutorial.js
@@ -1,5 +1,5 @@
-const Tutorial = () => (
-
+const Tutorial = ({ className="w-6 h-6" }) => (
+
diff --git a/packages/freesewing.shared/components/layouts/default.js b/packages/freesewing.shared/components/layouts/default.js
index 18c33ab486e..7c946d1e4ab 100644
--- a/packages/freesewing.shared/components/layouts/default.js
+++ b/packages/freesewing.shared/components/layouts/default.js
@@ -74,8 +74,9 @@ const asideClasses = `
overflow-y-scroll
z-20
bg-base-100 text-base-content
- sm:bg-neutral sm:bg-opacity-95 sm:text-neutral-content
- transition-all `
+ transition-all
+ xl:w-1/4
+`
const DefaultLayout = ({
@@ -110,53 +111,58 @@ const DefaultLayout = ({
bg-base-100
`}>
-
-
-
-
- {title && (
- <>
-
-
- >
+
+
+
+
+
+
+
+
+ {title && (
+ <>
+
+
+ >
+ )}
+ {children}
+
+
-
+
+
+
{workbench && AltMenu && (
,
- tutorials: ,
- guides: ,
- howtos: ,
- reference:
+ blog: (className='') => ,
+ tutorials: (className='') => ,
+ guides: (className='') => ,
+ howtos: (className='') => ,
+ reference: (className='') =>
}
/* helper method to order nav entries */
@@ -33,7 +33,7 @@ const order = obj => orderBy(obj, ['__order', '__title'], ['asc', 'asc'])
// Exported for re-use
export const Chevron = ({w=8, m=2}) =>
@@ -47,9 +47,9 @@ const currentChildren = current => Object.values(order(current))
// Exported for re-use
export const linkClasses = `text-lg lg:text-xl
py-1
- text-base-content sm:text-neutral-content
+ text-base-content sm:text-base-content
hover:text-secondary
- sm:hover:text-secondary-focus
+ sm:hover:text-secondary
`
// Figure out whether a page is on the path to the active page
@@ -76,7 +76,7 @@ const SubLevel = ({ nodes={}, active }) => (
flex flex-row
px-2
text-base-content
- sm:text-neutral-content
+ sm:text-base-content
hover:cursor-row-resize
items-center
`}>
@@ -86,16 +86,16 @@ const SubLevel = ({ nodes={}, active }) => (
${linkClasses}
hover:cursor-pointer
hover:border-secondary
- sm:hover:border-secondary-focus
+ sm:hover:border-secondary
${child.__slug === active
- ? 'text-secondary border-secondary sm:text-secondary-focus sm:border-secondary-focus'
- : 'text-base-content sm:text-neutral-content'
+ ? 'text-secondary border-secondary sm:text-secondary sm:border-secondary'
+ : 'text-base-content sm:text-base-content'
}
`}>
@@ -120,15 +120,15 @@ const SubLevel = ({ nodes={}, active }) => (
${linkClasses}
hover:cursor-pointer
hover:border-secondary
- sm:hover:border-secondary-focus
+ sm:hover:border-secondary
${child.__slug === active
- ? 'text-secondary border-secondary sm:text-secondary-focus sm:border-secondary-focus'
- : 'text-base-content sm:text-neutral-content'
+ ? 'text-secondary border-secondary sm:text-secondary sm:border-secondary'
+ : 'text-base-content sm:text-base-content'
}`}>
@@ -154,15 +154,15 @@ const TopLevel = ({ icon, title, nav, current, slug, hasChildren=false, active }
hover:cursor-row-resize
p-2
text-base-content
- sm:text-neutral-content
+ sm:text-base-content
items-center
`}>
- {icon}
+ {icon}
@@ -175,12 +175,15 @@ const TopLevel = ({ icon, title, nav, current, slug, hasChildren=false, active }
)
-const Navigation = ({ app, active }) => {
+const Navigation = ({ app, active, className='' }) => {
if (!app.navigation) return null
const output = []
for (const page of order(app.navigation)) output.push(° }
+ icon={icons[page.__slug]
+ ? icons[page.__slug]('w-6 h-6')
+ : °
+ }
title={page.__title}
slug={page.__slug}
hasChildren={keepClosed.indexOf(page.__slug) === -1}
@@ -189,13 +192,39 @@ const Navigation = ({ app, active }) => {
active={active}
/>)
- return {output}
+ return {output}
+}
+
+const Icons = ({ app, active, className='' }) => {
+ if (!app.navigation) return null
+ const output = []
+ for (const page of order(app.navigation)) {
+ output.push(
+
+
+
+ {icons[page.__slug]
+ ? icons[page.__slug]('w-14 h-14')
+ :
+ }
+ {page.__title}
+
+
+
+ )
+ }
+
+ return
}
const PrimaryMenu = ({ app, active }) => (
-
-
-
+
+
+
+
)
diff --git a/packages/freesewing.shared/components/wrappers/toc.js b/packages/freesewing.shared/components/wrappers/toc.js
new file mode 100644
index 00000000000..2da98ba1f5e
--- /dev/null
+++ b/packages/freesewing.shared/components/wrappers/toc.js
@@ -0,0 +1,41 @@
+/*
+ * This is used to wrap a Table of Contents (toc) as returned
+ * from the mdxLoader method (see shared/mdx/loader.js)
+ * It is NOT for wrapping plain markdown/mdx
+ */
+import { useState, useEffect, Fragment } from 'react'
+
+// See: https://mdxjs.com/guides/mdx-on-demand/
+import { run } from '@mdx-js/mdx'
+import * as runtime from 'react/jsx-runtime.js'
+
+// Components that are available in all MDX
+import customComponents from 'shared/components/mdx'
+
+const TocWrapper = ({toc, app}) => {
+
+ const [mdxModule, setMdxModule] = useState()
+
+ useEffect(() => {
+ ;(async () => {
+ setMdxModule(await run(toc, runtime))
+ })()
+ }, [toc])
+
+ // React component for MDX content
+ const MdxContent = mdxModule ? mdxModule.default : Fragment
+
+ return (
+
+ {mdxModule && }
+
+ )
+}
+
+export default TocWrapper
+
diff --git a/packages/freesewing.shared/mdx/loader.js b/packages/freesewing.shared/mdx/loader.js
index aa398c1cf5d..9a164fb5f44 100644
--- a/packages/freesewing.shared/mdx/loader.js
+++ b/packages/freesewing.shared/mdx/loader.js
@@ -10,6 +10,7 @@ import remarkFrontmatter from 'remark-frontmatter'
import remarkGfm from 'remark-gfm'
import remarkCopyLinkedFiles from 'remark-copy-linked-files'
import { remarkIntroPlugin } from './remark-intro-plugin.mjs'
+import mdxPluginToc from './mdx-plugin-toc.mjs'
import smartypants from 'remark-smartypants'
// Rehype plugins we want to use
import rehypeHighlight from 'rehype-highlight'
@@ -61,7 +62,27 @@ const mdxLoader = async (language, site, slug) => {
})
)
- return {mdx, intro}
+ // This is not ideal as we're adding a second pass but for now it will do
+ // See: https://github.com/remarkjs/remark-toc/issues/37
+ const toc = String(
+ await compile(md, {
+ outputFormat: 'function-body',
+ remarkPlugins: [
+ remarkFrontmatter,
+ remarkGfm,
+ smartypants,
+ [
+ mdxPluginToc,
+ { language: 'en' }
+ ]
+ ],
+ rehypePlugins: [
+ rehypeSlug,
+ ],
+ })
+ )
+
+ return {mdx, intro, toc}
}
export default mdxLoader
diff --git a/packages/freesewing.shared/mdx/mdx-plugin-toc.mjs b/packages/freesewing.shared/mdx/mdx-plugin-toc.mjs
new file mode 100644
index 00000000000..4ce5bf443f2
--- /dev/null
+++ b/packages/freesewing.shared/mdx/mdx-plugin-toc.mjs
@@ -0,0 +1,30 @@
+import {toc} from 'mdast-util-toc'
+
+const headings = {
+ en: 'Table of contents',
+ fr: 'Table des matières',
+ nl: 'Inhoudsopgave',
+ es: 'Tabla de contenido',
+ de: 'Inhaltsverzeichnis'
+}
+
+export default function mdxToc(options = {}) {
+ return (node) => {
+ const result = toc(node, { heading: false })
+
+ if (result.map) node.children = [
+ {
+ type: 'heading',
+ depth: 4,
+ children: [{
+ type: 'text',
+ value: headings[options.language || 'en']
+ }]
+ },
+ result.map
+ ]
+ else node.children = []
+
+ return
+ }
+}
diff --git a/packages/freesewing.shared/package.json b/packages/freesewing.shared/package.json
index fedd3a1f886..c0e10de0db0 100644
--- a/packages/freesewing.shared/package.json
+++ b/packages/freesewing.shared/package.json
@@ -22,6 +22,7 @@
"highlight.js": "^11.4.0",
"lodash.orderby": "^4.6.0",
"lodash.unset": "^4.5.2",
+ "mdast-util-toc": "^6.1.0",
"react-markdown": "^8.0.0",
"react-sizeme": "^3.0.2",
"react-timeago": "^6.2.1",
diff --git a/packages/freesewing.shared/styles/globals.css b/packages/freesewing.shared/styles/globals.css
index 8a7d4fb5835..a60a4dcb373 100644
--- a/packages/freesewing.shared/styles/globals.css
+++ b/packages/freesewing.shared/styles/globals.css
@@ -35,6 +35,12 @@
.mdx a[aria-hidden="true"] {
text-decoration: none;
}
+ /* Watch out of P tags in the toc list */
+ .mdx-toc ul li p {
+ margin: 0;
+ padding: 0;
+ display: inline;
+ }
/* FreeSewing SVG output styles */
.fs-stroke-fabric { stroke: var(--pattern-fabric); }
diff --git a/yarn.lock b/yarn.lock
index 8bad8cda74b..3c276466f68 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4428,6 +4428,11 @@
"@types/qs" "*"
"@types/serve-static" "*"
+"@types/extend@^3.0.0":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@types/extend/-/extend-3.0.1.tgz#923dc2d707d944382433e01d6cc0c69030ab2c75"
+ integrity sha512-R1g/VyKFFI2HLC1QGAeTtCBWCo6n75l41OnsVYNbmKG+kempOESaodf6BeJyUM3Q0rKa/NQcTHbB2+66lNnxLw==
+
"@types/formidable@^1.0.31":
version "1.2.5"
resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-1.2.5.tgz#561d026e5f09179e5c8ef7b31e8f4652e11abe4c"
@@ -4435,6 +4440,11 @@
dependencies:
"@types/node" "*"
+"@types/github-slugger@^1.0.0":
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@types/github-slugger/-/github-slugger-1.3.0.tgz#16ab393b30d8ae2a111ac748a015ac05a1fc5524"
+ integrity sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g==
+
"@types/glob@^7.1.1":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
@@ -12725,7 +12735,7 @@ github-from-package@0.0.0:
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
-github-slugger@^1.1.1:
+github-slugger@^1.0.0, github-slugger@^1.1.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.4.0.tgz#206eb96cdb22ee56fdc53a28d5a302338463444e"
integrity sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==
@@ -17418,6 +17428,20 @@ mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0:
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz#56c506d065fbf769515235e577b5a261552d56e9"
integrity sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==
+mdast-util-toc@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/mdast-util-toc/-/mdast-util-toc-6.1.0.tgz#1f38419f5ce774449c8daa87b39a4d940b24be7c"
+ integrity sha512-0PuqZELXZl4ms1sF7Lqigrqik4Ll3UhbI+jdTrfw7pZ9QPawgl7LD4GQ8MkU7bT/EwiVqChNTbifa2jLLKo76A==
+ dependencies:
+ "@types/extend" "^3.0.0"
+ "@types/github-slugger" "^1.0.0"
+ "@types/mdast" "^3.0.0"
+ extend "^3.0.0"
+ github-slugger "^1.0.0"
+ mdast-util-to-string "^3.1.0"
+ unist-util-is "^5.0.0"
+ unist-util-visit "^3.0.0"
+
mdn-data@2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"