This commit is contained in:
sup39 2022-11-04 21:44:29 +09:00
commit 2c1f2adcf8
28 changed files with 4155 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules/
.next/

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 sup39
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# supMDX
A template to make blog-like site with MDX
## Configuration
- `supMDX.yml`

10
components/Footer.tsx Normal file
View file

@ -0,0 +1,10 @@
import config from '#config';
export default function Footer() {
const {startYear: year0, siteAuthor} = config;
const year = new Date().getFullYear();
return <footer>{siteAuthor &&
<div>Copyright © {year>year0 ? `${year0}-${year}` : year} {siteAuthor} All rights reserved.</div>
}</footer>;
}

35
components/MDXRoot.tsx Normal file
View file

@ -0,0 +1,35 @@
import Head from 'next/head';
import Nav from './Nav';
import MetaInfo from './MetaInfo';
import type {HeadingInfo} from '@sup39/rehype-mdx-export-headings';
export type MDXProps = {
children: JSX.Element
data: {
pathname: string,
},
meta: Partial<{
title: string
description: string
h1: string
[key: string]: any
}>
headings: HeadingInfo[]
};
export default function MDXRoot({children, data: {pathname}, meta={}, headings}: MDXProps) {
const {title, description} = meta;
const h1 = meta.h1 ?? title;
return <>
<Head>
<title>{title}</title>
{description && <meta name="description" content={description} />}
</Head>
<Nav pathname={pathname} headings={headings} />
<main>
{h1 ? <h1>{h1}</h1> : <></>}
<MetaInfo data={meta} />
{children}
</main>
</>;
}

12
components/MetaInfo.tsx Normal file
View file

@ -0,0 +1,12 @@
import config from '#config';
export default function MetaInfo({data}: {data: {[_: string]: any}}) {
const {metaFields: fields = []} = config;
return <div>{fields.map(({label, prop}) => {
const val = data[prop];
return val == null ? null : <div key={prop}>
<span>{label}</span>
<span>{val}</span>
</div>;
})}</div>;
}

53
components/Nav.tsx Normal file
View file

@ -0,0 +1,53 @@
import {useState} from 'react';
import Link from 'next/link';
import NavHeader from './NavHeader';
import type {HeadingInfo} from '@sup39/rehype-mdx-export-headings';
import config from '#config';
export type NavEntryInfo = {
name: string
link: string
children?: NavEntryInfo[]|null
};
export function NavEntry<Body, >({
entry: {name, link, children}, dir, here, children: childrenJSX,
}: {entry: NavEntryInfo, dir: string, here: string, children?: Body}) {
const [toggle, setToggle] = useState(false);
const href = dir+link;
const isHere = href.replace(/\/$/, '')===here; // remove trailing slash
const entryCls = 'nav-entry'+(isHere ? ' nav-here' : '');
return children?.length ? <div className={'nav-dir'+(toggle ? ' nav-fold-open' : '')}><>
<div className={entryCls}>
<Link href={href}>{name}</Link>
<svg viewBox="0 0 8 8" onClick={()=>setToggle(e=>!e)}><polyline points="6 3 4 5 2 3"></polyline></svg>
</div>
{isHere ? childrenJSX : <></>}
<div className='nav-dir-child'>{
children.map(entry => <NavEntry key={entry.link} entry={entry} dir={href} here={here}>{childrenJSX}</NavEntry>)
}</div>
</></div> : <div className={entryCls}>
<Link href={href}>{name}</Link>
</div>;
}
export default function Nav({children, headings, pathname}: {
children?: JSX.Element
headings: HeadingInfo[]
pathname: string
}) {
const [navFold, setNavFold] = useState(false);
const headingsJSX = <ul className=''>{headings.map(({label, id}) => <li key={id}>
<a href={'#'+id}>{label}</a>
</li>)}</ul>;
return <nav className={navFold ? 'open' : ''}>
<NavHeader onToggleFold={()=>setNavFold(e=>!e)} />
{children}
<div className='nav-root'>
{config.nav.map(entry => <NavEntry key={entry.link} entry={entry} dir={'/'} here={pathname} />)}
{headingsJSX}
</div>
</nav>;
}

