├── .browserslistrc ├── .eleventy.js ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .stylelintrc ├── README.md ├── babel.config.js ├── netlify.toml ├── package.json ├── rollup.config.js ├── src ├── _data │ └── pages.js ├── _includes │ └── layout.njk ├── app.js ├── components │ ├── App.js │ ├── App.scss │ ├── BaseWrapper.js │ ├── BaseWrapper.scss │ ├── LayoutDefault.js │ ├── LayoutDefault.scss │ ├── LikeForm.js │ ├── LikeForm.scss │ ├── SectionContent.js │ ├── SectionHero.js │ ├── SectionHero.scss │ ├── SectionMasonry.js │ ├── SectionMasonry.scss │ ├── SectionTeaser.js │ ├── SectionTeaser.scss │ └── with-hydration.js ├── page.11ty.js ├── styles │ ├── generic │ │ ├── base.scss │ │ └── box-sizing-reset.scss │ ├── index.scss │ ├── scopes │ │ └── content.scss │ └── utilities │ │ └── text-align.scss └── utils │ ├── is-server.js │ └── when-visible.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 0.5% 2 | not ie <= 10 3 | not op_mini all 4 | not dead 5 | -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | module.exports = function eleventy() { 2 | return { 3 | dir: { 4 | input: `src`, 5 | output: `dist`, 6 | }, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | 4 | !.eleventy.js 5 | !.eslintrc.js 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | `preact`, 5 | `@avalanche/eslint-config`, 6 | ], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === `production` ? `error` : `warn`, 9 | 'no-debugger': process.env.NODE_ENV === `production` ? `error` : `warn`, 10 | 'no-underscore-dangle': `off`, 11 | }, 12 | parserOptions: { 13 | ecmaVersion: 2020, 14 | parser: `babel-eslint`, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.log 5 | *.orig 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.tgz 10 | *.vi 11 | *.zip 12 | *~ 13 | 14 | # OS or Editor folders 15 | ._* 16 | .cache 17 | .DS_Store 18 | .idea 19 | .project 20 | .settings 21 | .tmproj 22 | *.esproj 23 | *.sublime-project 24 | *.sublime-workspace 25 | nbproject 26 | Thumbs.db 27 | 28 | # Folders to ignore 29 | dist 30 | node_modules 31 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@avalanche/stylelint-config", 3 | "rules": { 4 | "max-nesting-depth": 4 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eleventy-preact 2 | 3 | [![Patreon](https://img.shields.io/badge/patreon-donate-blue.svg)](https://www.patreon.com/maoberlehner) 4 | [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/maoberlehner) 5 | 6 | ## Build Setup 7 | 8 | ```bash 9 | # Serve with hot reload. 10 | npm run dev 11 | 12 | # Build for production with minification. 13 | npm run build 14 | 15 | # Lint all files. 16 | npm run lint 17 | ``` 18 | 19 | ## About 20 | 21 | ### Author 22 | 23 | Markus Oberlehner 24 | Website: https://markus.oberlehner.net 25 | Twitter: https://twitter.com/MaOberlehner 26 | PayPal.me: https://paypal.me/maoberlehner 27 | Patreon: https://www.patreon.com/maoberlehner 28 | 29 | ### License 30 | 31 | MIT 32 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | `@babel/preset-env`, 5 | { 6 | loose: true, 7 | modules: false, 8 | }, 9 | ], 10 | ], 11 | plugins: [ 12 | // See https://github.com/developit/htm/tree/master/packages/babel-plugin-htm 13 | // for configuration options. 14 | [`babel-plugin-htm`, { 15 | import: `preact`, 16 | }], 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/like/*" 3 | to = "/" 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eleventy-preact", 3 | "version": "0.1.0", 4 | "author": "Markus Oberlehner", 5 | "homepage": "https://github.com/maoberlehner/eleventy-preact", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "build": "NODE_ENV=production concurrently 'eleventy' 'npm run scripts:app' 'npm run styles'", 10 | "dev": "NODE_ENV=dev concurrently 'eleventy --serve' 'npm run watch'", 11 | "scripts:app": "rollup --config --file dist/app.js --format iife --name App src/app.js", 12 | "scripts:app:watch": "npm run scripts:app -- --watch", 13 | "styles": "node-sass --importer node_modules/node-sass-magic-importer/dist/cli.js -o dist src/styles/index.scss", 14 | "styles:watch": "chokidar 'src/**/*.scss' -c 'npm run styles'", 15 | "watch": "concurrently 'npm run scripts:app:watch' 'npm run styles:watch'", 16 | "lint:scripts": "eslint --ext .js .", 17 | "lint:styles": "stylelint 'src/**/*.scss'", 18 | "lint": "concurrently 'npm run lint:scripts' 'npm run lint:styles'" 19 | }, 20 | "dependencies": { 21 | "htm": "^3.0.3", 22 | "preact": "^10.3.4", 23 | "reset-css": "^5.0.1" 24 | }, 25 | "devDependencies": { 26 | "@11ty/eleventy": "^0.10.0", 27 | "@avalanche/eslint-config": "^4.0.0", 28 | "@avalanche/stylelint-config": "^1.0.1", 29 | "@babel/preset-env": "^7.9.0", 30 | "@rollup/plugin-commonjs": "^11.0.2", 31 | "@rollup/plugin-node-resolve": "^7.1.1", 32 | "babel-eslint": "^10.1.0", 33 | "babel-plugin-htm": "^3.0.0", 34 | "chokidar-cli": "^2.1.0", 35 | "concurrently": "^5.1.0", 36 | "eslint": "^6.8.0", 37 | "eslint-config-preact": "^1.1.1", 38 | "eslint-plugin-import": "^2.20.1", 39 | "node-sass": "^4.13.1", 40 | "node-sass-magic-importer": "^5.3.2", 41 | "preact-render-to-string": "^5.1.4", 42 | "rollup": "^2.2.0", 43 | "rollup-plugin-babel": "^4.4.0", 44 | "rollup-plugin-filesize": "^6.2.1", 45 | "rollup-plugin-terser": "^5.3.0", 46 | "stylelint": "^13.2.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from 'rollup-plugin-terser'; 2 | import babel from 'rollup-plugin-babel'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import filesize from 'rollup-plugin-filesize'; 5 | import resolve from '@rollup/plugin-node-resolve'; 6 | 7 | export default { 8 | plugins: [ 9 | resolve(), 10 | commonjs(), 11 | babel(), 12 | terser(), 13 | filesize(), 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /src/_data/pages.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | date: new Date(), 4 | name: `Hello World`, 5 | sections: [ 6 | { 7 | name: `hero`, 8 | data: { 9 | image: `https://images.unsplash.com/photo-1518699705938-d9be21ec6ff6?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1920&h=450&q=50`, 10 | title: `Welcome!`, 11 | text: `This is a site built with Eleventy + Preact.`, 12 | }, 13 | }, 14 | { 15 | name: `content`, 16 | data: { 17 | html: `

Lorem Ipsum

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.

Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.

Dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.

Ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.

Make sure to also visit Page 2!

`, 18 | }, 19 | }, 20 | { 21 | name: `masonry`, 22 | data: { 23 | images: [ 24 | { 25 | alt: `Snowy mountains.`, 26 | id: 1, 27 | src: `https://images.unsplash.com/reserve/4JUAYmLTuS1mBvBBwe3D_St_Anton.png?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80`, 28 | }, 29 | { 30 | alt: `Snowy mountains.`, 31 | id: 2, 32 | src: `https://images.unsplash.com/photo-1464277621354-a197b03f19cb?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80`, 33 | }, 34 | { 35 | alt: `Snowy mountains.`, 36 | id: 3, 37 | src: `https://images.unsplash.com/photo-1467049878711-ebcf8c4b23da?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80`, 38 | }, 39 | { 40 | alt: `Snowy mountains.`, 41 | id: 4, 42 | src: `https://images.unsplash.com/photo-1464852045489-bccb7d17fe39?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80`, 43 | }, 44 | { 45 | alt: `Snowy mountains.`, 46 | id: 5, 47 | src: `https://images.unsplash.com/photo-1462351850891-7aedcb9e2450?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80`, 48 | }, 49 | { 50 | alt: `Snowy mountains.`, 51 | id: 6, 52 | src: `https://images.unsplash.com/photo-1465726208258-198e6f4402ae?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80`, 53 | }, 54 | ], 55 | }, 56 | }, 57 | { 58 | name: `teaser`, 59 | data: [ 60 | { 61 | image: `https://images.unsplash.com/photo-1569938298692-3547607c467a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjF9&auto=format&fit=crop&w=600&h=600&q=30`, 62 | title: `Lorem Ipsum`, 63 | text: `Tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.`, 64 | link: `https://markus.oberlehner.net`, 65 | }, 66 | { 67 | image: `https://images.unsplash.com/photo-1562650740-ce0f04d0d73e?ixlib=rb-1.2.1&auto=format&fit=crop&w=600&h=600&q=30`, 68 | title: `Lorem Ipsum`, 69 | text: `Tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.`, 70 | link: `https://markus.oberlehner.net`, 71 | }, 72 | { 73 | image: `https://images.unsplash.com/photo-1580565387371-f73cd4b83859?ixlib=rb-1.2.1&auto=format&fit=crop&w=600&h=600&q=30`, 74 | title: `Lorem Ipsum`, 75 | text: `Tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.`, 76 | link: `https://markus.oberlehner.net`, 77 | }, 78 | ], 79 | }, 80 | ], 81 | slug: `/`, 82 | }, 83 | { 84 | date: new Date(), 85 | name: `Page 2`, 86 | sections: [ 87 | { 88 | name: `hero`, 89 | data: { 90 | image: `https://images.unsplash.com/photo-1517690623533-ca77a9a4b402?ixlib=rb-1.2.1&auto=format&fit=crop&w=1920&h=450&q=50`, 91 | title: `Welcome to page 2!`, 92 | text: `This is a site built with Eleventy + Preact.`, 93 | }, 94 | }, 95 | { 96 | name: `content`, 97 | data: { 98 | html: `

Lorem Ipsum

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.

Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.

Dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.

Ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.

`, 99 | }, 100 | }, 101 | ], 102 | slug: `/page-2`, 103 | }, 104 | ]; 105 | -------------------------------------------------------------------------------- /src/_includes/layout.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{ content | safe }} 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const { html, render } = require(`htm/preact`); 2 | 3 | const whenVisible = require(`./utils/when-visible`); 4 | 5 | const LikeForm = require(`./components/LikeForm`); 6 | 7 | const componentMap = { 8 | LikeForm, 9 | }; 10 | 11 | const $componentMarkers = document.querySelectorAll(`[data-cmp-id]`); 12 | 13 | Array.from($componentMarkers).forEach(($marker) => { 14 | const $component = $marker.nextElementSibling; 15 | 16 | whenVisible($component, () => { 17 | const { name, props } = window.__STATE__.components[$marker.dataset.cmpId]; 18 | const Component = componentMap[name]; 19 | 20 | render(html`<${Component} ...${props}/>`, $component.parentNode, $component); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | const { html } = require(`htm/preact`); 2 | 3 | const LayoutDefault = require(`./LayoutDefault`); 4 | const SectionContent = require(`./SectionContent`); 5 | const SectionHero = require(`./SectionHero`); 6 | const SectionMasonry = require(`./SectionMasonry`); 7 | const SectionTeaser = require(`./SectionTeaser`); 8 | 9 | const sections = { 10 | content: SectionContent, 11 | hero: SectionHero, 12 | masonry: SectionMasonry, 13 | teaser: SectionTeaser, 14 | }; 15 | 16 | module.exports = ({ page }) => html` 17 | <${LayoutDefault}> 18 |
19 | ${page.sections.map(({ data, name }) => html` 20 | <${sections[name]} data=${data}/> 21 | `)} 22 |
23 | 24 | `; 25 | -------------------------------------------------------------------------------- /src/components/App.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | > :not(:first-child) { 3 | margin-top: 3em; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/BaseWrapper.js: -------------------------------------------------------------------------------- 1 | const { html } = require(`htm/preact`); 2 | 3 | module.exports = ({ children }) => html` 4 |
5 | ${children} 6 |
7 | `; 8 | -------------------------------------------------------------------------------- /src/components/BaseWrapper.scss: -------------------------------------------------------------------------------- 1 | .BaseWrapper { 2 | margin-right: auto; 3 | margin-left: auto; 4 | max-width: 48em; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/LayoutDefault.js: -------------------------------------------------------------------------------- 1 | const { html } = require(`htm/preact`); 2 | 3 | const BaseWrapper = require(`./BaseWrapper`); 4 | 5 | module.exports = ({ children }) => html` 6 |
7 |
8 | ${children} 9 |
10 | 15 |
16 | `; 17 | -------------------------------------------------------------------------------- /src/components/LayoutDefault.scss: -------------------------------------------------------------------------------- 1 | .LayoutDefault__footer { 2 | margin-top: 5em; 3 | padding-top: 1em; 4 | padding-bottom: 1em; 5 | border-top: 1px solid #efefef; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/LikeForm.js: -------------------------------------------------------------------------------- 1 | const { html } = require(`htm/preact`); 2 | const { useState } = require(`preact/hooks`); 3 | 4 | const withHydration = require(`./with-hydration`); 5 | 6 | function LikeForm({ id }) { 7 | const [likes, setLikes] = useState(0); 8 | const handleClick = (e) => { 9 | e.preventDefault(); 10 | setLikes(likes + 1); 11 | }; 12 | 13 | return html` 14 |
19 | 26 | ${likes} 27 |
28 | `; 29 | } 30 | 31 | module.exports = withHydration(LikeForm); 32 | -------------------------------------------------------------------------------- /src/components/LikeForm.scss: -------------------------------------------------------------------------------- 1 | .LikeForm__button { 2 | margin-right: 0.5em; 3 | padding: 0; 4 | border: none; 5 | background-color: transparent; 6 | cursor: pointer; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/SectionContent.js: -------------------------------------------------------------------------------- 1 | const { html } = require(`htm/preact`); 2 | 3 | const BaseWrapper = require(`./BaseWrapper`); 4 | 5 | module.exports = ({ data }) => html` 6 | <${BaseWrapper}> 7 |
11 | 12 | `; 13 | -------------------------------------------------------------------------------- /src/components/SectionHero.js: -------------------------------------------------------------------------------- 1 | const { html } = require(`htm/preact`); 2 | 3 | module.exports = ({ data }) => html` 4 |
5 | ${data.title} 10 |

11 | 12 | ${data.title} 13 | 14 | ${data.text} 15 |

16 |
17 | `; 18 | -------------------------------------------------------------------------------- /src/components/SectionHero.scss: -------------------------------------------------------------------------------- 1 | .SectionHero { 2 | position: relative; 3 | } 4 | 5 | .SectionHero__image { 6 | max-width: 100%; 7 | height: auto; 8 | } 9 | 10 | .SectionHero__info { 11 | display: flex; 12 | position: absolute; 13 | top: 0; 14 | right: 0; 15 | bottom: 0; 16 | left: 0; 17 | flex-direction: column; 18 | justify-content: center; 19 | align-items: center; 20 | color: #fff; 21 | text-shadow: 0 0 0.75em rgba(0, 0, 0, 0.4); 22 | font-size: 2em; 23 | } 24 | 25 | .SectionHero__title { 26 | margin-bottom: 0.25em; 27 | font-size: 2em; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/SectionMasonry.js: -------------------------------------------------------------------------------- 1 | const { html } = require(`htm/preact`); 2 | 3 | const BaseWrapper = require(`./BaseWrapper`); 4 | const LikeForm = require(`./LikeForm`); 5 | 6 | module.exports = ({ data }) => html` 7 | <${BaseWrapper}> 8 |
9 | ${data.images.map(({ alt, id, src }) => html` 10 |
11 | ${alt} 17 | <${LikeForm} id=${id}/> 18 |
19 | `)} 20 |
21 | 22 | `; 23 | -------------------------------------------------------------------------------- /src/components/SectionMasonry.scss: -------------------------------------------------------------------------------- 1 | .SectionMasonry { 2 | margin-top: -1em; 3 | columns: 3 242px; 4 | column-gap: 1em; 5 | text-align: right; 6 | } 7 | 8 | .SectionMasonry__item { 9 | padding-top: 1em; 10 | page-break-inside: avoid; 11 | } 12 | 13 | .SectionMasonry__image { 14 | max-width: 100%; 15 | height: auto; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/SectionTeaser.js: -------------------------------------------------------------------------------- 1 | const { html } = require(`htm/preact`); 2 | 3 | const BaseWrapper = require(`./BaseWrapper`); 4 | 5 | module.exports = ({ data }) => html` 6 | <${BaseWrapper}> 7 |
8 | ${data.map(teaser => html` 9 | 28 | `)} 29 |
30 | 31 | `; 32 | -------------------------------------------------------------------------------- /src/components/SectionTeaser.scss: -------------------------------------------------------------------------------- 1 | .SectionTeaser { 2 | display: flex; 3 | margin-top: -1em; 4 | margin-left: -1em; 5 | flex-wrap: wrap; 6 | } 7 | 8 | .SectionTeaser__item { 9 | padding-top: 1em; 10 | padding-left: 1em; 11 | min-width: 14em; 12 | flex-basis: 0%; 13 | flex-grow: 1; 14 | flex-shrink: 1; 15 | } 16 | 17 | .SectionTeaser__image { 18 | max-width: 100%; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/with-hydration.js: -------------------------------------------------------------------------------- 1 | const { html } = require(`htm/preact`); 2 | 3 | const isServer = require(`../utils/is-server`); 4 | 5 | let id = 0; 6 | 7 | module.exports = Component => (props) => { 8 | id += 1; 9 | 10 | const scriptSrc = ` 11 | window.__STATE__.components[${id}]={name:${JSON.stringify(Component.name)},props:${JSON.stringify(props)}} 12 | `; 13 | 14 | return html` 15 | ${isServer && html``} 16 | <${Component} ...${props}/> 17 | `; 18 | }; 19 | -------------------------------------------------------------------------------- /src/page.11ty.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | const { html } = require(`htm/preact`); 3 | const render = require(`preact-render-to-string`); 4 | 5 | const App = require(`./components/App`); 6 | 7 | module.exports = class Page { 8 | data() { 9 | return { 10 | title: `Setting up Eleventy with Preact and htm`, 11 | layout: `layout.njk`, 12 | pagination: { 13 | data: `pages`, 14 | size: 1, 15 | alias: `page`, 16 | addAllPagesToCollections: true, 17 | }, 18 | permalink: ({ page }) => `/${page.slug}/index.html`, 19 | }; 20 | } 21 | 22 | render(data) { 23 | return render(html`<${App} page=${data.page}/>`); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/styles/generic/base.scss: -------------------------------------------------------------------------------- 1 | html { 2 | text-size-adjust: 100%; 3 | } 4 | 5 | body { 6 | font-weight: 300; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/generic/box-sizing-reset.scss: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | 5 | * { 6 | &, 7 | &::before, 8 | &::after { 9 | box-sizing: inherit; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | // Generic 2 | @import '~reset-css/sass/reset'; 3 | @import './generic/**/*.scss'; 4 | 5 | // Components 6 | @import '../components/**/*.scss'; 7 | 8 | // Scopes 9 | @import './scopes/**/*.scss'; 10 | 11 | // Utilities 12 | @import './utilities/**/*.scss'; 13 | -------------------------------------------------------------------------------- /src/styles/scopes/content.scss: -------------------------------------------------------------------------------- 1 | .s-content { 2 | line-height: 1.5; 3 | 4 | > :not(:first-child) { 5 | margin-top: 1em; 6 | } 7 | 8 | h1 { 9 | font-size: 2.027em; 10 | } 11 | 12 | h2 { 13 | font-size: 1.802em; 14 | } 15 | 16 | h3 { 17 | font-size: 1.602em; 18 | } 19 | 20 | h4 { 21 | font-size: 1.424em; 22 | } 23 | 24 | h5 { 25 | font-size: 1.266em; 26 | } 27 | 28 | strong { 29 | font-weight: 600; 30 | } 31 | 32 | li { 33 | list-style-type: none; 34 | 35 | &::before { 36 | margin-right: 0.5em; 37 | content: '●'; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/styles/utilities/text-align.scss: -------------------------------------------------------------------------------- 1 | .u-text-align-center { 2 | text-align: center; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/is-server.js: -------------------------------------------------------------------------------- 1 | module.exports = typeof window === `undefined`; 2 | -------------------------------------------------------------------------------- /src/utils/when-visible.js: -------------------------------------------------------------------------------- 1 | module.exports = function whenVisible($element, callback, options) { 2 | if (typeof IntersectionObserver === `undefined`) { 3 | callback(); 4 | return; 5 | } 6 | 7 | const observer = new IntersectionObserver((entries) => { 8 | entries.forEach((entry) => { 9 | if (!entry.isIntersecting) return; 10 | observer.unobserve($element); 11 | callback(); 12 | }); 13 | }, options); 14 | observer.observe($element); 15 | }; 16 | --------------------------------------------------------------------------------