├── src ├── scripts │ ├── js │ │ └── is-land.js │ └── jsx │ │ ├── app.jsx │ │ └── fetch.jsx ├── assets │ ├── webfonts │ │ └── battlestar │ │ │ ├── BattleStar.ttf │ │ │ ├── info.txt │ │ │ ├── BattleStarItalic.ttf │ │ │ └── misc │ │ │ └── license.txt │ └── svg │ │ ├── esbuild-logo.svg │ │ ├── 11ty-logo.svg │ │ └── solid-logo.svg ├── _data │ ├── manifest.json │ ├── meta.js │ └── buildmeta.json ├── _includes │ ├── components │ │ └── menu.webc │ ├── base.webc │ ├── default.html │ └── solid-base.svg ├── index.md └── style │ └── style.scss ├── .gitattributes ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── 11tybuild-update.yml ├── config ├── clean │ └── clean.js ├── shortcodes │ └── solidify.js └── build │ ├── purgecss.js │ └── esbuild.js ├── LICENSE.md ├── package.json ├── eleventy.config.js └── README.md /src/scripts/js/is-land.js: -------------------------------------------------------------------------------- 1 | import '@11ty/is-land/is-land.js'; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Templates 2 | *.webc text 3 | 4 | # Reclassify .webc files as HTML: 5 | *.webc linguist-language=HTML 6 | -------------------------------------------------------------------------------- /src/assets/webfonts/battlestar/BattleStar.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woodcox/11ty-solid-base/HEAD/src/assets/webfonts/battlestar/BattleStar.ttf -------------------------------------------------------------------------------- /src/assets/webfonts/battlestar/info.txt: -------------------------------------------------------------------------------- 1 | license: Freeware, commercial use requires donation 2 | link: https://www.fontspace.com/battle-star-font-f44459 -------------------------------------------------------------------------------- /src/assets/webfonts/battlestar/BattleStarItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woodcox/11ty-solid-base/HEAD/src/assets/webfonts/battlestar/BattleStarItalic.ttf -------------------------------------------------------------------------------- /src/_data/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "app-S5HW3ASK.min.js", 3 | "fetch": "fetch-BUN3JDA4.min.js", 4 | "is-land": "is-land-TXS7GROR.min.js", 5 | "style": "style-ZSG4TA35.min.css" 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies installed by npm 2 | node_modules 3 | 4 | # build artifacts 5 | dist 6 | _tmp 7 | 8 | # secrets and errors 9 | .env 10 | .log 11 | 12 | # macOS related files 13 | .DS_Store 14 | 15 | #IDE 16 | .vscode 17 | -------------------------------------------------------------------------------- /src/_includes/components/menu.webc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/esbuild-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | -------------------------------------------------------------------------------- /src/_data/meta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | url: process.env.URL || 'http://localhost:8080', 3 | environment: process.env.ELEVENTY_ENV, 4 | nav: [ 5 | { 6 | text: 'Home', 7 | href: '/' 8 | }, 9 | { 10 | text: 'About', 11 | href: '/about/' 12 | }, 13 | { 14 | text: 'Inline', 15 | href: '/inline/' 16 | }, 17 | { 18 | text: 'Islands', 19 | href: '/island/' 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/scripts/jsx/app.jsx: -------------------------------------------------------------------------------- 1 | import { createSignal, onCleanup } from "solid-js"; 2 | import { render } from "solid-js/web"; 3 | 4 | const CountingComponent = () => { 5 | const [count, setCount] = createSignal(0); 6 | const interval = setInterval( 7 | () => setCount(c => c + 1), 8 | 1000 9 | ); 10 | onCleanup(() => clearInterval(interval)); 11 | return
Count value is {count()}
; 12 | }; 13 | 14 | render(() => , document.getElementById("app")); 15 | -------------------------------------------------------------------------------- /src/assets/webfonts/battlestar/misc/license.txt: -------------------------------------------------------------------------------- 1 | This font is for personal use ONLY ... although donations are appreciated! 2 | 3 | Kindly give as much as you honestly feel the font is worth to you. 4 | 5 | Paypal account for donation: 6 | 7 | dawsonjoseph333@gmail.com 8 | 9 | For commercial use you must pay $10 USD. 10 | 11 | Link to purchase a commercial license : 12 | https://www.creativefabrica.com/product/battle-star/ref/236312/ 13 | 14 | Please visit our store for more great fonts: 15 | https://www.creativefabrica.com/ref/236312/ 16 | -------------------------------------------------------------------------------- /src/_includes/base.webc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <template @text="title" webc:nokeep></template> 7 | 8 | 9 | 10 | 11 | 12 |
13 |

14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/_includes/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ title }}

14 |
15 |
16 | {{ content }} 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /config/clean/clean.js: -------------------------------------------------------------------------------- 1 | // script to delete files from ./dist/app & ./_tmp folders prior to build 2 | const fs = require('fs'); 3 | 4 | function deleteFolderRecursive(path) { 5 | if (fs.existsSync(path) && fs.lstatSync(path).isDirectory()) { 6 | fs.readdirSync(path).forEach(function(file, index){ 7 | const curPath = path + "/" + file; 8 | 9 | if (fs.lstatSync(curPath).isDirectory()) { // recurse 10 | deleteFolderRecursive(curPath); 11 | } else { // delete file 12 | fs.unlinkSync(curPath); 13 | } 14 | }); 15 | 16 | console.log(`Deleting directory "${path}"...`); 17 | fs.rmdirSync(path); 18 | } 19 | } 20 | 21 | console.log("Cleaning working tree..."); 22 | 23 | // Add any folers here you want to delete prior to build 24 | 25 | deleteFolderRecursive("./dist/app"); 26 | deleteFolderRecursive("./_tmp"); 27 | 28 | console.log("Successfully cleaned working tree!"); 29 | 30 | // recreate directory 31 | const dir = './_tmp'; 32 | 33 | if (!fs.existsSync(dir)){ 34 | fs.mkdirSync(dir); 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Paul Woodcock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/shortcodes/solidify.js: -------------------------------------------------------------------------------- 1 | const esbuild = require("esbuild"); 2 | const isProd = process.env.ELEVENTY_ENV === 'prod' ? true : false 3 | const { solidPlugin } = require('esbuild-plugin-solid'); 4 | const fsPromises = require('fs').promises; 5 | const { http, default_schemes } = require('@hyrious/esbuild-plugin-http'); 6 | 7 | module.exports = async (code, filename, bundled) => { 8 | let bundleJsx = bundled !== 'bundleOff' ? true : false; 9 | await fsPromises.writeFile('./_tmp/solid-' + filename + '.jsx', code), 10 | 11 | await esbuild.build({ 12 | entryPoints: ['_tmp/solid-*.jsx'], 13 | entryNames: '[name]', 14 | // write: false, 15 | outdir: './_tmp/app', 16 | bundle: bundleJsx, 17 | format: 'esm', 18 | minify: isProd, 19 | treeShaking: isProd, 20 | target: isProd ? 'es6' : 'esnext', 21 | plugins: [ 22 | http({ 23 | filter: (url) => true, 24 | schemes: { default_schemes }, 25 | cache: new Map() 26 | }), 27 | solidPlugin() 28 | ] 29 | }).catch((err) => { 30 | console.error(err); 31 | process.exitCode = 1; 32 | }) 33 | const solidifyJsx = await fsPromises.readFile('./_tmp/app/solid-' + filename + '.js', 'utf8'); 34 | return ``; 35 | }; -------------------------------------------------------------------------------- /src/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default.html 3 | title: 11ty-solid-base 4 | author: Rustie Woodcox 5 | --- 6 | 7 | 8 | 9 | ## The counter 10 | To test if the island partial hydration is working; on a mobile phone, turn the phone to landscape view. This should swap the html for javascript to start the counter. 11 | 12 | 13 | 14 |