10
components/NavHeader.tsx Normal file
View file

@ -0,0 +1,10 @@
import Link from 'next/link';
export default function NavHeader({onToggleFold}: {onToggleFold?: ()=>void}) {
return <header>
<Link href="/" id="icon-link">
<div style={{fontSize: '1.5em'}}>supMDX</div>
</Link>
<div className="menu-toggle" onClick={onToggleFold} />
</header>;
}

12
components/mdx.tsx Normal file
View file

@ -0,0 +1,12 @@
/** <S $="span" _=".a.b.c" className="extra class-name">...</S> */
export function S<E extends React.ElementType='span'>({
$: TagName = 'span',
_: modifier = '',
className, children, ...props
}: {
$?: string,
_?: string,
} & React.ComponentProps<E>) {
const cls = [className, ...(modifier.match(/\.(\w+)/g) ?? [])].map(s => s.slice(1)).join(' ');
return <TagName className={cls} {...props}>{children}</TagName>;
}

5
next-env.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

30
next.config.mjs Normal file
View file

@ -0,0 +1,30 @@
import mdx from '@next/mdx';
import remarkFrontmatter from 'remark-frontmatter';
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
import ExportHeadings from '@sup39/rehype-mdx-export-headings';
import ComponentWrapper from '@sup39/rehype-mdx-component-wrapper';
const withMDX = mdx({
extension: /\.mdx?$/,
options: {
remarkPlugins: [
remarkFrontmatter,
() => remarkMdxFrontmatter({name: 'meta'}),
],
rehypePlugins: [
() => ExportHeadings({tags: ['h2'], name: 'headings'}),
() => ComponentWrapper({props: ['headings', 'meta']}),
],
},
});
export default withMDX({
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
trailingSlash: true,
webpack(config) {
config.module.rules.push({
test: /\.ya?ml$/,
use: 'yaml-loader',
});
return config;
},
});

67
package.json Normal file
View file

@ -0,0 +1,67 @@
{
"name": "supMDX",
"version": "0.1.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint --ext .js,.jsx,.ts,.tsx,.md,.mdx ."
},
"eslintConfig": {
"extends": [
"next/core-web-vitals",
"@sup39/typescript"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {
"no-undef": "off"
}
},
{
"files": [
"*.mdx"
],
"extends": [
"plugin:mdx/recommended"
],
"rules": {
"no-trailing-spaces": "off",
"indent": "off"
}
}
]
},
"eslintIgnore": [
"node_modules"
],
"dependencies": {
"@mdx-js/loader": "^2.1.5",
"@mdx-js/react": "^2.1.5",
"@next/mdx": "^13.0.2",
"@sup39/rehype-mdx-component-wrapper": "^0.1.0",
"@sup39/rehype-mdx-export-headings": "^0.1.1",
"next": "^13.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remark-frontmatter": "^4.0.1",
"remark-mdx-frontmatter": "^2.1.1",
"sass": "^1.56.0",
"yaml-loader": "^0.8.0"
},
"devDependencies": {
"@sup39/eslint-config-typescript": "^0.1.2",
"@types/node": "18.11.9",
"@types/react": "18.0.24",
"eslint": "^8.26.0",
"eslint-config-next": "^13.0.2",
"eslint-plugin-mdx": "^2.0.5",
"typescript": "4.8.4"
}
}

20
pages/_app.tsx Normal file
View file

@ -0,0 +1,20 @@
import React from 'react';
import type {AppProps} from 'next/app';
import {MDXProvider} from '@mdx-js/react';
import {S} from '@/mdx';
import '../styles/index.sass';
// add anchor to all headings having id
const hx = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;
const extendedHx = Object.fromEntries(hx.map(H => [H,
({children, id, ...props}: React.ComponentProps<(typeof hx)[number]>) => <H id={id} {...props}>
{id && <a className="anchor" />}
{children}
</H>,
]));
export default function App({Component, pageProps, router: {pathname}}: AppProps) {
return <MDXProvider components={{S, ...extendedHx}}>
<Component data={{pathname}} {...pageProps} />
</MDXProvider>;
}

8
pages/index.mdx Normal file
View file

@ -0,0 +1,8 @@
---
title: supMDX
description: You can write any meta data you want
author: me
---
## h2
### h3

27
styles/a.sass Normal file
View file

@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 sup39
a, .link
color: #72E5DB
text-decoration: none
cursor: pointer
a:hover, .link:hover
color: #72E5DB
text-decoration: underline
a:active, .link:active
color: #A0E5DF
text-decoration: underline
a.anchor
float: left
padding-right: 4px
margin-left: -24px
a.anchor:before
content: ""
background-image: url("https://cdn.sup39.dev/img/anchor.svg")
width: 16px
height: 16px
vertical-align: middle
margin: 0 0 4px 0
display: inline-block
visibility: hidden

23
styles/code.sass Normal file
View file

@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 sup39
code
font-family: Menlo, Monaco, Consolas, "Courier New", "Hiragino Kaku Gothic ProN", monospace
padding: 2px 4px
margin: 0 1px 0 0
font-size: 90%
font-variant-numeric: slashed-zero
background-color: #4a4a4a
border-radius: 4px
white-space: nowrap
pre code
white-space: break-spaces
background: unset
padding: 0
margin: 0
pre
background: #333
color: #fff
border-radius: 4px
overflow-x: auto
padding: 0.6em

35
styles/heading.sass Normal file
View file

@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 sup39
h1, h2, h3, h4, h5, h6
margin: 0
h1
font-size: 2.5em
font-weight: 600
line-height: 1.3
margin-bottom: 0.5em
h2
font-size: 1.8em
font-weight: 500
line-height: 1.3
padding-bottom: 0.1em
border-bottom: 1px solid var(--bd)
margin-top: 1.0em
margin-bottom: 0.25em
h3
font-size: 1.5em
font-weight: bold
line-height: 1.6
margin-top: 0.5em
h4
font-size: 1.2em
font-weight: 600
margin-top: 1em
h5
font-size: 1.1em
font-weight: 500
margin-top: 1em
h6
font-size: 1em
font-weight: 400
margin-top: 1em

11
styles/index.sass Normal file
View file

@ -0,0 +1,11 @@
@import './vars.sass'
@import './nav.sass'
@import './menu-toggle.sass'
@import './heading.sass'
@import './span.sass'
@import './a.sass'
@import './code.sass'
@import './misc.sass'
@media only screen and (min-width: 768px)
@import './nav.pc.sass'

37
styles/menu-toggle.sass Normal file
View file

@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 sup39
.menu-toggle
width: 20px
height: 20px
display: block
cursor: pointer
.menu-toggle:before
top: 30%
.menu-toggle:after
top: 70%
.menu-toggle:before,
.menu-toggle:after
position: absolute
content: ""
width: 100%
height: 2px
transform: translate(0, -50%)
background: var(--fg)
transition: all .25s ease-out
.open
.menu-toggle:before
transform: translate(0, -50%) rotate(135deg)
.menu-toggle:after
transform: translate(0, -50%) rotate(-135deg)
.menu-toggle:before,
.menu-toggle:after
top: 50%
left: -10%
width: 120%
@media only screen and (min-width: 768px)
.menu-toggle
display: none

261
styles/misc.sass Normal file
View file