Count value is 0

15 |
16 | 20 |
21 | 22 | 23 | ## The shortcode 24 | The shortcode adds the js inline. Need to add import maps for this. 25 | 26 | 27 | 28 |

The solidify shortcode is inactive :)

29 |
30 | 44 |
45 | 46 | ## SolidJS 47 | A more interesting example 48 | 49 |

50 | -------------------------------------------------------------------------------- /src/assets/svg/11ty-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/svg/solid-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "11ty-solid-base", 3 | "version": "1.2.0", 4 | "description": "A minimal HTML5 template and the esbuild setup to compile your Solidjs app alongside 11ty.", 5 | "main": ".eleventy.js", 6 | "scripts": { 7 | "clean": "node config/clean/clean.js", 8 | "dev:sass": "sass --no-source-map --watch src/style:./dist/app", 9 | "dev:eleventy": "ELEVENTY_ENV=dev npx @11ty/eleventy --serve --incremental --output=dist", 10 | "build:sass": "sass --no-source-map src/style:./dist/app", 11 | "build:eleventy": "npx @11ty/eleventy", 12 | "prefix": "lightningcss --browserslist ./dist/app/*.css -o ./dist/app/*.css", 13 | "start": "NODE_ENV=dev conc --kill-others 'npm:dev:*'", 14 | "cloud": "npm run clean && npm run build:sass && NODE_ENV=staging ELEVENTY_ENV=staging npm run build:eleventy -- --incremental", 15 | "build": "npm run clean && npm run build:sass && npm run prefix && NODE_ENV=staging ELEVENTY_ENV=staging npm run build:eleventy -- --incremental --pathprefix=11ty-solid-base", 16 | "minify": "npm run clean && npm run build:sass && npm run prefix -- --minify && NODE_ENV=production ELEVENTY_ENV=prod npm run build:eleventy -- --incremental --pathprefix=11ty-solid-base" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/woodcox/11ty-solid-base.git" 21 | }, 22 | "author": "woodcox", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "@11ty/eleventy": "^2.0.1", 26 | "@11ty/eleventy-plugin-webc": "^0.11.2", 27 | "@11ty/is-land": "^4.0.0", 28 | "@hyrious/esbuild-plugin-http": "^0.1.5", 29 | "@luncheon/esbuild-plugin-gzip": "^0.1.0", 30 | "concurrently": "^8.2.2", 31 | "esbuild": "^0.19.12", 32 | "esbuild-plugin-manifest": "^1.0.3", 33 | "esbuild-plugin-purgecss-2": "^1.0.1", 34 | "esbuild-plugin-solid": "^0.5.0", 35 | "lightningcss-cli": "^1.23.0", 36 | "purgecss": "^5.0.0", 37 | "sass": "^1.71.0", 38 | "solid-js": "^1.8.15" 39 | }, 40 | "browserslist": [ 41 | "> 0.2%", 42 | "last 2 versions", 43 | "Firefox ESR", 44 | "not dead" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /config/build/purgecss.js: -------------------------------------------------------------------------------- 1 | // A modified version of https://github.com/arslanakram/esbuild-plugin-purgecss-2.0 2 | const esbuild = require('esbuild'); 3 | const fs = require('fs'); 4 | const { PurgeCSS } = require('purgecss'); 5 | const path = require('path'); 6 | const buildMetafile = JSON.parse(fs.readFileSync('./src/_data/buildmeta.json', 'utf8')); 7 | 8 | let purgecssPlugin = function purgecssPlugin(options) { 9 | return { 10 | name: 'purgecss', 11 | setup(build) { 12 | if (!buildMetafile) { 13 | throw new Error('Make sure eleventy generates a buildmeta.json from .src/config/build/esbuild.js'); 14 | } 15 | 16 | build.onEnd(async (args) => { 17 | path: args.path; 18 | // outputKeyss gets metafile build output of .js files and .css files 19 | const outputKeys = Object.keys(buildMetafile.outputs); 20 | console.log(outputKeys); 21 | // create a file extension filter 22 | const genFilter = (postfix) => (k) => k.endsWith(postfix); 23 | // filter the metafile output to only return the css files 24 | const css = outputKeys.filter(genFilter('.css')); 25 | 26 | // Create a jS object of the purgecss config for css 27 | let cssConfig = { css: css }; 28 | // Merge the css config with the purgecssPlugin options 29 | let config = Object.assign(options, cssConfig); 30 | 31 | // check if there is the purgecss config js object and throw error if not 32 | const opts = config ? config : () => {}; 33 | 34 | // pass the purgecss config including the relevant css file to purgecss 35 | const purgeResult = await new PurgeCSS().purge({ ...opts }); 36 | 37 | for (let index = 0; index < purgeResult.length; index++) { 38 | const { file, css } = purgeResult[index]; 39 | await fs.promises.writeFile(file, css); 40 | } 41 | }); 42 | }, 43 | }; 44 | }; 45 | 46 | 47 | module.exports = async () => { 48 | let result = await esbuild.build({ 49 | entryPoints: ['dist/app/*.css'], 50 | allowOverwrite: true, 51 | minify: true, 52 | outdir: './dist/app', 53 | plugins: [ 54 | purgecssPlugin({ 55 | // For your production build. Add other content by using a glob-all pattern glob.sync(["dist/*.html", "dist/**/index.html"]) 56 | content: ["src/scripts/jsx/*.jsx", "src/scripts/jsx/**/*.jsx", "dist/index.html"] 57 | }) 58 | ] 59 | }) 60 | }; 61 | -------------------------------------------------------------------------------- /.github/workflows/11tybuild-update.yml: -------------------------------------------------------------------------------- 1 | name: Build 11ty and css 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | # Cancel a previous build if still running 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | # Conditional environmental if's 13 | env: 14 | UPDATE_NPM: false # false = deploy eleventy , true = update npm 15 | 16 | jobs: 17 | build_deploy_eleventy: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout repo 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up Node.js 18.x 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: '18' 27 | cache: 'npm' 28 | 29 | - name: Install dependencies 30 | if: env.UPDATE_NPM != 'true' 31 | run: npm i # npm update --save 32 | 33 | - name: Update dependencies 34 | if: env.UPDATE_NPM == 'true' 35 | run: npm update --save 36 | 37 | - name: Npm Build 38 | if: env.UPDATE_NPM != 'true' 39 | run: npm run minify 40 | 41 | - name: Commit buildmeta and manifest changes 42 | if: env.UPDATE_NPM != 'true' 43 | uses: EndBug/add-and-commit@v9 44 | with: 45 | default_author: github_actions 46 | message: "Update json files" 47 | add: '["*.json"]' 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | - name: Npm Rebuild # for buildmeta and manifest changes to take effect 52 | if: env.UPDATE_NPM != 'true' 53 | run: npm run minify 54 | 55 | - name: Deploy 56 | if: env.UPDATE_NPM != 'true' 57 | uses: peaceiris/actions-gh-pages@v3 58 | with: 59 | publish_dir: ./dist 60 | publish_branch: gh-pages 61 | github_token: ${{ secrets.GITHUB_TOKEN }} 62 | 63 | - name: pull request on npm update 64 | if: env.UPDATE_NPM == 'true' 65 | uses: peter-evans/create-pull-request@v4 66 | with: 67 | token: ${{ secrets.GITHUB_TOKEN }} 68 | commit-message: Update dependencies 69 | title: Update dependencies 70 | body: | 71 | - Dependency updates 72 | 73 | Auto-generated by [create-pull-request][1] 74 | 75 | [1]: https://github.com/peter-evans/create-pull-request 76 | branch: update-dependencies 77 | add-paths: | # to update the package.json and package-lock.json 78 | *.json 79 | 80 | -------------------------------------------------------------------------------- /src/scripts/jsx/fetch.jsx: -------------------------------------------------------------------------------- 1 | import { createSignal, createResource } from 'solid-js'; 2 | import { render, Show } from 'solid-js/web'; 3 | 4 | // Debounce function 5 | function debounce(func, delay) { 6 | let timeoutId; 7 | return function (...args) { 8 | clearTimeout(timeoutId); 9 | timeoutId = setTimeout(() => func.apply(this, args), delay); 10 | }; 11 | } 12 | 13 | const fetchWord = async (word) => { 14 | if (word != '') { 15 | try { 16 | const response = await fetch( 17 | `https://api.dictionaryapi.dev/api/v2/entries/en/${word}/` 18 | ); 19 | if (!response.ok) { 20 | throw new Error(`${word} is not in the dictionary.`); 21 | } 22 | return await response.json(); 23 | } catch (error) { 24 | return { error: error.message }; 25 | } 26 | } 27 | }; 28 | 29 | const App = () => { 30 | const [wordId, setWordId] = createSignal(''); 31 | const [debouncedWordId, setDebouncedWordId] = createSignal(''); 32 | const [text] = createResource(debouncedWordId, fetchWord); 33 | 34 | // Debounce the word input 35 | const debouncedSetWordId = debounce(setDebouncedWordId, 500); 36 | 37 | const handleInput = (e) => { 38 | const newWord = e.currentTarget.value; 39 | setWordId(newWord); 40 | debouncedSetWordId(newWord); 41 | }; 42 | 43 | return ( 44 | <> 45 | 46 | 47 | Loading... 48 | 49 | 50 |
Error: {text().error}
51 |
52 | 0} 54 | > 55 |