@ -0,0 +1,261 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 sup39
html
font-size: 16px
body
color: var(--fg)
background: var(--bg)
line-height: 1.6
font-weight: 300
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, Verdana, "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif
font-variant-numeric: tabular-nums
margin: 0
*
box-sizing: border-box
/**** font ****/
html:lang(zh-TW) body
font-family: "SF Pro TC","SF Pro Text","SF Pro Icons","Helvetica Neue","Helvetica","Arial",sans-serif
/**** image ****/
article img
max-width: 100%
height: auto
margin: 1em 0
#icon-link
text-decoration: none
> *
float: left
.icon
width: 72px
height: 72px
.icon-text
height: 72px
font-size: 27px
font-weight: 600
line-height: 1.2
padding-left: 16px
display: flex
flex-direction: column
align-items: center
justify-content: center
text-align: center
.pink
font-size: 24px
#icon-link:link
color: unset
/**** main ****/
main
padding: 24px
article
font-weight: 300
h3
+ table, + h4
margin-top: 0.5em
h4 + table
margin-top: 0.5em
table
margin-block-start: 1em
margin-block-end: 1em
table,
table th,
table td
border: solid 1px var(--bd)
border-collapse: collapse
padding: 4px
> ul
padding-inline-start: 2em
margin-block-start: 0.5em
margin-block-end: 1em
ul ul
padding-inline-start: 1.25em
> h2 + ul
padding-inline-start: 1.5em
footer
background: #18181e
background: #222
font-size: 0.75em
line-height: 1em
padding: 1.5em 2em
margin-top: 1em
p + *.compact
margin-block-start: -1em
*.compact
margin-block-start: 0em
/**** table ****/
.tac
text-align: center
.tar
text-align: right
table.tb-r tbody
text-align: right
/**** form ****/
form > div
display: flex
align-items: center
> *
margin: 3px
/**** input ****/
input, textarea
color: var(--fg)
background: #222
border: var(--bd) solid 1px
font-size: 1em
input[invalid]
background: #600
input[type="number"]
text-align: right
/**** button ****/
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
color: var(--fg)
background: #2a8
border: #2a8 1px solid
border-radius: 4px
font-size: 14px
cursor: pointer
button:hover,
input[type="button"]:hover,
input[type="submit"]:hover,
input[type="reset"]:hover
border: #197 1px solid
background: #197
button:active,
input[type="button"]:active,
input[type="submit"]:active,
input[type="reset"]:active
border: #075 1px solid
background: #075
button:disabled
background: #444
border: #555 1px solid
cursor: not-allowed
button[variant="warning"]
background: #c87603
border: #c87603 1px solid
button[variant="warning"]:enabled:hover
background: #b60
border: #b60 1px solid
button[variant="warning"]:enabled:active
background: #a50
border: #b60 1px solid
button[variant="danger"]:enabled
background: #c32
border: #c32 1px solid
button[variant="danger"]:enabled:hover
background: #b21
border: #b21 1px solid
button[variant="danger"]:enabled:active
background: #a10
border: #b21 1px solid
button.sm
font-size: 12px
/**** div.bt ****/
div.bt-plus, div.bt-minus
position: relative
width: 1.2em
height: 1.2em
border: #2ee5b8 1px solid
border-radius: 4px
cursor: pointer
user-select: none
-webkit-user-select: none
-moz-user-select: none
-khtml-user-select: none
-ms-user-select: none
div.bt-plus:before,
div.bt-plus:after,
div.bt-minus:before
content: ""
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
width: 50%
height: 1px
background: var(--fg)
div.bt-plus:after
width: 1px
height: 50%
/**** variant ****/
span.danger
color: #f66
button.danger
background: #a33
/**** details ****/
details
border: 1px solid
padding: 0.5em 1em
margin-block-start: 0.5em
margin-block-end: 0.5em
> summary
padding: 4px 0.5em
margin: -0.5em -1em -0.5em
details[open]
padding-bottom: 0
> summary
border-bottom: 1px solid
margin: -0.5em -1em 0
> ul, > ol
padding-inline-start: 1.5em
/**** misc ****/
video
max-width: 100%
.noselect
-webkit-touch-callout: none // iOS Safari
-webkit-user-select: none // Safari
-khtml-user-select: none // Konqueror HTML
-moz-user-select: none // Old versions of Firefox
-ms-user-select: none // Internet Explorer/Edge
user-select: none // Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox
/**** hide ****/
div.hide-ctn
position: relative
color: #fff
margin-block-start: -1em
margin-block-end: 2em
z-index:-1
*
position: absolute
font-size: 0.25em
color: #0000
/**** highlight ****/
table.code-lnum
padding: 0
margin: 0
width: 100%
border: none
td
border: none
td:first-child
padding-left: 0.5em
padding-right: 1em
text-align: right
td:last-child
width: 100%
white-space: pre
.hljs-comment
color: #7d6
.hljs-keyword
color: #ffa
font-weight: bold
.hljs-number, .hljs-string
color: #ffb7e7

32
styles/nav.pc.sass Normal file
View file

@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 sup39
nav
width: var(--nav-width)
overflow: auto
position: fixed
left: 0
top: 0
bottom: 0
border-bottom: none
border-right: 1px solid var(--bd)
padding: 1em 1em
header
margin-bottom: 1em
.menu-toggle
display: none
.nav-root
height: auto
opacity: 1
visibility: visible
main, footer
margin-left: var(--nav-width)
main
padding: 24px 32px
*:hover > a.anchor:before
visibility: visible
.mobile-only
display: none

107
styles/nav.sass Normal file
View file

@ -0,0 +1,107 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 sup39
$yellow: #ff9
$bd: var(--bd)
/** mobile */
nav
width: 100%
background: #28282e
border-bottom: 1px solid var(--bd)
border-right: none
padding: 1.25em 1.25em
header
display: flex
justify-content: center
position: relative
.menu-toggle
position: absolute
top: 50%
right: 1em
transform: translate(-50%, -50%)
.nav-root
height: 0
// opacity: 0
visibility: hidden
*
padding: 0
margin: 0
a
display: block
color: #eee
.nav-dir-child
padding-left: 1em
.nav-here, .nav-here a,
.nav-here + ul, .nav-here + ul a
color: $yellow
ul
list-style-type: square
margin-block-start: 0
margin-block-end: 0
padding-inline-start: 24px
.nav-root
> div
border-top: 1px solid $bd
padding: 0.5em 0.5em
/** heading list */
> ul
border-top: 1px solid $bd
padding-top: 0.75em
// margin-bottom: 0.5em
/** here */
.nav-here
font-weight: bold
/** container of entry */
.nav-entry
display: flex
align-items: center
> a
flex-grow: 1
> svg
stroke: #9ff
fill: none
stroke-width: 1
stroke-linecap: round
stroke-linejoin: round
width: 1.2em
height: 1.2em
cursor: pointer
border-radius: 50%
> svg:hover
background: #18181e
/** folder hide */
.nav-dir-child
display: none
.nav-fold-open > .nav-dir-child
display: block
.nav-fold-open > .nav-entry > svg
rotate: 180deg
nav.open
header
margin-bottom: 1.25em
.nav-root
height: auto
opacity: 1
visibility: visible
// transition: opacity 0.4s ease-in-out

29
styles/span.sass Normal file
View file

@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 sup39
.mint
color: var(--mint)
.pink
color: var(--pink)
.kw
color: #9ff
font-weight: bold
.mk
color: #ff99e5
font-weight: bold
.y
color: #ff7
font-weight: bold
.note
color: #777
.arg
color: #da8cff
font-weight: bold
.u
text-decoration: underline
font-weight: bold
.mono
font-family: monospace
span[title]
cursor: help
text-decoration: dotted underline

10
styles/vars.sass Normal file
View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 sup39
:root
--bg: #222228
--fg: #eee
--bd: #777
--mint: #2ee5b8
--pink: #e58acf
--nav-width: 256px

16
supMDX-env.d.ts vendored Normal file
View file

@ -0,0 +1,16 @@
declare module '#config' {
import type {NavEntry} from '@/Nav';
type Config = {
startYear: number
siteAuthor?: string
metaFields?: {label: string, prop: string}[]
nav: NavEntry[]
};
const config: Config;
export default config;
}
declare module '*.yaml' {
const data: any;
export default data;
}

7
supMDX.yml Normal file
View file

@ -0,0 +1,7 @@
startYear: 2022
siteAuthor: <Edit author name in supMDX.yml>
metaFields:
- {prop: author, label: 'Author: '}
- {prop: createdAt, label: 'Created at '}
- {prop: updatedAt, label: 'Updated at '}
nav: []

37
tsconfig.json Normal file
View file

@ -0,0 +1,37 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["components/*"],
"#/*": ["pages/*"],
"#config": ["supMDX.yml"],
"%/*": ["utils/*"]
},
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}

3233
yarn.lock Normal file

File diff suppressed because it is too large Load diff