Dictionary: {text()[0].word}

56 | 57 | {text()[0].meanings.map((meaning, index) => ( 58 |
59 |

{meaning.partOfSpeech}

60 |
    61 | {meaning.definitions.map((definition, idx) => ( 62 |
  • 63 |

    {definition.definition}

    64 |
  • 65 | ))} 66 |
67 |
68 | ))} 69 | {text()[0].phonetics && text()[0].phonetics.length > 0 && ( 70 | <> 71 |

Dictionary Audio:

72 | 75 | 76 | )} 77 |
78 | 79 | ); 80 | }; 81 | 82 | render(App, document.getElementById('wordapp')); -------------------------------------------------------------------------------- /eleventy.config.js: -------------------------------------------------------------------------------- 1 | const sass = require("sass"); 2 | const pluginWebc = require("@11ty/eleventy-plugin-webc"); 3 | const { EleventyRenderPlugin } = require("@11ty/eleventy"); 4 | const { EleventyHtmlBasePlugin } = require("@11ty/eleventy"); 5 | const now = String(Date.now()); 6 | const solidShortcode = require('./config/shortcodes/solidify.js'); 7 | const esbuildPipeline = require('./config/build/esbuild.js'); 8 | const purgecssPipeline = require('./config/build/purgecss.js'); 9 | const path = require("path"); 10 | const manifest = require('./src/_data/manifest.json'); 11 | const isProd = process.env.ELEVENTY_ENV === 'prod' ? true : false; 12 | 13 | const TEMPLATE_ENGINE = "liquid"; 14 | 15 | module.exports = function (eleventyConfig) { 16 | // DEV SERVER 17 | eleventyConfig.setServerOptions({ 18 | port: 8080, 19 | watch: ["dist/app/*.css", "dist/app/*.js"], 20 | liveReload: true, 21 | domDiff: true, 22 | }); 23 | 24 | // WATCH 25 | // esbuild is also watching the js & jsx files 26 | eleventyConfig.watchIgnores.add("./src/_data/manifest.json"); 27 | eleventyConfig.watchIgnores.add("./src/_data/buildmeta.json"); 28 | 29 | // BUILD HOOK 30 | eleventyConfig.on("eleventy.before", esbuildPipeline); 31 | if (isProd){ 32 | eleventyConfig.on("eleventy.after", purgecssPipeline); 33 | }; 34 | 35 | // PLUGINS 36 | eleventyConfig.addPlugin(pluginWebc, { 37 | components: "src/_includes/components/*.webc", 38 | }); 39 | // to use other templates like liquid and nunjunks 40 | eleventyConfig.addPlugin(EleventyRenderPlugin); 41 | eleventyConfig.addPlugin(EleventyHtmlBasePlugin); 42 | 43 | // SHORTCODES & FILTERS 44 | // Add cache busting by using {{ 'myurl' | version }} 45 | eleventyConfig.addFilter("version", (url) => { 46 | const [urlPart, paramPart] = url.split("?"); 47 | const params = new URLSearchParams(paramPart || ""); 48 | params.set("v", `${now}`); 49 | return `${urlPart}?${params}`; 50 | }); 51 | 52 | // Use this filter only if the asset is processed by esbuild and is in _data/manifest.json. Use {{ 'myurl' | hash }} 53 | eleventyConfig.addFilter("hash", (url) => { 54 | const urlbase = path.basename(url); 55 | const [basePart, ...paramPart] = urlbase.split("."); 56 | const urldir = path.dirname(url); 57 | let hashedBasename = manifest[basePart]; 58 | return `${urldir}/${hashedBasename}`; 59 | }); 60 | 61 | /* Use filter to resolve promises from async functions. No more [object Promise] in your templates. {{ myAsyncFunction() | await }} */ 62 | eleventyConfig.addFilter("await", async promise => { 63 | return promise; 64 | }); 65 | 66 | eleventyConfig.addPairedShortcode("solid", solidShortcode); 67 | 68 | // Let Eleventy transform HTML files as liquidjs 69 | // So that we can use .html instead of .liquid 70 | 71 | return { 72 | dir: { 73 | input: "src", 74 | output: "dist", 75 | data: "_data", 76 | }, 77 | templateFormats: ["html", "md", TEMPLATE_ENGINE], 78 | markdownTemplateEngine: TEMPLATE_ENGINE, 79 | htmlTemplateEngine: TEMPLATE_ENGINE, 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /src/_data/buildmeta.json: -------------------------------------------------------------------------------- 1 | {"inputs":{"node_modules/solid-js/dist/solid.js":{"bytes":52004,"imports":[{"path":"","kind":"import-statement","external":true}],"format":"esm"},"node_modules/solid-js/web/dist/web.js":{"bytes":27923,"imports":[{"path":"node_modules/solid-js/dist/solid.js","kind":"import-statement","original":"solid-js"},{"path":"node_modules/solid-js/dist/solid.js","kind":"import-statement","original":"solid-js"}],"format":"esm"},"src/scripts/jsx/app.jsx":{"bytes":2488,"imports":[{"path":"node_modules/solid-js/web/dist/web.js","kind":"import-statement","original":"solid-js/web"},{"path":"node_modules/solid-js/web/dist/web.js","kind":"import-statement","original":"solid-js/web"},{"path":"node_modules/solid-js/web/dist/web.js","kind":"import-statement","original":"solid-js/web"},{"path":"node_modules/solid-js/dist/solid.js","kind":"import-statement","original":"solid-js"},{"path":"node_modules/solid-js/web/dist/web.js","kind":"import-statement","original":"solid-js/web"}],"format":"esm"},"src/scripts/jsx/fetch.jsx":{"bytes":12289,"imports":[{"path":"node_modules/solid-js/web/dist/web.js","kind":"import-statement","original":"solid-js/web"},{"path":"node_modules/solid-js/web/dist/web.js","kind":"import-statement","original":"solid-js/web"},{"path":"node_modules/solid-js/web/dist/web.js","kind":"import-statement","original":"solid-js/web"},{"path":"node_modules/solid-js/web/dist/web.js","kind":"import-statement","original":"solid-js/web"},{"path":"node_modules/solid-js/web/dist/web.js","kind":"import-statement","original":"solid-js/web"},{"path":"node_modules/solid-js/web/dist/web.js","kind":"import-statement","original":"solid-js/web"},{"path":"node_modules/solid-js/web/dist/web.js","kind":"import-statement","original":"solid-js/web"},{"path":"node_modules/solid-js/dist/solid.js","kind":"import-statement","original":"solid-js"},{"path":"node_modules/solid-js/web/dist/web.js","kind":"import-statement","original":"solid-js/web"},{"path":"","kind":"import-statement","external":true}],"format":"cjs"},"node_modules/@11ty/is-land/is-land.js":{"bytes":8708,"imports":[{"path":"","kind":"import-statement","external":true}],"format":"esm"},"src/scripts/js/is-land.js":{"bytes":34,"imports":[{"path":"node_modules/@11ty/is-land/is-land.js","kind":"import-statement","original":"@11ty/is-land/is-land.js"}],"format":"esm"},"dist/app/style.css":{"bytes":10358,"imports":[]}},"outputs":{"dist/app/app-S5HW3ASK.min.js":{"imports":[],"exports":[],"entryPoint":"src/scripts/jsx/app.jsx","inputs":{"node_modules/solid-js/dist/solid.js":{"bytesInOutput":7272},"node_modules/solid-js/web/dist/web.js":{"bytesInOutput":3748},"src/scripts/jsx/app.jsx":{"bytesInOutput":226}},"bytes":11697},"dist/app/fetch-BUN3JDA4.min.js":{"imports":[],"exports":[],"entryPoint":"src/scripts/jsx/fetch.jsx","inputs":{"node_modules/solid-js/dist/solid.js":{"bytesInOutput":9600},"node_modules/solid-js/web/dist/web.js":{"bytesInOutput":4468},"src/scripts/jsx/fetch.jsx":{"bytesInOutput":1657}},"bytes":16493},"dist/app/is-land-TXS7GROR.min.js":{"imports":[],"exports":[],"entryPoint":"src/scripts/js/is-land.js","inputs":{"node_modules/@11ty/is-land/is-land.js":{"bytesInOutput":3959},"src/scripts/js/is-land.js":{"bytesInOutput":0}},"bytes":4344},"dist/app/style-ZSG4TA35.min.css":{"imports":[],"entryPoint":"dist/app/style.css","inputs":{"dist/app/style.css":{"bytesInOutput":9926}},"bytes":9927}}} -------------------------------------------------------------------------------- /src/_includes/solid-base.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /config/build/esbuild.js: -------------------------------------------------------------------------------- 1 | // https://www.seancdavis.com/posts/javascript-for-11ty-with-esbuild/ 2 | const esbuild = require('esbuild'); 3 | const isProd = process.env.ELEVENTY_ENV === 'prod' ? true : false; 4 | const isDev = process.env.ELEVENTY_ENV === 'dev' ? true : false; 5 | const { solidPlugin } = require('esbuild-plugin-solid'); 6 | const manifestPlugin = require('esbuild-plugin-manifest'); 7 | const gzipPlugin = require('@luncheon/esbuild-plugin-gzip'); 8 | const { http, default_schemes } = require('@hyrious/esbuild-plugin-http'); 9 | // cacheMap stores { url => contents }, you can easily persist it in file system - see https://github.com/hyrious/esbuild-plugin-http 10 | let cacheMap = new Map(); 11 | const fs = require('fs'); 12 | const path = require("path"); 13 | 14 | // Get arguments from npm script (such as --pathprefix) - https://reflect.run/articles/sending-command-line-arguments-to-an-npm-script/ 15 | const parseArgs = (args) => { 16 | const parsedArgs = {}; 17 | 18 | args.forEach((arg) => { 19 | const parts = arg.split("="); 20 | 21 | parsedArgs[parts[0].slice(2)] = parts[1]; 22 | }); 23 | 24 | return parsedArgs; 25 | }; 26 | 27 | const npmScriptArgs = parseArgs(process.argv); 28 | 29 | // pathPrefix and defineEnv const's access the environment variable PATHPREFIX set by the npm scripts (in the package.json) which is passed to solid-js by esbuild.js. Esbuild defines the environmental variables to pass through to solid-js app using the define config. 30 | const pathPrefix = npmScriptArgs.pathprefix || ''; 31 | 32 | const defineEnv = { 33 | 'process.env.PATHPREFIX': JSON.stringify(pathPrefix), 34 | // Add other environment variables as needed 35 | }; 36 | 37 | const esbuildOpts = { 38 | entryPoints: ['src/scripts/jsx/*.jsx', 'src/scripts/js/*.js', 'dist/app/*.css'], // include css so that its in the manifest.json 39 | entryNames: isProd ? '[name]-[hash]' : '[name]', 40 | outExtension: isProd ? {'.js': '.min.js', '.css': '.min.css'} : {'.js': '.js', '.css': '.css'}, 41 | allowOverwrite: !isProd, // overwrite dist/app/style.css when in dev mode 42 | bundle: true, 43 | minify: isProd, 44 | write: !isProd, // this is required for the gzipPlugin to work 45 | treeShaking: isProd, 46 | outdir: './dist/app', 47 | sourcemap: !isProd, 48 | target: isProd ? 'es6' : 'esnext', 49 | metafile: true, 50 | define: defineEnv, 51 | plugins: [ 52 | // To run development/staging build (skips purgingcss) if isProd = false when ELEVENTY_ENV != 'prod'. 53 | // This is implimented in the package.json scripts 54 | http({ 55 | filter: (url) => true, 56 | schemes: { default_schemes }, 57 | cache: cacheMap 58 | }), 59 | solidPlugin(), 60 | manifestPlugin({ 61 | // NOTE: Save to src/_data. This is always relative to `outdir`. 62 | filename: '../../src/_data/manifest.json', 63 | shortNames: true, 64 | extensionless: 'input', 65 | // Generate manifest.json - https://github.com/pellebjerkestrand/pokesite/blob/main/source/build/build-client.js 66 | generate: (entries) => 67 | Object.fromEntries( 68 | Object.entries(entries).map(([from, to]) => [ 69 | from, 70 | `${path.basename(to)}`, 71 | ]) 72 | ), 73 | }) 74 | ] 75 | } 76 | 77 | // If isProd include gzipPlugin. This is pushed into esBuildOpts.plugins because in dev/staging mode the esBuild's write api must be true. But the gzipPlugin requires it to be false. 78 | if (isProd) { 79 | esbuildOpts.plugins.push(gzipPlugin({ 80 | uncompressed: isProd, 81 | gzip: isProd, 82 | brotli: isProd, 83 | })); 84 | } 85 | 86 | module.exports = async () => { 87 | let ctx = await esbuild.context({ 88 | ...esbuildOpts, 89 | }).catch((error) => { 90 | console.error(error); 91 | process.exitCode = 1; 92 | }) 93 | if (isDev === true){ 94 | // Enable watch mode - NOTE buildmeta.json is not generated when watching 95 | // Need to limit esbuild so only watching js & jsx 96 | await ctx.watch(); 97 | console.log("[esbuild] is watching for changes..."); 98 | } else { 99 | // Build once and exit if not watch mode 100 | await ctx.rebuild().then(result => { 101 | ctx.dispose(); 102 | fs.writeFileSync('./src/_data/buildmeta.json', JSON.stringify(result.metafile)); 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 11ty solid base 3 |
4 |
5 | 6 | A minimal base HTML5 template and the [esbuild](https://esbuild.github.io/) setup to **compile your SolidJS app within 11ty**. 7 | 8 | Includes: 9 | - [11ty/is-land](https://www.11ty.dev/dist/plugins/partial-hydration/) 10 | - [WebC](https://www.11ty.dev/dist/languages/webc/), 11 | - [esbuild](https://esbuild.github.io) 12 | - Minifiying and autoprefixing of styles using [Lightning CSS](https://lightningcss.dev/) 13 | - Uses [Purgecss](https://purgecss.com/) to remove unused styles via a slightly modified version of [esbuild-plugin-purgecss-2](https://github.com/arslanakram/esbuild-plugin-purgecss-2.0/blob/master/src/index.js) 14 | - A [shortcode](https://github.com/woodcox/11ty-solid-base/blob/main/config/shortcodes/solidify.js) to compile SolidJS inline 15 | - Cashebusting via an esbuild generated hash. 16 | - A [manifest.json file and a buildmeta.json file](https://github.com/woodcox/11ty-solid-base/tree/main/src/_data) 17 | - You can also import HTTP URLs into JavaScript code using [esbuild-plugin-http](https://github.com/hyrious/esbuild-plugin-http). 18 | 19 | ## Compile Solidjs to js 20 | Add `your_solid.jsx` file to the `src/scripts/jsx` or the `src/scripts/js` folders. Esbuild will output a minified js file. To configure esbuild modify `config/build/esbuild.js`. 21 | 22 | ## Shortcode 23 | If you need to compile your js script inline, use this shortcode: 24 | 25 | ~~~liquid 26 | {% solid "filename", "bundled" %} 27 | your.solid.jsx.code 28 | {% endsolid %} 29 | ~~~ 30 | 31 | The shortcode will generate a module script tag. 32 | 33 | ~~~html 34 | 35 | ~~~ 36 | 37 | ### Arguments 38 | There are two arguments: 39 | - `filename` (required): The name of the file which is saved to `dist/app`. This name is automatically prefixed by `solid-`. 40 | - `bundled` (optional): The solid.jsx is bundled by default. To switch bundling off pass the value: `"bundleOff"`. 41 | 42 | ## SolidJS configuration 43 | To configure esbuild for js/jsx files modify `config/build/esbuild.js` or to configure the shortcode, modify `config/shortcode/solidify.js`. For further info check out the [esbuild-plugin-solid](https://github.com/amoutonbrady/esbuild-plugin-solid) github repo by [amoutonbrady](https://amoutonbrady.dev/). 44 | 45 | ~~~js 46 | /** Configuration options for esbuild-plugin-solid */ 47 | export interface Options { 48 | /** The options to use for @babel/preset-typescript @default {} */ 49 | typescript: object 50 | /** 51 | * Pass any additional babel transform options. They will be merged with 52 | * the transformations required by Solid. 53 | * 54 | * @default {} 55 | */ 56 | babel: 57 | | TransformOptions 58 | | ((source: string, id: string, ssr: boolean) => TransformOptions) 59 | | ((source: string, id: string, ssr: boolean) => Promise); 60 | /** 61 | * Pass any additional [babel-plugin-jsx-dom-expressions](https://github.com/ryansolid/dom-expressions/tree/main/packages/babel-plugin-jsx-dom-expressions#plugin-options). 62 | * They will be merged with the defaults sets by [babel-preset-solid](https://github.com/solidjs/solid/blob/main/packages/babel-preset-solid/index.js#L8-L25). 63 | * 64 | * @default {} 65 | */ 66 | solid: { 67 | /** 68 | * The name of the runtime module to import the methods from. 69 | * 70 | * @default "solid-js/web" 71 | */ 72 | moduleName?: string; 73 | 74 | /** 75 | * The output mode of the compiler. 76 | * Can be: 77 | * - "dom" is standard output 78 | * - "ssr" is for server side rendering of strings. 79 | * - "universal" is for using custom renderers from solid-js/universal 80 | * 81 | * @default "dom" 82 | */ 83 | generate?: 'ssr' | 'dom' | 'universal'; 84 | 85 | /** 86 | * Indicate whether the output should contain hydratable markers. 87 | * 88 | * @default false 89 | */ 90 | hydratable?: boolean; 91 | 92 | /** 93 | * Boolean to indicate whether to enable automatic event delegation on camelCase. 94 | * 95 | * @default true 96 | */ 97 | delegateEvents?: boolean; 98 | 99 | /** 100 | * Boolean indicates whether smart conditional detection should be used. 101 | * This optimizes simple boolean expressions and ternaries in JSX. 102 | * 103 | * @default true 104 | */ 105 | wrapConditionals?: boolean; 106 | 107 | /** 108 | * Boolean indicates whether to set current render context on Custom Elements and slots. 109 | * Useful for seemless Context API with Web Components. 110 | * 111 | * @default true 112 | */ 113 | contextToCustomElements?: boolean; 114 | 115 | /** 116 | * Array of Component exports from module, that aren't included by default with the library. 117 | * This plugin will automatically import them if it comes across them in the JSX. 118 | * 119 | * @default ["For","Show","Switch","Match","Suspense","SuspenseList","Portal","Index","Dynamic","ErrorBoundary"] 120 | */ 121 | builtIns?: string[]; 122 | }; 123 | } 124 | ~~~ 125 | 126 | ## Cachebusting hash filter 127 | 128 | Esbuild is configured to add a hash to the CSS and JS files it processes in the `src/scripts/jsx`, `src/scripts/js` and the `dist/app/css` folders (it purges the prefixed output of the scss in situ). It outputs a `manifest.json` file to the `src/_data` directory. 129 | The manifest.json file is used in the hash filter to modify the URL src or href in the html: 130 | 131 | ~~~html 132 | 133 | 134 | ~~~ 135 | 136 | As a bonus if the file has been minified in production it will alter the file extension to `[hash]-min.js` or `[hash]-min.css`, for example: 137 | 138 | ~~~html 139 | 140 | ~~~ 141 | 142 | ## Purgecss 143 | Your css files will be automatically purged of unused css in production. To configure the purgecssPlugin modify the `config/build/purgecss.js` file. You can use any configuration pattern from [purgecss](https://purgecss.com/configuration.html), but you don't need to set the `css:` options as this is automatically included from the buildmeta.json. 144 | 145 | ~~~js 146 | plugins: [ 147 | ... 148 | purgecssPlugin({ 149 | content: ["dist/index.html"] 150 | }), 151 | ] 152 | ~~~ 153 | 154 | ## Environment variables 155 | The `pathPrefix` npm script argument from 11ty (`--pathprefix=form-2-pdf", 156 | `) is passed through as an environment variable (along with all the npm script arguments) using [esbuild's define api](https://esbuild.github.io/api/#define). You can add other environment variables by adding them to the defineEnv const in the `config/build/esbuild.js` script. 157 | 158 | ~~~js 159 | const defineEnv = { 160 | 'process.env.PATHPREFIX': JSON.stringify(pathPrefix), 161 | // Add other environment variables as needed 162 | }; 163 | ~~~ 164 | 165 | If you decide to use a client-side router such as [solid router](https://github.com/solidjs/solid-router) you could do the following in your jsx: 166 | 167 | ~~~jsx 168 | const pathPrefix = process.env.PATHPREFIX; 169 | const urlPrefix = pathPrefix ? `/${pathPrefix}` : ""; 170 | 171 | render( 172 | () => ( 173 | 174 | {/* solid-js router uses urlPrefix here to set the url path */} 175 | 176 | 177 | 178 | ), 179 | document.getElementById('app') 180 | ); 181 | ~~~ 182 | 183 | ## Compression - Gzip and Brotli 184 | 185 | In a production build the css and js files are automatically compressed and output as minified, gzipped and brotli files: 186 | 187 | - `app-S5YUTCHU.min.js` 188 | - `app-S5YUTCHU.min.js.br` 189 | - `app-S5YUTCHU.min.js.gz` 190 | 191 | To alter this behaviour modifiy the following in the `config/build/esbuild.js` script: 192 | 193 | ~~~js 194 | if (isProd) { 195 | esbuildOpts.plugins.push(gzipPlugin({ 196 | uncompressed: isProd, 197 | gzip: isProd, 198 | brotli: isProd, 199 | })); 200 | } 201 | ~~~ 202 | 203 | ## Development Scripts 204 | 205 | **`npm start`** 206 | 207 | > Run 11ty with hot reload at localhost:8080, including reload based on Sass and JS changes. 208 | 209 | **`npm run cloud`** 210 | 211 | > Development build for use on cloud IDE's such as [Stackblitz](https://stackblitz.com/) without the pathprefix of /11ty-solid-base/. The CSS is autoprefixed but no minification or purging happens. 212 | 213 | If your using [Stackblitz](https://stackblitz.com/). To start the cloud dev server run: `npm run cloud` then `npm run start`. 214 | 215 | **`npm run build`** 216 | 217 | > Staging build with autoprefixed CSS but no minification or purging. 218 | 219 | **`npm run minify`** 220 | 221 | > Production build includes minified, autoprefixed and purged CSS 222 | 223 | Use this as the "Publish command" if needed by hosting such as Netlify. 224 | 225 | 226 | ## Need scoped CSS? 227 | [Lightningcss](https://lightningcss.dev/css-modules.html) can be configured to support css modules by adding `--css-modules` to the `prefix` npm script. Alternatively you could use [esbuild css modules plugin](https://github.com/indooorsman/esbuild-css-modules-plugin#readme). An example [setup of scoped css modules with esbuild](https://how-to.dev/how-to-set-up-css-modules-with-esbuild). 228 | 229 | 230 | ## To do 231 | - Look at adding js import maps 232 | - The web component (webC example) 233 | - improve styling and make prettier hydration examples 234 | - Example using [Solid Element](https://github.com/solidjs/solid/blob/main/packages/solid-element/README.md) web components 235 | - Maybe look as using [eleventy-plugin-bundle](https://github.com/11ty/eleventy-plugin-bundle) and esbuild to create per page solidjs asset buckets/bundles. 236 | 237 | 238 | ### Planning for 2.0 release 239 | - Alter the solid shortcode API so that the arguements are `bundle` and bundleOff instead of the current API of `bundled` and `bundleOff`. 240 | - Change the esbuild/solid internal configuration so that its an eleventy-plugin instead. 241 | - Should I remove the sass dependency? 242 | -------------------------------------------------------------------------------- /src/style/style.scss: -------------------------------------------------------------------------------- 1 | /* Based on MVP.css v1.14 - https://github.com/andybrewer/mvp */ 2 | @use 'sass:math'; 3 | // ============== 4 | // Sass variables 5 | // ============== 6 | $scrollbar-color: rgb(202, 202, 232); 7 | 8 | 9 | :root { 10 | --active-brightness: 0.85; 11 | --border-radius: 5px; 12 | --box-shadow: 2px 2px 10px; 13 | --color-accent: #118bee15; 14 | --color-bg: #fff; 15 | --color-bg-secondary: #e9e9e9; 16 | --color-link: #118bee; 17 | --color-secondary: rebeccapurple; 18 | --gradient: conic-gradient(from .5turn at bottom left, deeppink, rebeccapurple); 19 | --color-secondary-accent: #920de90b; 20 | --color-shadow: #f4f4f4; 21 | --color-table: #118bee; 22 | --color-text: #000; 23 | --color-text-secondary: #999; 24 | --font-family: Verdana, Geneva, DejaVu Sans, sans-serif; 25 | --hover-brightness: 1.2; 26 | --justify-important: center; 27 | --justify-normal: left; 28 | --line-height: 1.5; 29 | --width-card: 285px; 30 | --width-card-medium: 460px; 31 | --width-card-wide: 800px; 32 | --width-content: 1080px; 33 | --bg-cube-1: #dee2e6; // grey-3 34 | --bg-cube-2: #ffffff; // white 35 | --bg-cube-3: #f1f3f5; // gray-1 36 | --text-size--2: clamp(0.7813rem, 0.7747rem + 0.0326vw, 0.8rem); 37 | --text-size--1: clamp(0.9375rem, 0.9158rem + 0.1087vw, 1rem); 38 | --text-size-base: clamp(1.125rem, 1.0815rem + 0.2174vw, 1.25rem); 39 | --text-size-1: clamp(1.35rem, 1.2761rem + 0.3696vw, 1.5625rem); 40 | --text-size-2: clamp(1.62rem, 1.5041rem + 0.5793vw, 1.9531rem); 41 | --text-size-3: clamp(1.9438rem, 1.7707rem + 0.8652vw, 2.4413rem); 42 | --text-size-4: clamp(2.3325rem, 2.0823rem + 1.2511vw, 3.0519rem); 43 | --text-size-5: clamp(2.7994rem, 2.4461rem + 1.7663vw, 3.815rem); 44 | --text-size-6: clamp(3.3594rem, 2.8694rem + 2.45vw, 4.7681rem); 45 | --box-density-size: 0.3rem; 46 | } 47 | 48 | @media (prefers-color-scheme: dark) { 49 | :root[color-mode='user'] { 50 | --color-accent: #0097fc4f; 51 | --color-bg: #333; 52 | --color-bg-secondary: #555; 53 | --color-link: #0097fc; 54 | --color-secondary: #e20de9; 55 | --color-secondary-accent: #e20de94f; 56 | --color-shadow: #bbbbbb20; 57 | --color-table: #0097fc; 58 | --color-text: #f7f7f7; 59 | --color-text-secondary: #aaa; 60 | --bg-cube-1: #212529; // grey-9 61 | --bg-cube-2: #000000; // black 62 | --bg-cube-3: #0d0f12; // gray-11 63 | } 64 | } 65 | 66 | /* 67 | Mixin to create selector with density shifts to control spacing - see https://complementary.space/ and https://github.com/damato-design/system/blob/main/src/decorations/density.scss 68 | 69 | body { ... } 70 | body [data-density-shift] { ... } 71 | body [data-density-shift] [data-density-shift] { ... } 72 | 73 | */ 74 | 75 | $spacescale: 2; 76 | $typescale: 1.25; 77 | 78 | @function tofixed($value, $decimal-place: 3) { 79 | $pow: math.pow(10, $decimal-place); 80 | @return math.div(round($value * $pow), $pow); 81 | } 82 | 83 | @function space-calc($n) { 84 | $pow: math.pow($spacescale, $n); 85 | @return tofixed($pow); 86 | } 87 | 88 | @function type-calc($n) { 89 | $pow: math.pow($typescale, $n); 90 | @return tofixed($pow); 91 | } 92 | 93 | @mixin spacing-vars($selector, $levels, $attr-sel: '[data-density-shift]') { 94 | @for $i from 1 through $levels { 95 | $nest-sel: if($i == 1, $selector, selector-nest($nest-sel, $attr-sel)); 96 | 97 | #{$nest-sel} { 98 | font-size: calc(#{type-calc($levels - $i)} * var(--text-size-base, 1rem)); 99 | --text-display: min(#{type-calc(($levels - $i) + 5)} * var(--text-size-base, 1rem), var(--text-size-min, 6vw)); 100 | --text-heading: calc(#{type-calc(($levels - $i) + 1)} * var(--text-size-base, 1rem)); 101 | --text-detail: calc(#{type-calc(($levels - $i) - 1)} * var(--text-size-base, 1rem)); 102 | --space-near: calc(#{space-calc($levels - $i)} * var(--box-density-size, 0.5rem)); 103 | --space-away: calc(#{space-calc(($levels - $i) + 1)} * var(--box-density-size, 0.5rem)); 104 | } 105 | } 106 | } 107 | 108 | @include spacing-vars('body', 3); 109 | 110 | 111 | // ============== 112 | // Background 113 | // ============== 114 | html { 115 | --s: 175px; /* control the size */ 116 | --c1: var(--bg-cube-1); 117 | --c2: var(--bg-cube-2); 118 | --c3: var(--bg-cube-3); 119 | --_g: var(--c3) 0 120deg, #0000 0; 120 | background: conic-gradient(from -60deg at 50% calc(100% / 3), var(--_g)), 121 | conic-gradient(from 120deg at 50% calc(200% / 3), var(--_g)), 122 | conic-gradient( 123 | from 60deg at calc(200% / 3), 124 | var(--c3) 60deg, 125 | var(--c2) 0 120deg, 126 | #0000 0 127 | ), 128 | conic-gradient(from 180deg at calc(100% / 3), var(--c1) 60deg, var(--_g)), 129 | linear-gradient( 130 | 90deg, 131 | var(--c1) calc(100% / 6), 132 | var(--c2) 0 50%, 133 | var(--c1) 0 calc(500% / 6), 134 | var(--c2) 0 135 | ); 136 | background-size: calc(1.732 * var(--s)) var(--s); 137 | } 138 | 139 | html { 140 | scroll-behavior: smooth; 141 | } 142 | 143 | @media (prefers-reduced-motion: reduce) { 144 | html { 145 | scroll-behavior: auto; 146 | } 147 | } 148 | 149 | // ============== 150 | // Layout 151 | // ============== 152 | article aside { 153 | background: var(--color-secondary-accent); 154 | border-left: 4px solid var(--color-secondary); 155 | padding: 0.01rem 0.8rem; 156 | } 157 | 158 | body { 159 | //background: var(--color-bg); 160 | color: var(--color-text); 161 | font-family: var(--font-family); 162 | line-height: var(--line-height); 163 | margin: 0; 164 | overflow-x: hidden; 165 | padding: 0; 166 | } 167 | 168 | header, 169 | main { 170 | max-width: var(--width-content); 171 | } 172 | 173 | footer, 174 | header, 175 | main { 176 | margin: 0 auto; 177 | padding: var(--space-away) var(--space-near); 178 | } 179 | 180 | footer { 181 | background-color: var(--color-secondary); 182 | background-image: var(--gradient); 183 | } 184 | 185 | hr { 186 | background-color: var(--color-bg-secondary); 187 | border: none; 188 | height: 1px; 189 | margin: 4rem 0; 190 | width: 100%; 191 | } 192 | 193 | section { 194 | display: flex; 195 | flex-wrap: wrap; 196 | justify-content: var(--justify-important); 197 | } 198 | 199 | section img, 200 | article img { 201 | max-width: 100%; 202 | } 203 | 204 | section pre { 205 | overflow: auto; 206 | } 207 | 208 | section aside { 209 | border: 1px solid var(--color-bg-secondary); 210 | border-radius: var(--border-radius); 211 | box-shadow: var(--box-shadow) var(--color-shadow); 212 | margin: 1rem; 213 | padding: 1.25rem; 214 | width: var(--width-card); 215 | } 216 | 217 | section aside:hover { 218 | box-shadow: var(--box-shadow) var(--color-bg-secondary); 219 | } 220 | 221 | [hidden] { 222 | display: none; 223 | } 224 | 225 | // ============== 226 | // Headers 227 | // ============== 228 | article header, 229 | div header, 230 | main header { 231 | padding-top: 0; 232 | } 233 | 234 | header { 235 | text-align: var(--justify-important); 236 | } 237 | 238 | header a b, 239 | header a em, 240 | header a i, 241 | header a strong { 242 | margin-left: var(--space-near); 243 | margin-right: 0.5rem; 244 | } 245 | 246 | header nav img { 247 | margin: 1rem 0; 248 | } 249 | 250 | section header { 251 | padding-top: 0; 252 | width: 100%; 253 | } 254 | 255 | // ============== 256 | // Nav 257 | // ============== 258 | nav { 259 | align-items: center; 260 | display: flex; 261 | font-weight: bold; 262 | justify-content: space-between; 263 | margin-bottom: 7rem; 264 | } 265 | 266 | nav ul { 267 | list-style: none; 268 | padding: 0; 269 | } 270 | 271 | nav ul li { 272 | display: inline-block; 273 | margin: 0 0.5rem; 274 | position: relative; 275 | text-align: left; 276 | } 277 | 278 | // ============== 279 | // Nav Dropdown 280 | // ============== 281 | 282 | nav ul li:hover ul { 283 | display: block; 284 | } 285 | 286 | nav ul li ul { 287 | background: var(--color-bg); 288 | border: 1px solid var(--color-bg-secondary); 289 | border-radius: var(--border-radius); 290 | box-shadow: var(--box-shadow) var(--color-shadow); 291 | display: none; 292 | height: auto; 293 | left: -2px; 294 | padding: 0.5rem 1rem; 295 | position: absolute; 296 | top: 1.7rem; 297 | white-space: nowrap; 298 | width: auto; 299 | z-index: 1; 300 | } 301 | 302 | nav ul li ul::before { 303 | /* fill gap above to make mousing over them easier */ 304 | content: ''; 305 | position: absolute; 306 | left: 0; 307 | right: 0; 308 | top: -0.5rem; 309 | height: 0.5rem; 310 | } 311 | 312 | nav ul li ul li, 313 | nav ul li ul li a { 314 | display: block; 315 | } 316 | 317 | // ============== 318 | // Typography 319 | // ============== 320 | code, 321 | samp { 322 | background-color: var(--color-accent); 323 | border-radius: var(--border-radius); 324 | color: var(--color-text); 325 | display: inline-block; 326 | margin: 0 0.1rem; 327 | padding: 0 0.5rem; 328 | } 329 | 330 | details { 331 | margin: 1.3rem 0; 332 | } 333 | 334 | details summary { 335 | font-weight: bold; 336 | cursor: pointer; 337 | } 338 | 339 | 340 | 341 | h1, 342 | h2, 343 | h3, 344 | h4, 345 | h5, 346 | h6 { 347 | line-height: var(--line-height); 348 | text-wrap: balance; 349 | margin-block-end: 0; 350 | } 351 | 352 | h1 { 353 | font-size: var(--text-size-5); 354 | } 355 | 356 | h2 { 357 | font-size: var(--text-size-4); 358 | } 359 | 360 | h3 { 361 | font-size: var(--text-size-3); 362 | } 363 | 364 | mark { 365 | padding: 0.1rem; 366 | } 367 | 368 | ol li, 369 | ul li { 370 | padding: var(--space-near) 0; 371 | } 372 | 373 | p { 374 | margin: var(--space-near) 0; 375 | padding: 0; 376 | width: 100%; 377 | font-size: var(--text-size-base); 378 | } 379 | 380 | pre { 381 | margin: 1rem 0; 382 | max-width: var(--width-card-wide); 383 | padding: 1rem 0; 384 | } 385 | 386 | pre code, 387 | pre samp { 388 | display: block; 389 | max-width: var(--width-card-wide); 390 | padding: 0.5rem 2rem; 391 | white-space: pre-wrap; 392 | } 393 | 394 | small { 395 | color: var(--color-text-secondary); 396 | } 397 | 398 | sup { 399 | background-color: var(--color-secondary); 400 | border-radius: var(--border-radius); 401 | color: var(--color-bg); 402 | font-size: xx-small; 403 | font-weight: bold; 404 | margin: 0.2rem; 405 | padding: 0.2rem 0.3rem; 406 | position: relative; 407 | top: -2px; 408 | } 409 | 410 | // ============== 411 | // Links 412 | // ============== 413 | a { 414 | color: var(--color-link); 415 | display: inline-block; 416 | font-weight: bold; 417 | text-decoration: underline; 418 | } 419 | 420 | a:active { 421 | filter: brightness(var(--active-brightness)); 422 | } 423 | 424 | a:hover { 425 | filter: brightness(var(--hover-brightness)); 426 | } 427 | 428 | a b, 429 | a em, 430 | a i, 431 | a strong, 432 | button, 433 | input[type='submit'] { 434 | border-radius: var(--border-radius); 435 | display: inline-block; 436 | font-size: medium; 437 | font-weight: bold; 438 | line-height: var(--line-height); 439 | margin: 0.5rem 0; 440 | padding: 1rem 2rem; 441 | } 442 | 443 | button, 444 | input[type='submit'] { 445 | font-family: var(--font-family); 446 | } 447 | 448 | button:active, 449 | input[type='submit']:active { 450 | filter: brightness(var(--active-brightness)); 451 | } 452 | 453 | button:hover, 454 | input[type='submit']:hover { 455 | cursor: pointer; 456 | filter: brightness(var(--hover-brightness)); 457 | } 458 | 459 | a b, 460 | a strong, 461 | button, 462 | input[type='submit'] { 463 | background-color: var(--color-link); 464 | border: 2px solid var(--color-link); 465 | color: var(--color-bg); 466 | } 467 | 468 | a em, 469 | a i { 470 | border: 2px solid var(--color-link); 471 | border-radius: var(--border-radius); 472 | color: var(--color-link); 473 | display: inline-block; 474 | padding: 1rem 2rem; 475 | } 476 | 477 | article aside a { 478 | color: var(--color-secondary); 479 | } 480 | 481 | // ============== 482 | // Images 483 | // ============== 484 | figure { 485 | margin: 0; 486 | padding: 0; 487 | } 488 | 489 | figure img { 490 | max-width: 100%; 491 | } 492 | 493 | figure figcaption { 494 | color: var(--color-text-secondary); 495 | } 496 | 497 | // ============== 498 | // Forms 499 | // ============== 500 | button:disabled, 501 | input:disabled { 502 | background: var(--color-bg-secondary); 503 | border-color: var(--color-bg-secondary); 504 | color: var(--color-text-secondary); 505 | cursor: not-allowed; 506 | } 507 | 508 | button[disabled]:hover, 509 | input[type='submit'][disabled]:hover { 510 | filter: none; 511 | } 512 | 513 | form { 514 | border: 1px solid var(--color-bg-secondary); 515 | border-radius: var(--border-radius); 516 | box-shadow: var(--box-shadow) var(--color-shadow); 517 | display: block; 518 | max-width: var(--width-card-wide); 519 | min-width: var(--width-card); 520 | padding: 1.5rem; 521 | text-align: var(--justify-normal); 522 | } 523 | 524 | form header { 525 | margin: 1.5rem 0; 526 | padding: 1.5rem 0; 527 | } 528 | 529 | input, 530 | label, 531 | select, 532 | textarea { 533 | display: block; 534 | font-size: inherit; 535 | max-width: var(--width-card-wide); 536 | } 537 | 538 | input[type='checkbox'], 539 | input[type='radio'] { 540 | display: inline-block; 541 | } 542 | 543 | input[type='checkbox'] + label, 544 | input[type='radio'] + label { 545 | display: inline-block; 546 | font-weight: normal; 547 | position: relative; 548 | top: 1px; 549 | } 550 | 551 | input[type='range'] { 552 | padding: 0.4rem 0; 553 | } 554 | 555 | input, 556 | select, 557 | textarea { 558 | border: 1px solid var(--color-bg-secondary); 559 | border-radius: var(--border-radius); 560 | margin-bottom: 1rem; 561 | padding: 0.4rem 0.8rem; 562 | } 563 | 564 | input[type='text'], 565 | textarea { 566 | width: calc(100% - 1.6rem); 567 | } 568 | 569 | input[readonly], 570 | textarea[readonly] { 571 | background-color: var(--color-bg-secondary); 572 | } 573 | 574 | label { 575 | font-weight: bold; 576 | margin-bottom: 0.2rem; 577 | } 578 | 579 | // ============== 580 | // Popups 581 | // ============== 582 | dialog { 583 | border: 1px solid var(--color-bg-secondary); 584 | border-radius: var(--border-radius); 585 | box-shadow: var(--box-shadow) var(--color-shadow); 586 | position: fixed; 587 | top: 50%; 588 | left: 50%; 589 | transform: translate(-50%, -50%); 590 | width: 50%; 591 | z-index: 999; 592 | } 593 | 594 | // ============== 595 | // Tables 596 | // ============== 597 | table { 598 | border: 1px solid var(--color-bg-secondary); 599 | border-radius: var(--border-radius); 600 | border-spacing: 0; 601 | display: inline-block; 602 | max-width: 100%; 603 | overflow-x: auto; 604 | padding: 0; 605 | white-space: nowrap; 606 | } 607 | 608 | table td, 609 | table th, 610 | table tr { 611 | padding: 0.4rem 0.8rem; 612 | text-align: var(--justify-important); 613 | } 614 | 615 | table thead { 616 | background-color: var(--color-table); 617 | border-collapse: collapse; 618 | border-radius: var(--border-radius); 619 | color: var(--color-bg); 620 | margin: 0; 621 | padding: 0; 622 | } 623 | 624 | table thead th:first-child { 625 | border-top-left-radius: var(--border-radius); 626 | } 627 | 628 | table thead th:last-child { 629 | border-top-right-radius: var(--border-radius); 630 | } 631 | 632 | table thead th:first-child, 633 | table tr td:first-child { 634 | text-align: var(--justify-normal); 635 | } 636 | 637 | table tr:nth-child(even) { 638 | background-color: var(--color-accent); 639 | } 640 | 641 | // ============== 642 | // Quotes 643 | // ============== 644 | blockquote { 645 | display: block; 646 | font-size: x-large; 647 | line-height: var(--line-height); 648 | margin: 1rem auto; 649 | max-width: var(--width-card-medium); 650 | padding: 1.5rem 1rem; 651 | text-align: var(--justify-important); 652 | } 653 | 654 | blockquote footer { 655 | color: var(--color-text-secondary); 656 | display: block; 657 | font-size: var(--text-size--2); 658 | line-height: var(--line-height); 659 | padding: 1.5rem 0; 660 | } 661 | 662 | // ============== 663 | // Scrollbars 664 | // ============== 665 | * { 666 | scrollbar-width: thin; 667 | scrollbar-color: $scrollbar-color auto; 668 | } 669 | 670 | *::-webkit-scrollbar { 671 | width: 5px; 672 | height: 5px; 673 | } 674 | 675 | *::-webkit-scrollbar-track { 676 | background: transparent; 677 | } 678 | 679 | *::-webkit-scrollbar-thumb { 680 | background-color: $scrollbar-color; 681 | border-radius: 10px; 682 | } --------------------------------------------------------------------------------